0

I am trying to port this python module to powershell

def str_to_tuple(s):
    '''        @source kindall at stackoverflow          
        http://stackoverflow.com/a/11887825/3447669
    '''
    return tuple(map(int, (s.split("."))))

End result is I am porting my python code to powershell for using on windows servers, since the rest of the team does not know python.

I have a python script that compares 2 app version numbers (ie 1.2.33 & 1.2.34) The module above give me a tuple that I can compare between 2 version #s > or <.

I'm specifically testing $appVersion -lt $myVersionString which is supplied at runtime, so -eq is not a sufficient test for our script.

I have tried using $str.split() to put the string elements in an array, however comparing doesn't work, and I didn't want to loop through each array to compare each item, since version numbers can be different lengths...

Examples of lengths
1.2 
2.3.4 
2.3.44

Any guidance is gratefully appreciated.

5
  • 3
    why don't you use the [version] dotnet type instead? it understands that 1.2.3.4 is less than 1.19.2.4. Commented Feb 10, 2019 at 2:57
  • @Lee_Dailey Thanks. [version] will solve my issue. I tried it on Powershell Core on my mac and it doesn't work, but I confirmed this helps me for when run on Win machines. It would be awesome to have something portable though. Commented Feb 10, 2019 at 4:18
  • 1
    i had not thot about cross platform needs. [blush] i cannot find any info at all about the [system.version] type in dotnet core ... but the search engine used refuses to search for that exact phrase - only for those two words. ///// you may want to look at "semantic version" over at the powershell gallery. this looks interesting >> PowerShell Gallery | PoshSemanticVersion 1.5.1 — powershellgallery.com/packages/PoshSemanticVersion/1.5.1 Commented Feb 10, 2019 at 4:31
  • Nice! it works great. Here's how I think I will implement it...... $compareResult = Compare-PoshSemanticVersion "1.2.3" "1.2.4" if ($compareResult.Precedence -eq "<") { $true } else { $false } Commented Feb 10, 2019 at 5:28
  • kool! glad to know it works for your situation ... and thank you for the feedback! [grin] Commented Feb 10, 2019 at 5:31

2 Answers 2

2

As @Lee_Dailey pointed out, the simplest solution would be casting the strings to the type System.Version:

PS /home/me> $s1 = '1.2.3'
PS /home/me> $s2 = '1.19.2'
PS /home/me> [Version]$v1 = $s1
PS /home/me> [Version]$v2 = $s2
PS /home/me> $v1

Major  Minor  Build  Revision
-----  -----  -----  --------
1      2      3      -1

PS /home/me> $v2

Major  Minor  Build  Revision
-----  -----  -----  --------
1      19     2      -1

PS /home/me> $v1 -gt $v2
False
PS /home/me> $v2 -gt $v1
True

I just tested this in PowerShell Core 6.1 on Linux and it worked just fine, so I'd expect it to work in PowerShell Core on Mac OS X as well.

However, you could also use the exact same approach you're using in Python: create tuples from the strings.

PS /home/me> $s1 = '1.2.3'
PS /home/me> $s2 = '1.19.2'
PS /home/me> [int[]]$v1 = $s1.Split('.')
PS /home/me> [int[]]$v2 = $s2.Split('.')
PS /home/me> $t1 = New-Object 'Tuple[int,int,int]' $a1
PS /home/me> $t2 = New-Object 'Tuple[int,int,int]' $a2
PS /home/me> $t1

Item1 Item2 Item3 Length
----- ----- ----- ------
    1     2     3      3

PS /home/me> $t2

Item1 Item2 Item3 Length
----- ----- ----- ------
    1    19     2      3

PS /home/me> $t1 -gt $t2
False
PS /home/me> $t2 -gt $t1
True

Note that splitting the string will give you a string array, so you MUST cast that to an integer array, otherwise the comparisons won't work correctly (as string comparisons rather than numeric comparisons would be used).

Note also that the number of elements in the type definition (Tuple[<type>,<type>,...]) MUST match the number of elements in the array from which the tuple is created. This answer shows a reusable function for creating tuples from arbitrary arrays:

function New-Tuple {
    Param(
        [Parameter(
            Mandatory=$true,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true
        )]
        [ValidateCount(2,20)]
        [array]$Values
    )

    Process {
        $types = ($Values | ForEach-Object { $_.GetType().Name }) -join ','
        New-Object "Tuple[$types]" $Values
    }
}

so you could change

$t1 = New-Object 'Tuple[int,int,int]' $a1
$t2 = New-Object 'Tuple[int,int,int]' $a2

to

$t1 = New-Tuple $a1
$t2 = New-Tuple $a2

Beware, though, that comparing tuples requires them to have the same number of elements, otherwise the comparison will fail:

