Skip to content

Instantly share code, notes, and snippets.

@rmbolger
Last active March 9, 2025 13:36
Show Gist options
  • Save rmbolger/37163a50e367eed677fc588864812935 to your computer and use it in GitHub Desktop.
Save rmbolger/37163a50e367eed677fc588864812935 to your computer and use it in GitHub Desktop.
PowerShell sorting classes IPv4 CIDR strings, FQDNs, and Email addresses
# 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