Last active
March 9, 2025 13:36
-
-
Save rmbolger/37163a50e367eed677fc588864812935 to your computer and use it in GitHub Desktop.
PowerShell sorting classes IPv4 CIDR strings, FQDNs, and Email addresses
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# When working with large sets of domain names, email addresses, network CIDR ranges, | |
# and IP addresses, the default string/lexical sorting those values is not ideal. | |
# | |
# Sorting numbers as strings which what happens with IP addresses and CIDR rangers | |
# ends up putting "2" after "11" and "22" after "111" for example. | |
# | |
# Similarly with domain names and email addresses, I find that I often want all the | |
# email addresses with the same domain suffix grouped together instead of all the | |
# bob@<domain> addresses together. And I want FQDNs like <blah>.example.com to sort | |
# together instead of all the mail.<domain> values together. | |
# | |
# Here are some PowerShell classes you can add to your profile that will make it easier | |
# to properly sort these string types. | |
# For standalone IPv4 addresses, you don't actually need a custom class. You can abuse | |
# the built-in [version] class to provide the necessary sorting. | |
# | |
# '1.1.1.1','10.10.10.10','2.2.2.2' | Sort-Object {[version]$_} | |
# For IPv4 CIDR ranges such as 192.168.0.0/24, the CIDR sorter will provide a similar | |
# octet sensitive sort for the IP portion. For ranges that have matching IP portions, | |
# it will sort larger capacity networks (smaller numbers) before smaller ones. So | |
# 10.0.0.0/8 comes before 10.0.0.0/24. | |
# | |
# '10.0.0.0/8','10.0.0.0/24','2.3.4.0/16' | Sort-Object {[Cidr]$_} | |
class Cidr : IComparable | |
{ | |
[string]$Cidr | |
hidden [version] $_ip | |
hidden [int] $_mask | |
Cidr([string]$_Cidr) { | |
$this.Cidr = $_Cidr | |
} | |
[int] CompareTo([object]$other) { | |
if ($other -isnot [Cidr]) { | |
throw [ArgumentException]::new('other') | |
} | |
$ipCompare = $this.GetIP().CompareTo($other.GetIP()) | |
if (0 -ne $ipCompare) { | |
return $ipCompare | |
} else { | |
return ($this.GetMask().CompareTo($other.GetMask())) | |
} | |
} | |
hidden [version] GetIP() { | |
if (-not $this._ip) { | |
$this._ip = [version]$this.Cidr.Substring(0, $this.Cidr.IndexOf('/')) | |
} | |
return $this._ip | |
} | |
hidden [int] GetMask() { | |
if (-not $this._mask) { | |
$this._mask = [int]$this.Cidr.Substring($this.Cidr.IndexOf('/')+1) | |
} | |
return $this._mask | |
} | |
[string] ToString() { | |
return $this.Cidr.ToString() | |
} | |
} | |
# The FQDN sorter will sort based on the labels in reverse order. So .com's will | |
# sort near each other, everything within the same domain will sort near each other, | |
# etc. | |
# | |
# 'a.example.org','a.example.com','example.org','example.com' | Sort-Object {[Fqdn]$_} | |
class Fqdn : IComparable | |
{ | |
[string]$Fqdn | |
hidden [string] $_namespaceOrder = $null | |
Fqdn([string]$_fqdn) { | |
$this.Fqdn = $_fqdn | |
} | |
[int] CompareTo([object]$other) { | |
if($other -isnot [Fqdn]) { | |
throw [ArgumentException]::new('other') | |
} | |
return [string]::Compare( | |
$this.GetNamespaceOrder(), | |
$other.GetNamespaceOrder(), | |
[StringComparison]::InvariantCultureIgnoreCase | |
) | |
} | |
hidden [string] GetNamespaceOrder() { | |
if (-not $this._namespaceOrder) { | |
$labels = $this.Fqdn.Split('.') | |
[array]::Reverse($labels) | |
$this._namespaceOrder = $labels -join '.' | |
} | |
return $this._namespaceOrder | |
} | |
[string] ToString() { | |
return $this.Fqdn.ToString() | |
} | |
} | |
# The Email sorter will sort based on the labels in reverse order. So [email protected] | |
# will effectively be sorted as com.example@me which keeps addresses in the same | |
# domain near each other. | |
# | |
# '[email protected]','[email protected]','[email protected]','[email protected]' | Sort-Object {[Email]$_} | |
class Email : IComparable | |
{ | |
[string]$Address | |
hidden [string] $_namespaceOrder = $null | |
Email([string]$_address) { | |
$this.Address = $_address | |
} | |
[int] CompareTo([object]$other) { | |
if($other -isnot [Email]) { | |
throw [ArgumentException]::new('other') | |
} | |
return [string]::Compare( | |
$this.GetNamespaceOrder(), | |
$other.GetNamespaceOrder(), | |
[StringComparison]::InvariantCultureIgnoreCase | |
) | |
} | |
hidden [string] GetNamespaceOrder() { | |
if (-not $this._namespaceOrder) { | |
$user,$domain = $this.Address.Split('@') | |
$dParts = $domain.Split('.') | |
[array]::Reverse($dParts) | |
$this._namespaceOrder = '{0}@{1}' -f ($dParts -join '.'),$user | |
} | |
return $this._namespaceOrder | |
} | |
[string] ToString() { | |
return $this.Address.ToString() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment