Source/Public/Sort-ObjectGraph.ps1

<#
.SYNOPSIS
    Sort object graph
 
.DESCRIPTION
    Recursively sorts a object graph.
 
.PARAMETER InputObject
    The input object that will be recursively sorted.
 
    > [!NOTE]
    > Multiple input object might be provided via the pipeline.
    > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline.
    > To avoid a list of (root) objects to unroll, use the **comma operator**:
 
        ,$InputObject | Sort-Object.
 
.PARAMETER PrimaryKey
    Any primary key defined by the [-PrimaryKey] parameter will be put on top of [-InputObject]
    independent of the (descending) sort order.
 
    It is allowed to supply multiple primary keys.
 
.PARAMETER MatchCase
    Indicates that the sort is case-sensitive. By default, sorts aren't case-sensitive.
 
.PARAMETER Descending
    Indicates that Sort-Object sorts the objects in descending order. The default is ascending order.
 
    > [!NOTE]
    > Primary keys (see: [-PrimaryKey]) will always put on top.
 
.PARAMETER MaxDepth
    The maximal depth to recursively compare each embedded property (default: 10).
#>


function ConvertTo-SortedObjectGraph {
    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseApprovedVerbs', '')]
    [CmdletBinding()][OutputType([Object[]])] param(

        [Parameter(Mandatory = $true, ValueFromPipeLine = $True)]
        $InputObject,

        [Alias('By')][String[]]$PrimaryKey,

        [Switch]$MatchCase,

        [Switch]$Descending,

        [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth
    )
    begin {
        $Primary = @{}
        if ($PSBoundParameters.ContainsKey('PrimaryKey')) {
            for($i = 0; $i -lt $PrimaryKey.Count; $i++) {
                if ($Descending) {
                    $Primary[$PrimaryKey[$i]] = [Char]254 + '#' * ($PrimaryKey.Count - $i)
                }
                else {
                    $Primary[$PrimaryKey[$i]] = ' ' + '#' * $i
                }
            }
        }

        function SortObject([PSNode]$Node, [String[]]$PrimaryKey, [Switch]$MatchCase, [Switch]$Descending, [Switch]$SortIndex) {
            if ($Node -is [PSLeafNode]) {
                $SortKey = if ($Null -eq $($Node.Value)) { [Char]27 + '$Null' } elseif ($MatchCase) { "$($Node.Value)".ToUpper() } else { "$($Node.Value)" }
                $Output = @{ $SortKey = $($Node.Value) }
            }
            elseif ($Node -is [PSListNode]) {                                   # This will convert the list to an (fixed) array
                $Items = $Node.ChildNodes.foreach{ SortObject $_ -SortIndex -PrimaryKey $PrimaryKey -MatchCase:$MatchCase -Descending:$Descending }
                $Items = $Items | Sort-Object -CaseSensitive:$MatchCase -Descending:$Descending { $_.Keys[0] }
                $String = [Collections.Generic.List[String]]::new()
                $List   = [Collections.Generic.List[Object]]::new()
                foreach ($Item in $Items) {
                    $SortKey = $Item.GetEnumerator().Name
                    $String.Add($SortKey)
                    $List.Add($Item[$SortKey])
                }
                $Name = $String -Join [Char]255
                $Output = @{ $Name = @($List) }
            }
            elseif ($Node -is [PSMapNode]) {                                    # This will convert a dictionary to a PSCustomObject
                $HashTable = [HashTable]::New(0, [StringComparer]::Ordinal)
                $Node.ChildNodes.foreach{
                    $SortObject = SortObject $_ -PrimaryKey $PrimaryKey -MatchCase:$MatchCase -Descending:$Descending -SortIndex
                    $SortKey = $SortObject.GetEnumerator().Name
                    if ($Primary.Contains($_.Name)) { $Key = $Primary[$_.Name] } else { $Key = $_.Name}
                    $HashTable["$Key$([Char]255)$SortKey"] = @{ $_.Name = $SortObject[$SortKey] }
                }
                $SortedKeys = $HashTable.get_Keys() | Sort-Object -CaseSensitive:$MatchCase -Descending:$Descending
                $Properties = [System.Collections.Specialized.OrderedDictionary]::new([StringComparer]::Ordinal)
                @($SortedKeys).foreach{
                    $Item = $HashTable[$_]
                    $Name = $Item.GetEnumerator().Name
                    $Properties[[Object]$Name] = $Item[$Name]                   # https://github.com/PowerShell/PowerShell/issues/14791
                }
                $Name = $SortedKeys -Join [Char]255
                $Output = @{ $Name = [PSCustomObject]$Properties }              # https://github.com/PowerShell/PowerShell/issues/20753
            }
            else { Write-Error 'Should not happen' }
            if ($SortIndex) { $Output } else { $Output.get_Values() }
        }
    }

    process {
        $Node = [PSNode]::ParseInput($InputObject, $MaxDepth)
        SortObject $Node -PrimaryKey $PrimaryKey -MatchCase:$MatchCase -Descending:$Descending
    }
}

Set-Alias -Name 'Sort-ObjectGraph' -Value 'ConvertTo-SortedObjectGraph' -Scope Global