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
and14.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) or1
(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