Skip to content

Instantly share code, notes, and snippets.

@johlju
Last active January 31, 2024 09:44
Show Gist options
  • Save johlju/5f1ed2fe2f510c9e4272dd942a389723 to your computer and use it in GitHub Desktop.
Save johlju/5f1ed2fe2f510c9e4272dd942a389723 to your computer and use it in GitHub Desktop.
PowerShell class example that implements `IComparable` and `System.IEquatable`

A class that implements IComparable and System.IEquatable. Thanks to Chris Dent for the inspiration (DnsRecordType.ps1) and the StackOverflow community question How to compare multiple object values against each other and using mathematical operators in Powershell Overriding Assignment and Comparison operators (from JFFail gist. Mathematical operators are not included in the example below, but linked here for future reference.

Assuming the current state contain version 1.0.0 and the minimum required version is 1.0.1-preview1.

$currentStateResource =  [PSResourceObject] @{
    Name = 'MyModule'
    Version = '1.0.0'
}

$minimumRequiredResource =  [PSResourceObject] @{
    Name = 'MyModule'
    Version = '1.0.1'
    PreRelease = 'preview1'
}

$minimumRequiredResource.CompareTo($currentStateResource)

That would result in 1 meaning the minumim version required is not installed.

  • 1 = minimum version need to be installed
  • 0 = minimum version is compliant, the exact minimum version is installed
  • -1 = minimum version is compliant, there is already a higher version installed than the minimum version

Mermaid class diagram (syntax help in the docs):

classDiagram

class PSResourceObject {
    +Name: System.String
    +Version: System.Version
    +PreRelease: System.String

    +PSResourceObject()
    +PSResourceObject(System.String Name)
    +PSResourceObject(System.String Name, System.Version Version)
    +PSResourceObject(System.String Name, System.Version Version, System.String PreRelease)
    
    +Equals(System.Object): System.Boolean
    +CompareTo(System.Object): System.Int32
    +GetHashCode(): System.Int32
    +ToString(): System.String

    +static GetInstalledResource(System.String)$ PSResourceObject[]
    +static GetMinimumInstalledVersion(System.String)$ PSResourceObject
    +static GetMinimumInstalledVersion(PSResourceObject[])$ PSResourceObject
    +static GetMaximumInstalledVersion(System.String)$ PSResourceObject
    +static GetMaximumInstalledVersion(PSResourceObject[])$ PSResourceObject
    +static Install(System.Collections.Hashtable)$ void
    +IsPreRelease()$ System.Boolean

    -static op_Implicit(System.Management.Automation.PSModuleInfo): PSResourceObject

    -ToUniqueString(): System.String
}

class IComparable {
  <<Interface>>
  CompareTo(System.Object) System.Int32
}

class IEquatable {
  <<Interface>>
  Equals(System.Object) System.Boolean
}

IComparable <|-- PSResourceObject : implements
IEquatable <|-- PSResourceObject : implements
Loading

Example output:

using module SqlServerDsc # Just took an existing module that was already using classes to run the code in.

$a = [PSResourceObject] @{ Name = 'MyModule'; Version = '1.0.0' }
$b = [PSResourceObject] @{ Name = 'MyModule'; Version = '1.0.0' }
$c = [PSResourceObject] @{ Name = 'MyModule2'; Version = '1.0.0' }
$d = [PSResourceObject] @{ Name = 'MyModule'; Version = '1.0.1' }
$e = [PSResourceObject] @{ Name = 'MyModule2'; Version = '0.9.0' }
$f = [PSResourceObject] @{ Name = 'MyModule'; Version = '1.0.0'; PreRelease = 'preview3' }
$g = [PSResourceObject] @{ Name = 'MyModule'; Version = '1.0.0'; PreRelease = 'preview2' }
$h = [PSResourceObject] @{ Name = 'MyModule2'; Version = '1.0.0'; PreRelease = 'preview'  }
$all = @($a,$b,$c,$d,$e,$f,$g,$h)
$all | Sort-Object

Name      Version PreRelease
----      ------- ----------
MyModule  1.0.0   preview2
MyModule  1.0.0   preview3
MyModule  1.0.0
MyModule  1.0.0
MyModule  1.0.1
MyModule2 0.9.0
MyModule2 1.0.0   preview
MyModule2 1.0.0

PS > $all | Sort-Object -Descending

Name      Version PreRelease
----      ------- ----------
MyModule2 1.0.0
MyModule2 1.0.0   preview
MyModule2 0.9.0
MyModule  1.0.1
MyModule  1.0.0
MyModule  1.0.0
MyModule  1.0.0   preview3
MyModule  1.0.0   preview2

PS > $all | Sort-Object -Top 1

Name     Version PreRelease
----     ------- ----------
MyModule 1.0.0   preview2

PS > $all | Sort-Object -Unique

Name      Version PreRelease
----      ------- ----------
MyModule  1.0.0   preview2
MyModule  1.0.0   preview3
MyModule  1.0.0
MyModule  1.0.1
MyModule2 0.9.0
MyModule2 1.0.0   preview
MyModule2 1.0.0

PS > $all | Sort-Object -Property Version -Unique

Name      Version PreRelease
----      ------- ----------
MyModule2 0.9.0
MyModule  1.0.0
MyModule  1.0.1

PS > $a -eq $d
False
PS > $a -eq $b
True
PS > [PSResourceObject]::GetInstalledResource('Pester') | Sort-Object

Name   Version PreRelease
----   ------- ----------
Pester 3.4.0
Pester 5.4.0   rc1

PS > [PSResourceObject]::GetInstalledResource('Pester') | Sort-Object -Descending

Name   Version PreRelease
----   ------- ----------
Pester 5.4.0   rc1
Pester 3.4.0

PS > [PSResourceObject]::GetMinimumInstalledVersion('Pester')

Name   Version PreRelease
----   ------- ----------
Pester 3.4.0

PS > [PSResourceObject]::GetMinimumInstalledVersion(([PSResourceObject[]] @(@{Name = 'MyModule'; Version = '1.0.0'},@{Name = 'MyModule'; Version = '1.0.0'; PreRelease = 'preview'})))

Name     Version PreRelease
----     ------- ----------
MyModule 1.0.0   preview

PS > [PSResourceObject]::GetMaximumInstalledVersion('Pester')

Name   Version PreRelease
----   ------- ----------
Pester 5.4.0   rc1

PS > [PSResourceObject]::GetMaximumInstalledVersion(([PSResourceObject[]] @(@{Name = 'MyModule'; Version = '1.0.0'},@{Name = 'MyModule'; Version = '1.0.0'; PreRelease = 'preview'})))

Name     Version PreRelease
----     ------- ----------
MyModule 1.0.0

PS >
PS > $currentStateResource =  [PSResourceObject] @{Name = 'MyModule'; Version = '1.0.0'}
PS > $minimumRequiredResource =  [PSResourceObject] @{Name = 'MyModule'; Version = '1.0.1'; PreRelease = 'preview1'}
PS > $minimumRequiredResource.CompareTo($currentStateResource)
1
PS >
PS > $currentStateResource =  [PSResourceObject] @{Name = 'MyModule'; Version = '1.0.0'}
PS > $minimumRequiredResource =  [PSResourceObject] @{Name = 'MyModule'; Version = '1.0.0'; PreRelease = 'preview1'}
PS > $minimumRequiredResource.CompareTo($currentStateResource)
-1
PS >
PS > $currentStateResource =  [PSResourceObject] @{Name = 'MyModule'; Version = '1.0.0'}
PS > $minimumRequiredResource =  [PSResourceObject] @{Name = 'MyModule'; Version = '1.0.0'}
PS > $minimumRequiredResource.CompareTo($currentStateResource)
0
PS >
PS > $a.IsPrerelease()
False
PS > $f.IsPrerelease()
True

Implicit conversion from PSModuleInfo to PSResourceObject:

PS > $moduleInfo = get-module DscResource.DocGenerator
PS > $moduleInfo

ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Script     0.12.0     preview00… DscResource.DocGenerator            {Add-NewLine, Edit-CommandDocumentation, Invoke-Git, New-Ds…

PS> [PSResourceObject] $moduleInfo                   

Name                     Version PreRelease
----                     ------- ----------
DscResource.DocGenerator 0.12.0  preview0004
class PSResourceObject : IComparable, System.IEquatable[Object]
{
[System.String]
$Name
[System.Version]
$Version
[System.String]
$PreRelease
PSResource ()
{
}
PSResource ([System.String] $Name)
{
$this.Name = $Name
}
PSResource ([System.String] $Name, [System.Version] $Version)
{
$this.Name = $Name
$this.Version = $Version
}
PSResource ([System.String] $Name, [System.Version] $Version, [System.String] $PreRelease)
{
$this.Name = $Name
$this.Version = $Version
$this.PreRelease = $PreRelease
}
[System.Boolean] Equals([System.Object] $object)
{
$isEqual = $false
if ($object -is $this.GetType())
{
if ($this.CompareTo($object) -eq 0)
{
$isEqual = $true
}
}
return $isEqual
}
<#
Important if this class will be used as a key in a dictionary, HashSet<T>, or similar.
See more: https://stackoverflow.com/questions/371328/why-is-it-important-to-override-gethashcode-when-equals-method-is-overridden
#>
[System.Int32] GetHashCode()
{
# Tried but it did not work because Combine() fails on $null values.
#[System.HashCode]::Combine($this.Name, $this.Version, $this.PreRelease)
### -------
# Tried but did not work because the calculated hash was to great or low for Int32
# # Initialize a variable $hash with a prime number (13 in this case).
# [System.Int32] $hash = 13
# <#
# For each property that should be included in the hash code (Name, Version, PreRelease),
# multiply the current hash by another prime number (7 in this case) and add the hash
# code of the property. The GetHashCode method is called on each property to get its
# hash code. If the value is $null, the hash code is 0.
# #>
# $hash = ($hash * 7) + $this.Name.GetHashCode()
# $hash = ($hash * 7) + $this.Version.GetHashCode()
# $hash = ($hash * 7) + (if ($null -eq $this.PreRelease) { $this.PreRelease.GetHashCode() } else { 0 })
# # Return the final calculated hash.
# return $hash
### -------
# Works, but... Does ot handle $null values. Not available on .NET Framework before .NET 4.6.1.
# $hashCode = [System.HashCode]::new()
# $hashCode.Add($this.Name)
# $hashCode.Add($this.Version)
# if ($this.PreRelease) { $hashCode.Add($this.PreRelease) }
# return $hashCode.ToHashCode()
### -------
# Works (as far as I can tell).
return $this.ToUniqueString().GetHashCode()
}
# Helps to calculate the hash code.
hidden [System.String] ToUniqueString()
{
return "$($this.Name)-$($this.Version)-$($this.PreRelease)"
}
[System.String] ToString()
{
$output = $null
if ($this.Name)
{
$output = $this.Name
}
if ($this.Name)
{
if ($output)
{
$output += ' '
}
$output += 'v{0}' -f $this.Version
}
if ($this.IsPreRelease())
{
if ($output)
{
$output += '-'
}
$output += $this.PreRelease
}
return $output
}
[System.Int32] CompareTo([Object] $object)
{
[System.Int32] $returnValue = 0
# If $null object is passed in, put it at the end of the ascending sort order.
if ($null -eq $object)
{
return 1
}
if ($object -is $this.GetType())
{
<#
Order objects in the order of Name then Version.
Less than zero - The current instance precedes the object specified by the CompareTo
method in the sort order.
Zero - This current instance occurs in the same position in the sort order
as the object specified by the CompareTo method.
Greater than zero - This current instance follows the object specified by the CompareTo
method in the sort order.
#>
# Compare Name.
$returnValue = $this.Name.CompareTo($object.Name)
# If the name is equal, compare version.
if ($returnValue -eq 0)
{
$returnValue = $this.Version.CompareTo($object.Version)
# If the version is equal, compare pre-release if there are any.
if ($returnValue -eq 0 -and ($null -ne $this.PreRelease -or $null -ne $object.PreRelease))
{
if ($null -ne $this.PreRelease -and $null -ne $object.PreRelease)
{
# Both objects are a pre-release, compare pre-release string.
$returnValue = $this.PreRelease.CompareTo($object.PreRelease)
}
elseif ($null -eq $this.PreRelease)
{
<#
Current instance is not pre-release, current instance should
be sorted higher than a pre-release.
#>
$returnValue = 1
}
elseif ($null -eq $object.PreRelease)
{
<#
Current instance is a pre-release, current instance should
be sorted lower than a full-release.
#>
$returnValue = -1
}
}
}
}
else
{
#InvalidTypeForCompare = Invalid type in comparison. Expected type [{0}], but the type was [{1}]. (DP0001)
$errorMessage = $script:localizedData.InvalidTypeForCompare -f @(
$this.GetType().FullName,
$object.GetType().FullName
)
New-InvalidArgumentException -ArgumentName 'Object' -Message $errorMessage
}
return $returnValue
}
<#
Implicit conversion from PSModuleInfo to PSResourceObject.
Implicit operators are used when the conversion is guaranteed to succeed without data loss.
Explicit operators are used when the conversion might result in data loss or when the
conversion is not guaranteed to succeed.
https://medium.com/@gustavorestani/c-implicit-and-explicit-operators-a-comprehensive-guide-5e6972cc8671
#>
static [PSResourceObject] op_Implicit([System.Management.Automation.PSModuleInfo] $moduleInfo)
{
$moduleProperties = @{
Name = $moduleInfo.Name
Version = $moduleInfo.Version
}
if ($moduleInfo.PrivateData.PSData.ContainsKey('PreRelease'))
{
if (-not [System.String]::IsNullOrEmpty($moduleInfo.PrivateData.PSData.Prerelease))
{
$moduleProperties.PreRelease = $moduleInfo.PrivateData.PSData.Prerelease
}
}
return [PSResourceObject] $moduleProperties
}
static [PSResourceObject[]] GetInstalledResource([System.String] $Name)
{
$installedResources = [PSResourceObject[]] @()
$availableModules = Get-Module -Name $Name -ListAvailable
foreach ($module in $availableModules)
{
$availableResource = [PSResourceObject] $module
$installedResources += $availableResource
}
return $installedResources
}
static [PSResourceObject] GetMinimumInstalledVersion([System.String] $Name)
{
$installedResources = [PSResourceObject]::GetInstalledResource($Name)
return ($installedResources | Sort-Object -Top 1)
}
static [PSResourceObject] GetMinimumInstalledVersion([PSResourceObject[]] $resource)
{
return ($resource | Sort-Object -Top 1)
}
static [PSResourceObject] GetMaximumInstalledVersion([System.String] $Name)
{
$installedResources = [PSResourceObject]::GetInstalledResource($Name)
return ($installedResources | Sort-Object -Descending -Top 1)
}
static [PSResourceObject] GetMaximumInstalledVersion([PSResourceObject[]] $resource)
{
return ($resource | Sort-Object -Descending -Top 1)
}
static [void] Install([System.Collections.Hashtable] $parameters)
{
Install-Module @parameters
}
[System.Boolean] IsPreRelease()
{
return ($null -ne $this.PreRelease)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment