Gordon
Gordon

Reputation: 6863

Powershell sorting hash table

I am seeing some seemingly very weird behavior with a hash table I am sorting and then trying to review the results. I build the hash table, then I need to sort that table based on values, and I see two bits of weirdness.

This works fine outside of a class

$hash = [hashtable]::New()
$type = 'conformset'
$hash.Add($type, 1)
$type = 'applyset'
$hash.Add($type , 1)
$type = 'conformset'
$hash.$type ++
$hash.$type ++
$hash
Write-Host
$hash = $hash.GetEnumerator() | Sort-Object -property:Value
$hash

I see the contents of the hash twice, unsorted and then sorted. However, when using a class it does nothing.

class Test {
    # Constructor (abstract class)
    Test () {
        $hash = [hashtable]::New()
        $type = 'conformset'
        $hash.Add($type, 1)
        $type = 'applyset'
        $hash.Add($type , 1)
        $type = 'conformset'
        $hash.$type ++
        $hash.$type ++
        $hash
        Write-Host
        $hash = $hash.GetEnumerator() | Sort-Object -property:Value
        $hash
    }
}

[Test]::New()

This just echos Test to the console, with nothing related to the hash table. My assumption here is that it relates somehow to how the pipeline is interrupted, which lets be honest, is a great reason to move to classes, given how common polluted pipeline errors are. So, moving to a loop based approach, this fails to show the second, sorted, sorted hash table in a class or not.

$hash = [hashtable]::New()
$type = 'conformset'
$hash.Add($type, 1)
$type = 'applyset'
$hash.Add($type , 1)
$type = 'conformset'
$hash.$type ++
$hash.$type ++

foreach ($key in $hash.Keys) {
    Write-Host "$key $($hash.$key)!"
}
Write-Host
$hash = ($hash.GetEnumerator() | Sort-Object -property:Value)
foreach ($key in $hash.Keys) {
    Write-Host "$key $($hash.$key)!!"
}

But, very weirdly, this shows only the first loop based output, but BOTH of the direct dumps.

$hash = [hashtable]::New()
$type = 'conformset'
$hash.Add($type, 1)
$type = 'applyset'
$hash.Add($type , 1)
$type = 'conformset'
$hash.$type ++
$hash.$type ++

foreach ($key in $hash.Keys) {
    Write-Host "$key $($hash.$key)!"
}
$hash
Write-Host
$hash = ($hash.GetEnumerator() | Sort-Object -property:Value)
foreach ($key in $hash.Keys) {
    Write-Host "$key $($hash.$key)!!"
}
$hash

The output now is

conformset 3!
applyset 1!

Name                           Value                                                                                                                                                                          
----                           -----                                                                                                                                                                          
conformset                     3                                                                                                                                                                              
applyset                       1                                                                                                                                                                              

applyset                       1                                                                                                                                                                              
conformset                     3  

So obviously $hash is being sorted. But the loop won't show it? Huh? Is this buggy behavior, or intended behavior I just don't understand the reason for, and therefor the way around?

Upvotes: 6

Views: 20751

Answers (2)

mklement0
mklement0

Reputation: 438113

  • Vasil Svilenov Nikolov's helpful answer explains the fundamental problem with your approach:

    • You fundamentally cannot sort a hash table ([hashtable] instance) by its keys: the ordering of keys in a hash table is not guaranteed and cannot be changed.

    • What $hash = $hash.GetEnumerator() | Sort-Object -property:Value does is to instead create an array of [System.Collections.DictionaryEntry] instances; the resulting array has no .Keys property, so your second foreach ($key in $hash.Keys) loop is never entered.

  • An unrelated problem is that you generally cannot implicitly write to the output stream from PowerShell classes:

    • Writing to the output stream from a class method requires explicit use of return; similarly, errors must be reported via Throw statements.
    • In your case, the code is in a constructor for class Test, and constructors implicitly return the newly constructed instance - you're not allowed to return anything from them.

To solve your problem, you need a specialized data type that combines the features of a hash table with maintaining the entry keys in sort order.[1]

.NET type System.Collections.SortedList provides this functionality (there's also a generic version, as Lee Dailey notes):

You can use that type to begin with:

# Create a SortedList instance, which will maintain
# the keys in sorted order, as entries are being added.
$sortedHash = [System.Collections.SortedList]::new()

$type = 'conformset'
$sortedHash.Add($type, 1) # Or: $sortedHash[$type] = 1 or: $sortedHash.$type = 1
$type = 'applyset'
$sortedHash.Add($type , 1)
$type = 'conformset'
$sortedHash.$type++
$sortedHash.$type++

Or even convert from (and to) an existing hash table:

# Construct the hash table as before...
$hash = [hashtable]::new() # Or: $hash = @{}, for a case-INsensitive hashtable
$type = 'conformset'
$hash.Add($type, 1)
$type = 'applyset'
$hash.Add($type , 1)
$type = 'conformset'
$hash.$type++
$hash.$type++

# ... and then convert it to a SortedList instance with sorted keys.
$hash = [System.Collections.SortedList] $hash

[1] Note that this is different from an ordered dictionary, which PowerShell offers with literal syntax [ordered] @{ ... }: an ordered dictionary maintains the keys in the order in which they are inserted, not based on sorting. Ordered dictionaries are of type System.Collections.Specialized.OrderedDictionary

Upvotes: 13

Vasil Nikolov
Vasil Nikolov

Reputation: 757

When you do $hash = $hash.GetEnumerator() | Sort-Object -property:Value , you are re-assigning the hashtable in to an array, try $hash.GetType() , and that will of course behave differently than a hashtable, you can check out the methods etc. Get-Member -InputObject $hash

I dont think you can sort a hashtable, and you do not need to. You might instead try Ordered Dictionary $hash = [Ordered]@{}

Ordered dictionaries differ from hash tables in that the keys always appear in the order in which you list them. The order of keys in a hash table is not determined.

One of the best use of a hashtable that I like is the speed of search. For example, you can instantly get the value of a name in the hashtable the following way $hash['applyset']

If you want to know more about how hashtable work, and how/when to use it, I think this article is a good start : https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_hash_tables?view=powershell-6

Upvotes: 5

Related Questions