Home > Software engineering >  PowerShell version number comparison and leading zeros
PowerShell version number comparison and leading zeros

Time:08-19

I'm trying to compare a couple of version numbers with a simple PowerShell example. From the below, I would expect $thisversion to be less than $nextversion. But the comparison suggests not? What am i missing? I'm gathering that [version] treats "03" as just "3", but that doesn't solve my problem. How can i factor in leading zeros into version comparison?

$thisversion = "14.03.0.0"
$nextversion = "14.1.0.56686"

write-host $thisversion
write-host $nextversion

if (([version]$thisversion) -lt ([version]$nextversion)) {      
    write-host "$thisversion is less then $nextversion"
}

([version]$thisversion).CompareTo(([version]$nextversion))
#returns 1

The reason for this request is due to sloppy software vendors. I'm sorting through a list of software and trying to work out older versions. In a few cases (for example), "Vendor App 14.03.0.0" is an older version of "Vendor App 14.1.0.56686".

CodePudding user response:

The answer to why this happens is actually in: How can I prevent System.Version from removing leading zeroes?:

That's how system.Version works - it stores the components of the version as separate integers, so there's no distinction between 14.03.0.0 and 14.3.0.01.

If you need to compare it that way you expect it, you might use a function as:

function CompareVersionStrings([string]$Version1, [string]$Version2) {
    $VersionArray1 = $Version1.Split('.')
    $VersionArray2 = $Version2.Split('.')
    for ($i = 0; $i -lt [math]::Max($VersionArray1.count, $VersionArray2.count); $i  ) {
        $Compare = $VersionArray1[$i].CompareTo($VersionArray2[$i])
        if ($Compare) { break } # exit for if the component differs
    }
    $Compare
}

CompareVersionStrings '14.03.0.0' '14.1.0.56686'
-1

CompareVersionStrings '14.3.0.0' '14.1.0.56686'
1

CodePudding user response:

Continuing from my comment, I would convert all fields of the version to [float]. Before conversion, if a version field starts with zero, I would interpret it as a fraction of 1 by inserting a . after the first 0.

So 14.03.0.0 becomes the sequence of floating point numbers 14.0, 0.3, 0.0, 0.0.

Similarly, 14.003.01.0 becomes the sequence of floating point numbers 14.0, 0.03, 0.1, 0.0.

Simplest solution

$thisversion = "14.03.0.0"
$nextversion = "14.1.0.56686"

write-host $thisversion
write-host $nextversion

# Transform the version strings into arrays of floating point numbers,
# which are fractions of 1 if a field starts with '0'.
[float[]] $thisversionArray = $thisversion.Split('.') -replace '^0', '0.'
[float[]] $nextversionArray = $nextversion.Split('.') -replace '^0', '0.'

if( [Collections.StructuralComparisons]::StructuralComparer.Compare( $thisversionArray, $nextversionArray ) -lt 0 ) {
    write-host "$thisversion is less then $nextversion"
}
  • Arrays implement the IStructuralComparable interface, which provides lexicographic comparison. It isn't used by default though, i. e. $array1 -lt $array2 just doesn't work. To use it, we call [Collections.StructuralComparisons]::StructuralComparer.Compare(), which returns -1 (array1 < array2), 0 (array1 = array2) or 1 (array1 > array2).
  • The code assumes that both version numbers have the same number of fields. If they may have different number of fields (e. g. '1.0' vs. '1.0.2'), it would cause an error. To prevent that, use this code to resize the arrays before comparing (which adds 0.0 for missing elements):
    [Array]::Resize( [ref] $thisversionArray, 4 )
    [Array]::Resize( [ref] $nextversionArray, 4 )
    

More elaborate test:

