39

Is there a way to keep the order of keys in a hashtable as they were added? Like a push/pop mechanism.

Example:

$hashtable = @{}

$hashtable.Add("Switzerland", "Bern")
$hashtable.Add("Spain", "Madrid")
$hashtable.Add("Italy", "Rome")
$hashtable.Add("Germany", "Berlin")
$hashtable

I want to retain the order in which I've added the elements to the hashtable.

double-beep
  • 5,031
  • 17
  • 33
  • 41
Philipp
  • 1,001
  • 3
  • 10
  • 10

7 Answers7

66

There is no built-in solution in PowerShell V1 / V2. You will want to use the .NET System.Collections.Specialized.OrderedDictionary:

$order = New-Object System.Collections.Specialized.OrderedDictionary
$order.Add("Switzerland", "Bern")
$order.Add("Spain", "Madrid")
$order.Add("Italy", "Rome")
$order.Add("Germany", "Berlin")


PS> $order

Name                           Value
----                           -----
Switzerland                    Bern
Spain                          Madrid
Italy                          Rome
Germany                        Berlin

In PowerShell V3 you can cast to [ordered]:

PS> [ordered]@{"Switzerland"="Bern"; "Spain"="Madrid"; "Italy"="Rome"; "Germany"="Berlin"}

Name                           Value
----                           -----
Switzerland                    Bern
Spain                          Madrid
Italy                          Rome
Germany                        Berlin
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Loïc MICHEL
  • 24,935
  • 9
  • 74
  • 103
  • Thanks for the answers! I knew the PS v3 [ordered] but I must use PS 2.0 – Philipp Feb 15 '13 at 09:06
  • Thanks, what to say: strange implementation, I see no reason for that implementation as being the default. You need it unodered for raw speed? Do a custom class for that and let the base be, as most would expcet, ordered with the insert order. – user1708042 Dec 02 '20 at 10:36
  • 2
    Note that you also have to use this type (`System.Collections.Specialized.OrderedDictionary`) for the parameter if you want to [pass your ordered hash to a function](https://stackoverflow.com/q/42568949/1028230). You cannot use a flavor of the `[ordered][hash]` shortcut as a param typing, and must use the full type. You can pass as a `[hash]` parameter, but you lose the ordering (since it's passed as the less specific extended `hash` type; more at [that link](https://stackoverflow.com/q/42568949/1028230)), which is likely undesirable. – ruffin Dec 20 '21 at 22:49
  • Thanks for the tip re: casting to `[ordered]`. I was able to do this when declaring a global variable as follows: `Set-Variable -Name MyOrderedHashtable -Scope global -Option AllScope -Value ([ordered]@{})` – MikeOnline Jun 15 '23 at 16:50
10

You can use an ordered dictionary instead:

Like this:

$list = New-Object System.Collections.Specialized.OrderedDictionary
$list.Add("Switzerland", "Bern")
$list.Add("Spain", "Madrid")
$list.Add("Italy", "Rome")
$list.Add("Germany", "Berlin")
$list
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Frode F.
  • 52,376
  • 9
  • 98
  • 114
7

You can give one sequential key as you add elements:

$hashtable = @{}
$hashtable[$hashtable.count] = @("Switzerland", "Bern")
$hashtable[$hashtable.count] = @("Spain", "Madrid")
$hashtable[$hashtable.count] = @("Italy", "Rome")
$hashtable[$hashtable.count] = @("Germany", "Berlin")
$hashtable

Then, you can get elements sorted by the key:

echo "`nHashtable keeping the order as they were added"
foreach($item in $hashtable.getEnumerator() | Sort Key)
{
    $item
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Juan Antonio Tubío
  • 1,172
  • 14
  • 28
1

The PowerShell 1 way is to add a hashtable member to retain the add order. There is no need to use System.Collections.Specialized.OrderedDictionary:

$Hash = New-Object PSObject                                       
$Hash | Add-Member -MemberType NoteProperty -Name key1 -Value val1
$Hash | Add-Member -MemberType NoteProperty -Name key2 -Value val2
$Hash | Add-Member -MemberType NoteProperty -Name key3 -Value val3
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 5
    But that's not a hashtable, it's a PSCustomObject. Not the same thing, even if you name the variable "$Hash". ;) An OrderedDictionary functions just like a hashtable for all practical purposes that I've tried. – Adi Inbar Sep 06 '13 at 19:26
1

For compatibility with older PowerShell versions you might consider this cmdlet:

Function Order-Keys {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)][HashTable]$HashTable,
        [Parameter(Mandatory = $false, Position = 1)][ScriptBlock]$Function,
        [Switch]$Descending
    )
    $Keys = $HashTable.Keys | ForEach {$_} # Copy HashTable + KeyCollection
    For ($i = 0; $i -lt $Keys.Count - 1; $i++) {
        For ($j = $i + 1; $j -lt $Keys.Count; $j++) {
            $a = $Keys[$i]
            $b = $Keys[$j]
            If ($Function -is "ScriptBlock") {
                $a = $HashTable[$a] | ForEach $Function
                $b = $HashTable[$b] | ForEach $Function
            }
            If ($Descending) {
                $Swap = $a -lt $b
            }
            Else
            {
                $Swap = $a -gt $b
            }
            If ($Swap) {
                $Keys[$i], $Keys[$j] = $Keys[$j], $Keys[$i]
            }
        }
    }
    Return $Keys
}

This cmdlet returns a list of keys ordered by the function definition:

Sort by name:

$HashTable | Order-Keys | ForEach {Write-Host $_ $HashTable[$_]}
Germany Berlin
Italy Rome
Spain Madrid
Switzerland Bern

Sort by value:

$HashTable | Order-Keys {$_} | ForEach {Write-Host $_ $HashTable[$_]}
Germany Berlin
Switzerland Bern
Spain Madrid
Italy Rome

You might also consider to nest hash tables:

$HashTable = @{
    Switzerland = @{Order = 1; Capital = "Berne"}
    Germany     = @{Order = 2; Capital = "Berlin"}
    Spain       = @{Order = 3; Capital = "Madrid"}
    Italy       = @{Order = 4; Capital = "Rome"}
}

E.g. sort by (hashed) order property and return the key (country):

$HashTable | Order-Keys {$_.Order} | ForEach {$_}

Or sort (descending) by the predefined capital:

$HashTable | Order-Keys {$_.Capital} -Descending | ForEach {$_}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
iRon
  • 20,463
  • 10
  • 53
  • 79
1

Here is a simple routine that works for me.

function sortedKeys([hashtable]$ht) {
  $out = @()
  foreach($k in $ht.keys) {
    $out += $k
  }
  [Array]::sort($out)
  return ,$out
}

and the call to use it

forEach($k in (& sortedKeys $ht)) {
  ...
}
Steve Pritchard
  • 207
  • 3
  • 5
  • 1
    But the question was about keeping the order in which the hash was added to, not sorting by keys. How does this answer the question? – Peter Mortensen Jan 20 '19 at 10:25
  • Peter, you are correct. I was confused and thought Ordered dictionary was like a Java TreeMap not a LinkedHashMap. Thanks for the clarification. – Steve Pritchard Mar 01 '19 at 17:28
-1
function global:sortDictionaryByKey([hashtable]$dictionary)
{
    return $dictionary.GetEnumerator() | sort -Property name;
}
  • 5
    Welcome to Stack Overflow. While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value.[How to Answer](https://stackoverflow.com/help/how-to-answer). Thanks! – Elletlar Aug 30 '18 at 22:59