Private/Format-FoFileSize.ps1

function Format-FoFileSize {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [long]$Bytes,
        [ValidateSet('Auto', 'Bytes', 'KB', 'MB', 'GB')]
        [string]$Unit = 'Auto',
        [switch]$IncludeBytes
    )

    $abs = [math]::Abs($Bytes)
    $sign = if ($Bytes -lt 0) { '-' } else { '' }
    $ic = [System.Globalization.CultureInfo]::InvariantCulture

    $resolvedUnit = $Unit
    if ($resolvedUnit -eq 'Auto') {
        if ($abs -ge 1GB) { $resolvedUnit = 'GB' }
        elseif ($abs -ge 1MB) { $resolvedUnit = 'MB' }
        elseif ($abs -ge 1KB) { $resolvedUnit = 'KB' }
        else { $resolvedUnit = 'Bytes' }
    }

    $formatted = switch ($resolvedUnit) {
        'GB' { [string]::Format($ic, '{0:N2} GB', ($abs / 1GB)) }
        'MB' { [string]::Format($ic, '{0:N2} MB', ($abs / 1MB)) }
        'KB' { [string]::Format($ic, '{0:N1} KB', ($abs / 1KB)) }
        default { [string]::Format($ic, '{0:N0} B', $abs) }
    }

    if ($IncludeBytes -and $resolvedUnit -ne 'Bytes') {
        return [string]::Format($ic, '{0}{1} ({2:N0} B)', $sign, $formatted, $Bytes)
    }
    return ($sign + $formatted)
}

function Format-FoSizeChange {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [long]$OriginalSize,
        [Parameter(Mandatory)]
        [long]$FinalSize,
        [ValidateSet('Auto', 'Bytes', 'KB', 'MB', 'GB')]
        [string]$Unit = 'Auto'
    )

    $pct = if ($OriginalSize -gt 0) {
        [math]::Round((1 - $FinalSize / $OriginalSize) * 100, 1)
    }
    else { 0 }

    $ic = [System.Globalization.CultureInfo]::InvariantCulture
    return [string]::Format(
        $ic,
        '{0} -> {1} (-{2:0.#}%)',
        (Format-FoFileSize -Bytes $OriginalSize -Unit $Unit),
        (Format-FoFileSize -Bytes $FinalSize -Unit $Unit),
        $pct
    )
}

function Format-FoProcessArgument {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [AllowNull()]
        [string]$Value
    )

    if ($null -eq $Value) { return '""' }
    if ($Value.Length -eq 0) { return '""' }
    if ($Value -notmatch '[\s"]') { return $Value }

    $sb = New-Object System.Text.StringBuilder
    [void]$sb.Append('"')
    $backslashes = 0
    foreach ($ch in $Value.ToCharArray()) {
        if ($ch -eq '\') {
            $backslashes++
            continue
        }
        if ($ch -eq '"') {
            [void]$sb.Append('\', ($backslashes * 2 + 1))
            [void]$sb.Append('"')
            $backslashes = 0
            continue
        }
        if ($backslashes -gt 0) {
            [void]$sb.Append('\', $backslashes)
            $backslashes = 0
        }
        [void]$sb.Append($ch)
    }
    if ($backslashes -gt 0) {
        [void]$sb.Append('\', ($backslashes * 2))
    }
    [void]$sb.Append('"')
    return $sb.ToString()
}

function Format-FoDuration {
    <#
    .SYNOPSIS
    Formats a duration in milliseconds as either raw milliseconds or a pretty multi-unit string.
 
    .PARAMETER Milliseconds
    Elapsed time in milliseconds (negative values are treated as zero).
 
    .PARAMETER Unit
    Pretty (default) — years/months/days/hours/minutes/seconds/ms with zero parts omitted.
    Milliseconds — always "{n} ms".
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [long]$Milliseconds,
        [ValidateSet('Pretty', 'Milliseconds')]
        [string]$Unit = 'Pretty'
    )

    $ms = [math]::Max(0L, $Milliseconds)
    $ic = [System.Globalization.CultureInfo]::InvariantCulture

    if ($Unit -eq 'Milliseconds') {
        return [string]::Format($ic, '{0:N0} ms', $ms)
    }

    # Approximate calendar units for long durations (plugin steps are usually seconds).
    $msPerDay = 86400000L
    $msPerYear = 365L * $msPerDay
    $msPerMonth = 30L * $msPerDay
    $msPerHour = 3600000L
    $msPerMinute = 60000L
    $msPerSecond = 1000L

    $years = [long]([math]::Floor($ms / $msPerYear))
    $ms = $ms % $msPerYear
    $months = [long]([math]::Floor($ms / $msPerMonth))
    $ms = $ms % $msPerMonth
    $days = [long]([math]::Floor($ms / $msPerDay))
    $ms = $ms % $msPerDay
    $hours = [long]([math]::Floor($ms / $msPerHour))
    $ms = $ms % $msPerHour
    $minutes = [long]([math]::Floor($ms / $msPerMinute))
    $ms = $ms % $msPerMinute
    $seconds = [long]([math]::Floor($ms / $msPerSecond))
    $millis = $ms % $msPerSecond

    $parts = [System.Collections.Generic.List[string]]::new()
    if ($years -gt 0) {
        $parts.Add(('{0} {1}' -f $years, $(if ($years -eq 1) { 'year' } else { 'years' })))
    }
    if ($months -gt 0) {
        $parts.Add(('{0} {1}' -f $months, $(if ($months -eq 1) { 'month' } else { 'months' })))
    }
    if ($days -gt 0) {
        $parts.Add(('{0} {1}' -f $days, $(if ($days -eq 1) { 'day' } else { 'days' })))
    }
    if ($hours -gt 0) {
        $parts.Add(('{0} {1}' -f $hours, $(if ($hours -eq 1) { 'hour' } else { 'hours' })))
    }
    if ($minutes -gt 0) {
        $parts.Add(('{0} {1}' -f $minutes, $(if ($minutes -eq 1) { 'minute' } else { 'minutes' })))
    }
    if ($seconds -gt 0) {
        $parts.Add(('{0} {1}' -f $seconds, $(if ($seconds -eq 1) { 'second' } else { 'seconds' })))
    }
    if ($millis -gt 0 -or $parts.Count -eq 0) {
        $parts.Add([string]::Format($ic, '{0:N0} ms', $millis))
    }

    return ($parts -join ' ')
}