(
    ( '14.03.0.0' , '14.1.0.56686' ),
    ( '14.003.0.0', '14.03.0.0' ),
    ( '14.03.0.0' , '14.02.0.0' ),
    ( '14.03.0.0' , '14.03.0.0' ),
    ( '10.0.0.0'  , '2.0.0.0' ),
    ( '10.0'      , '2.0.0' )     
).ForEach{
    [float[]] $v1 = $_[0].Split('.') -replace '^0', '0.'
    [float[]] $v2 = $_[1].Split('.') -replace '^0', '0.'

    [Array]::Resize( [ref] $v1, 4 )
    [Array]::Resize( [ref] $v2, 4 )

    [PSCustomObject]@{
        Version1 = $_[0]
        Version2 = $_[1]
        CompareResult = [Collections.StructuralComparisons]::StructuralComparer.Compare( $v1, $v2 )    
    }
}

Output:

Version1   Version2     CompareResult
--------   --------     -------------
14.03.0.0  14.1.0.56686            -1
14.003.0.0 14.03.0.0               -1
14.03.0.0  14.02.0.0                1
14.03.0.0  14.03.0.0                0
10.0.0.0   2.0.0.0                  1
10.0       2.0.0                    1

Extended solution

You may want to encapsulate version numbers with [float] fields in a dedicated class, similar to [Version], to be able to use PowerShell's standard comparison operators like -lt, -eq and -gt.

The following class FloatVersion parses version numbers that may contain leading zeros and implements the IComparable interface to support the standard comparison operators.

The floating point numbers that make up the version are stored as [Tuple[float,float,float,float]], which already provides lexicographical comparison.

class FloatVersion : System.IComparable
{
    [Tuple[float,float,float,float]] $Fields

    # Default constructor
    FloatVersion() { $this.Fields = [Tuple]::Create( [float]0.0, [float]0.0, [float]0.0, [float]0.0 ) }

    # Convert from string
    FloatVersion( [string] $version ) {
        # Split version into array of floats. If field starts with '0', it is interpreted as a fraction of 1.
        [float[]] $v = $version.Split('.') -replace '^0', '0.'
        # Ensure array has 4 elements, so we don't get an exception in strict mode.
        [Array]::Resize( [ref] $v, 4 )
        # Convert array to tuple
        $this.Fields = [Tuple]::Create( $v[0], $v[1], $v[2], $v[3] )
    }

    # Implements the IComparable interface
    [int] CompareTo( [object] $obj ) {
        if( $null -eq $obj ) { return 1 }
        $otherVersion = $obj -as [FloatVersion]
        if( $null -ne $otherVersion ) {
            return ([IComparable]$this.Fields).CompareTo( $otherVersion.Fields )
        }
        throw [ArgumentException]::new('Object is not a FloatVersion')      
    }

    # Cheap conversion to string using the tuple's ToString() method.
    # TODO: A more elaborate implementation that reproduces the input string.
    [string] ToString() { return $this.Fields.ToString() }
}

Usage example:

[FloatVersion]'14.03.0.0' -lt [FloatVersion]'14.1.0.56686'
# "True"

More elaborate test:

(
    ( '14.03.0.0' , '14.1.0.56686' ),
    ( '14.003.0.0', '14.03.0.0' ),
    ( '14.03.0.0' , '14.02.0.0' ),
    ( '14.03.0.0' , '14.03.0.0' ),
    ( '10.0.0.0'  , '2.0.0.0' ),
    ( '10.0'      , '2.0.0' )    
).ForEach{
    [PSCustomObject]@{
        Version1 = $_[0]
        Version2 = $_[1]
        CompareResult = ([FloatVersion] $_[0]).CompareTo( [FloatVersion] $_[1] )    
    }
}

Output:

Version1   Version2     CompareResult
--------   --------     -------------
14.03.0.0  14.1.0.56686            -1
14.003.0.0 14.03.0.0               -1
14.03.0.0  14.02.0.0                1
14.03.0.0  14.03.0.0                0
10.0.0.0   2.0.0.0                  1
10.0       2.0.0                    1
  • Related