Private/Compare-WingetVersion.ps1

function Compare-WingetVersion {
    <#
    .SYNOPSIS
        Compare two package version strings. Returns -1, 0 or 1.
 
    .DESCRIPTION
        Package versions are not always valid [version] values ("1.2.3.4.5",
        "2024.01", "1.0.0-beta2"), and sorting them as text puts 9.0 above 10.0.
        Segments are split on . - + _ and compared numerically when both are numbers.
        A numeric segment beats a text segment, and a release beats its pre-release
        ("1.0" > "1.0-beta").
    #>

    [CmdletBinding()]
    param(
        [AllowEmptyString()][AllowNull()][string]$ReferenceVersion,
        [AllowEmptyString()][AllowNull()][string]$DifferenceVersion
    )

    # Split on separators, then between letters and digits ("beta10" -> "beta", "10")
    $split = {
        param($v)
        if (-not $v) { return @() }
        @($v.Trim().TrimStart('vV') -split '[.\-+_ ]' | ForEach-Object { $_ -split '(?<=\d)(?=\D)|(?<=\D)(?=\d)' } | Where-Object { $_ -ne '' })
    }
    $a = & $split $ReferenceVersion
    $b = & $split $DifferenceVersion

    $max = [Math]::Max($a.Count, $b.Count)
    for ($i = 0; $i -lt $max; $i++) {
        $x = if ($i -lt $a.Count) { $a[$i] } else { $null }
        $y = if ($i -lt $b.Count) { $b[$i] } else { $null }

        $xNum = $null -ne $x -and $x -match '^\d+$'
        $yNum = $null -ne $y -and $y -match '^\d+$'

        # A missing segment counts as 0 against a number, and as "release" against text
        if ($null -eq $x) { if ($yNum) { $x = '0'; $xNum = $true } else { return 1 } }
        if ($null -eq $y) { if ($xNum) { $y = '0'; $yNum = $true } else { return -1 } }

        if ($xNum -and $yNum) {
            $xt = $x.TrimStart('0'); $yt = $y.TrimStart('0')
            if ($xt.Length -ne $yt.Length) { return [Math]::Sign($xt.Length - $yt.Length) }
            $c = [string]::CompareOrdinal($xt, $yt)
            if ($c -ne 0) { return [Math]::Sign($c) }
        }
        elseif ($xNum) { return 1 }
        elseif ($yNum) { return -1 }
        else {
            $c = [string]::Compare($x, $y, [System.StringComparison]::OrdinalIgnoreCase)
            if ($c -ne 0) { return [Math]::Sign($c) }
        }
    }
    return 0
}