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" }
}
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
.
$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"
}
$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).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
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