PS /home/me> $s3 = '1.19.2.1'
PS /home/me> [int[]]$a3 = $s3.Split('.')
PS /home/me> $t3 = New-Object 'Tuple[int,int,int,int]' $a3
PS /home/me> $t3 -gt $t2
Could not compare "(1, 19, 2, 1)" to "(1, 19, 2)". Error: "Cannot convert the
"(1, 19, 2)" value of type "System.Tuple`3[[System.Int32, System.Private.CoreLib,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32,
System.Private.CoreLib, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]" to type
"System.Tuple`4[System.Int32,System.Int32,System.Int32,System.Int32]"."
At line:1 char:1
+ $t3 -gt $t2
+ ~~~~~~~~~~~
+ CategoryInfo          : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : ComparisonFailure

So you must ensure that the version tuples always have the same length, e.g. by appending 0 elements to the array and then picking the first 4 elements from the result:

[int[]]$a = ('1.2'.Split('.') + (0, 0, 0, 0))[0..3]
$t = New-Object 'Tuple[int,int,int,int]' $a

The [Version] type accelerator does not have this issue, because it creates objects of the same type, where missing numbers in a version string are automatically filled with the value -1.

PS /home/me> [Version]'1.2'

Major  Minor  Build  Revision
-----  -----  -----  --------
1      2      -1     -1

PS /home/me> [Version]'1.2.3'

Major  Minor  Build  Revision
-----  -----  -----  --------
1      2      3      -1

PS /home/me> [Version]'1.2.3.4'

Major  Minor  Build  Revision
-----  -----  -----  --------
1      2      3      4
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks! My versions will not have a consistent length ie 1.2 and future versions will be 1.2.1... I Tried [Version] again and don't know if I mistyped something but do infact see it available and work as Ansgar shows. Alas, since the versions can be different lengths I will implement PoshSemanticVersion for now.
@Donald.M As mentioned in my answer: [Version] already does handle variable-length version strings. Only if you want to use Tuple objects you need to take care of that yourself, as lined out in my answer.
1

Noticed Donald.M & Lee_Dailey have solved the solution in the comments using the PoshSematicVersion versus [version]. (which was found not to be PowerShell Core compatible)

This was the solution I worked on before a solution was posted and perhaps this may work with PowerShell Core - although unsure and untested. If you want it or not, here's my solution.

However, the str_to_tuple() function is a one liner much like the original! Technically, not a tuple being returned but a [System.Array], but perhaps a type tuple exists.

(Note: Unknown behavior with negative versioning - which even exist.)

function str_to_tuple($s) {
    return (($s.split(".") | ForEach-Object {([int]::parse($_))}))
}

# returns 1 if left ($lhs) is greater than right
# returns -1 if right ($rhs) is greater than left
# returns 0 if both right and left versions equal
function comp_vers {
    Param([Parameter(Position=0,Mandatory=$true)]$lhs, [Parameter(Position=1,Mandatory=$true)]$rhs)
    $min = [math]::Min($lhs.count,$rhs.count)

    for($i=0; $i -lt $min; $i++) {
        if ($lhs[$i] -eq $rhs[$i]) { continue }
        if ($lhs[$i] -gt $rhs[$i]) { return 1 }
        else { return -1 }
    }
    if ($lhs.count -eq $rhs.count) { return 0 }

    # Section 2 - compares version numbers further (for example, 1.1 versus 1.1.0.0 - should be equal)
    $max = [math]::Max($lhs.count,$rhs.count)
    $is_lhs_high = ($l.count -eq $max)
    for($i = $min; $i -lt $max; $i++) {
        if ($is_lhs_high) {
            if ($lhs[$i] -gt 0) { return 1 }
        } else {
            if ($rhs[$i] -gt 0) { return -1 }
        }

    }
    return 0
}

function vers_output($comp) {
    Switch($comp) {
        1 { " > " }
        0 { " = " }
        -1 { " < " }
    }
}

$tuple = str_to_tuple("1.1")
$tuple2 = str_to_tuple("1.1.0.0")
$tuple3 = str_to_tuple("3.3")
$tuple4 = str_to_tuple("2.2.2")
$tuple5 = str_to_tuple("2.2.2.0.1")

vers_output(comp_vers $tuple $tuple)    # 1.1 = 1.1
vers_output(comp_vers $tuple2 $tuple)   # 1.1.0.0 = 1.1
vers_output(comp_vers $tuple $tuple3)   # 1.1 < 3.3
vers_output(comp_vers $tuple3 $tuple4)  # 3.3 > 2.2.2
vers_output(comp_vers $tuple4 $tuple5)  # 2.2.2 < 2.2.2.0.1

1 Comment

interesting example The PoshSemanticVersion solution is working great. This method would be great if we didn't want to install the PoshSemanticVersion module.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.