1

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".

UPDATE

A tweak to @zett42 answer below:

function CompareVersionStrings([string]$Version1, [string]$Version2) {

    $v1 = $Version1.Split('.') -replace '^0', '0.'
    $v2 = $Version2.Split('.') -replace '^0', '0.'   

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

     for ($i=0; $i-lt 4; $i++) {      
        switch (($v1[$i].length).CompareTo(($v2[$i].length))) {
            {$_ -lt 0} { $v1[$i] = $v1[$i].PadRight($v2[$i].Length,'0') }
            {$_ -gt 0} { $v2[$i] = $v2[$i].PadRight($v1[$i].Length,'0') }
        }
     }
 
     $v1f = $v1 | % {[float]$_}
     $v2f = $v2 | % {[float]$_}

    return [Collections.StructuralComparisons]::StructuralComparer.Compare( $v1f, $v2f )  
}

$thisversion = "14.1.0.5668"
$nextversion = "14.1.0.56686"

switch (CompareVersionStrings $thisversion $nextversion) {
    {$_ -lt 0} { write-host "$thisversion is less than $nextversion" }
    {$_ -gt 0} { write-host "$thisversion is greater than $nextversion" }
    {$_ -eq 0} { write-host "$thisversion is the same as $nextversion" }
}
Captain_Planet
  • 1,228
  • 1
  • 12
  • 28
  • 1
    "$([version]'14.03.0.0')" → 14.3.0.0 – iRon Aug 18 '22 at 10:16
  • $thisversion = '14.03.0.0' $nextversion = '14.1.0.56686' write-host $thisversion write-host $nextversion if (([version]$nextversion) -lt ([version]$thisversion)) { write-host "$nextversion is less then $thisversion" } – Narayana Lvsl Aug 18 '22 at 10:43
  • 1
    This would be very non-standard, can you explain why you need this? – zett42 Aug 18 '22 at 10:44
  • 1
    @zett42 I've appended the reason to the bottom of the question. – Captain_Planet Aug 18 '22 at 11:56
  • But the question from @zett42 stays: What is the correct scheme for the comparison you expect? Apparently, there is an inconsistency in how you compare the first component (number comparison) in respect to the second component (string comparison). And what about the rest of the components? – iRon Aug 18 '22 at 12:21
  • @iRon I'm working with software vendor logic here - not my own. I think '14.1' is essentially '14.10'. Therefore '03' equates to 3 and '10' equates to 10.... – Captain_Planet Aug 18 '22 at 12:30
  • If you do this also on the first component and compare e.g. `14.03` to `2.1` you should compare `14.03` to `20.10`. Why is the first component treated differently than the second? What about the rest of the components? e.g. `14.1.0.56686` compared to `14.1.0.7` and 14.1.0.06686 compared to `14.1.0.1`? Is that " software vendor logic" defined somewhere? – iRon Aug 18 '22 at 12:57

2 Answers2

2

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
iRon
  • 20,463
  • 10
  • 53
  • 79
  • 1
    `CompareVersionStrings '2.0.0.0' '10.0.0.0'` -> `1`. I don't believe that is to be expected either. Unfortunately OP doesn't explain in detail, how their comparison scheme is supposed to work. Another possible interpretation: Treat version number fields as `[float]` and divide by 10 to the power of . That would make _some_ sense at least. – zett42 Aug 18 '22 at 11:42
  • Yes, 2.0.0.0 should be deemed less than 10.0.0.0. But in this example that doesn't seem to work.... – Captain_Planet Aug 18 '22 at 12:10
0

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
zett42
  • 25,437
  • 3
  • 35
  • 72
  • This is superb....that's exactly what I'm after. Thank you! – Captain_Planet Aug 18 '22 at 14:29
  • @Captain_Planet Great that it's working out for you. I've update the answer with a simpler way to convert from string. – zett42 Aug 18 '22 at 15:16
  • @Captain_Planet Found an even simpler way, see "Simplest Solution" above. – zett42 Aug 18 '22 at 18:47
  • That looks great - I've wrapped it in a function. Out of interest, what do you get if you compare "14.1.0.6" with "14.1.0.56686"? I'd expext "14.1.0.6" to be greater than "14.1.0.56686"? – Captain_Planet Aug 19 '22 at 07:28
  • So I've just done (6).CompareTo(56686), which obviously stacks up. Which makes me think we need to zero pad each part before we compare? So maybe 14.1.0.6 becomes 14.1.0.60000 before we compare? Am going to do a test now.... – Captain_Planet Aug 19 '22 at 07:36
  • @Captain_Planet I'd expect "14.1.0.6" to be less than "14.1.0.56686". Why should the number 6 be considered greater than 56686? I can go along with a scheme that considers numbers with leading zeros to be less (as in this answer), but zero-padding numbers like you suggest seems to be completely random. – zett42 Aug 19 '22 at 07:50
  • I've hacked around with your example and posted it in my question. Hope it makes sense. Tbh, versioning is a confusing subject and I can't find any decent documentation out there to give examples....but there is this: https://en.wikipedia.org/wiki/Software_versioning#:~:text=Software%20versioning%20is%20the%20process,new%20developments%20in%20the%20software. – Captain_Planet Aug 19 '22 at 09:06