Skip to content

Instantly share code, notes, and snippets.

@PanosGreg
Created February 24, 2025 13:27
Show Gist options
  • Save PanosGreg/c37429987f134f19698237ad59276243 to your computer and use it in GitHub Desktop.
Save PanosGreg/c37429987f134f19698237ad59276243 to your computer and use it in GitHub Desktop.
How to make a custom type sortable in PowerShell (with IComparer)
# How to make a custom type sortable
# There are 2 ways to make a type sortable
#
# 1) you can inherit from the IComparable interface [System.IComparable]
# where you will compare based on a predefined property of the object
# and you need to implement the CompareTo() method
#
# 2) you can create a custom Comparer type that will inherit from the IComparer interface [System.Collections.Generic.IComparer[T]]
# where you can compare based on any chosen property of the object
# and you'll need to implement the Compare() method
#
# And then you can sort the type with [Array]::Sort() (static method)
# with the .Sort() method of ArrayList or List
# or when you use a SortedSet
# create a custom type, that inherits from IComparable
class Car : System.IComparable {
# public properties
[string] $Brand # <-- car brand, ex. Mercedes, Audi
[string] $Model # <-- car model, ex. a160, a220, A4, A6
[int] $Year # <-- manufactured in that year, ex. 2019, 2020
# ctors
Car () {}
Car () {}
Car ([string]$Brand,[string]$Model,[int]$Year) {
$this.Brand = $Brand
$this.Model = $Model
$this.Year = $Year
}
# methods
[int] CompareTo ([object]$OtherCar) { # <-- we have to implement CompareTo method because of IComparable inheritance
# Note: we cannot use the [Car] type for the input parameter, since it does not exist yet in the current session
# hence why we use [object]. In C# it would be possible to do that, but not in PowerShell
# check if the input object is of the expected type
if ($OtherCar -isnot $this.GetType()) {
Write-Warning 'Cannot compare objects!'
$msg = 'Invalid type in comparison. Expected type [Car], but the type was [{0}]' -f $OtherCar.GetType().FullName
Write-Warning $msg
throw [System.ArgumentException]::new($msg)
}
# we will compare initially based on the brand name
$ValueA = $this.Brand
$ValueB = $OtherCar.Brand
# we need to output either 0, 1 or -1
# 1 = greater than
# 0 = equal
# -1 = less than
# we can do the comparison, either manually with -gt, -eq and -lt
# or we can use the existing .CompareTo() method of the [string] type
if ($ValueA -gt $ValueB) {$result = 1}
elseif ($ValueA -eq $ValueB) {$result = 0}
elseif ($ValueA -lt $ValueB) {$result = -1}
else {$result = -1} # <-- default behaviour just in case
# if we were to do it with the existing .CompareTo() method from [string], then:
# return $ValueA.CompareTo($ValueB) # <-- this returns 0, 1 or -1
# if the brands are equal, then we will compare based on the model
if ($result -eq 0) {
$ValueA = $this.Model
$ValueB = $OtherCar.Model
if ($ValueA -gt $ValueB) {$result = 1}
elseif ($ValueA -eq $ValueB) {$result = 0}
elseif ($ValueA -lt $ValueB) {$result = -1}
else {$result = -1}
}
# at this point if the models were also equal
# we could continue on and compare them based on the Year, but you get the idea
# finally return the result
return $result
}
[string] ToString() { # <-- this method is not really needed, I just override ToString for flavour
return ('{0} {1}' -f $this.Brand,$this.Model)
}
}
# create some objects
$MercModels = 'A160D','A180','E200','E250'
$BMWModels = 'X4','X6','M3','M5','i4','i5'
$AudiModels = 'A3','A5','Q5','Q7','RS','GT'
$a = [Car]::new('Mercedes',($MercModels | Get-Random),(2016..2024 | Get-Random))
$b = [Car]::new('Audi', ($AudiModels | Get-Random),(2016..2024 | Get-Random))
$c = [Car]::new('BMW', ($BMWModels | Get-Random),(2016..2024 | Get-Random))
$d = [Car]::new('Audi', ($AudiModels | Get-Random),(2016..2024 | Get-Random))
# Create a list
$list = [System.Collections.Generic.List[Car]]::new()
# add them to the list
$a, $b, $c, $d | ForEach-Object {$list.Add($_)}
# have a look at the unsorted list
$list
# finally sort them
$list.Sort() # <-- this sorts them in-place
# now show the list
$list
# another way is to use a SortedSet
$Set = [System.Collections.Generic.SortedSet[Car]]::new()
$a, $b, $c, $d | ForEach-Object {[void]$Set.Add($_)}
# Note: the .Add() method returns a boolean for success/failure, hence why I cast to [void]
# Note2: the SortedSet sorts the items when it adds them to the set, so no need for a separate (sorting) method call
######
# now here's a similar example but in C#
# this is a custom type in C# that implements IComparable
# in the following example, we opted to specify both the public properties and the private fields explicitly
# even though the private members would be added by the compiler by default (implicitly)
# Also I opted to specify the full type name, like System.IComparable, and not have a "using System;" statement at the top
$Code = @'
public class Square : System.IComparable
{
public Square( ){}
public Square(int height, int width)
{
this.height = height;
this.width = width;
}
private int height;
private int width;
public int Height
{
get{ return (height); }
set{ height = value; }
}
public int Width
{
get{ return (width); }
set{ width = value; }
}
public int CompareTo(object obj)
{
if (this.GetType( ) != obj.GetType( ))
{
throw (new System.ArgumentException(
"Both objects being compared must be of type Square."));
}
else
{
Square square2 = (Square)obj;
long area1 = this.Height * this.Width;
long area2 = square2.Height * square2.Width;
if (area1 == area2)
{
return (0);
}
else if (area1 > area2)
{
return (1);
}
else if (area1 < area2)
{
return (-1);
}
else
{
return (-1);
}
}
}
public override string ToString( )
{
return ("Height:" + height + " Width:" + width);
}
}
'@
Add-Type -TypeDefinition $Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment