ConvertTo-OrderedDictionary

ConvertTo-OrderedDictionary

  • Comments 8
  • Likes

Summary: Learn about creating ordered dictionaries in Windows PowerShell 3.0.

Hash tables are fabulous for storing data items that are associated with each other. The simple "key=value" format is easy to create and easy to search.

The only problem is that the order of elements in the hash table is arbitrary. If you need to have the items in a particular order, you need to sort them every time.

PS C:\> $hash = @{a=1; b=2; c=3}

PS C:\> $hash

Name                           Value

----                           -----

c                              3

b                              2

a                              1

PS C:\ > $hash.GetEnumerator() | sort

Name                           Value

----                           -----

a                              1

b                              2

c                              3

A new feature in Windows PowerShell 3.0 makes it easy to create ordered dictionaries, which are like hash tables that maintain the order of elements. To create an ordered dictionary, simply place the [ordered] attribute before the "@" symbol.

PS C:\> $dictionary = [ordered]@{a=1; b=2; c=3}

PS C:\> $dictionary 

Name                           Value

----                           -----

a                              1

b                              2

c                              3

Be sure to place the [ordered] before the @ and not before the variable name. If you mess up, you get the "The ordered attribute can be specified only on a hash literal node" error message, which is very accurate, I'm sure, but isn't easy to interpret without a computer science degree. Interpret it as, "You can't put the [ordered] attribute on a variable. Put it right before the @."

PS C:\> [ordered]$dictionary = @{a=1;b=2;c=3}

At line:1 char:1

+ [ordered]$dictionary = @{a=1;b=2;c=3}

+ ~~~~~~~~~~~~~~~~~~~~

The ordered attribute can be specified only on a hash literal node.

    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException

    + FullyQualifiedErrorId : OrderedAttributeOnlyOnHashLiteralNode

And the [ordered] attribute works ONLY on hash tables. You can't use it on other types of collections, such as arrays (comma-separated lists), because they don't have the key-value pair format.

PS C:\> $colors = [ordered]("red", "green", "blue")

At line:1 char:11

+ $colors = [ordered]("red", "green", "blue")

+           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The ordered attribute can be specified only on a hash literal node.

    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException

    + FullyQualifiedErrorId : OrderedAttributeOnlyOnHashLiteralNode

Ordered dictionaries are instances of the System.Collections.Specialized.OrderedDictionary class. In addition to many hash table-like properties and methods, you get a few goodies that are available because of the preserved order, such as indexing.

You can index into items in an ordered dictionary. The following command gets the value of the item at index 1. (Remember that the index starts at zero.)

PS C:\> $dictionary

Name                           Value

----                           -----

a                              1

b                              2

c                              3

PS C:\> $dictionary[1]

2

You can insert key-value pairs at a particular index position. The syntax of the Insert method is:

Insert(<index>, <key>, <value>)

PS C:\> $dictionary.Insert(1, "d", 2.5)

PS C:\> $dictionary

Name                           Value

----                           -----

a                              1

d                              2.5

b                              2

c                              3

You can remove key-value pairs at a particular index position. The syntax of the RemoveAt method is:

RemoveAt(<index>)

Name                           Value

----                           -----

a                              1

d                              2.5

b                              2

c                              3

PS C:\> $dictionary.RemoveAt(2)

PS C:\> $dictionary 

Name                           Value

----                           -----

a                              1

d                              2.5

c                              3

To perform operations on each element in an ordered dictionary, you can use a ForEach loop. That's what I usually do.

PS C:\> foreach ($value in $dictionary.values){$sum += $value}

PS C:\> $sum

6.5

But because it's indexed, you can also get values by using a traditional For loop.

PS C:\> for ($i = 0; $i -lt $dictionary.count; $i++)

>> {$sum += $dictionary[$i]}

PS C:\> $sum

6.5

All terrific stuff.  But if you forget about ordered dictionaries and create a standard hash table, you can't use the [ordered] attribute to cast or convert a hash table to an ordered dictionary. If you do...

PS C:\> $newDict = [ordered]$hash

At line:1 char:12

+ $newDict = [ordered]$hash

+            ~~~~~~~~~~~~~~

The ordered attribute can be specified only on a hash literal node.

    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException

    + FullyQualifiedErrorId : OrderedAttributeOnlyOnHashLiteralNode

So, I wrote a little script that converts a hash table to an ordered dictionary. Okay, it doesn't really convert anything. It creates a new ordered dictionary, sorts the keys in the hash table alphanumerically, adds the sorted key-value pairs in the hash table to the ordered dictionary, and returns the ordered dictionary.

Here's how you use it:

PS C:\>$hash = @{a=1;b=2;c=3}

#Oops! I meant to create a dictionary

PS C:\>$hash = .\ConvertTo-OrderedDictionary.ps1 -Hash $hash

PS C:\> $hash | Get-Member

   TypeName: System.Collections.Specialized.OrderedDictionary

PS C:\> $hash

Name                           Value

----                           -----

a                              1

b                              2

c                              3

PS C:\> $hash.Insert(1, "ItWorks!", [System.Math]::pi)

PS C:\> $hash

Name                           Value

----                           -----

a                              1

ItWorks!                       3.14159265358979

b                              2

c                              3

Ed mentioned that people also need to convert arrays to ordered dictionaries, so I added a little feature that does the deed. The keys in the ordered dictionary are integers beginning with 0, like this:

$winter = "December", "January", "February"

$winter = .\ConvertTo-OrderedDictionary.ps1 -Hash $winter

PS C:\> $winter

Name                           Value

----                           -----

0                              December

1                              January

2                              February

If you convert the script to a function, you can pipe hash tables to it. The following command uses the AsHashTable parameter of the Group-Object cmdlet to get a hash table of the Convert cmdlets in each module. The command passes the hash table to ConvertTo-OrderedDictionary, which returns an ordered dictionary.

PS C:\> $d = Get-Command Convert* | Group-Object -Property ModuleName -AsHashTable | ConvertTo-OrderedDictionary

PS C:\> $d

Name                           Value

----                           -----

                               {ConvertTo-OrderedDictionary, convert.exe}

Microsoft.PowerShell.Manage... {Convert-Path}

Microsoft.PowerShell.Security  {ConvertFrom-SecureString, ConvertTo-SecureString}

Microsoft.PowerShell.Utility   {ConvertFrom-Csv, ConvertFrom-Json, ConvertFrom-StringData, ConvertTo-Csv...}

MSOnline                       {Convert-MsolDomainToFederated, Convert-MsolDomainToStandard, Convert-MsolFederatedUser}

MSOnlineExtended               {Convert-MsolFederatedUser}

TrustedPlatformModule          {ConvertTo-TpmOwnerAuth}

 

PS C:\> $d.gettype().fullname

System.Collections.Specialized.OrderedDictionary

The entire script is available to download from the Script Center Repository: ConvertTo-OrderedDictionary. Have fun!

~June

Thanks, June!

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • I don't see the point in converting an array to an order hash using numeric keys starting at 0.  The array already has a 0-based index.  Now, if you start at 1, you can use that to emulate a 1-based array index, which can be quite useful in some applications.

  • That's an easy fix, Rob. The script is at gallery.technet.microsoft.com/.../ConvertTo-OrderedDictionary-cf2404ba. It would be neat if they had push requests. I would accept. :)

  • @mjolinor - can you easily insert elements into an array?

    Try this:

    $a=1,2,3,4

    $a.Insert(2,'x')

  • @jrv - No, but I can covert an array to a collection (which I can insert into) pretty easily:

    $a = 1,2,3,4

    $a = {$a}.invoke()

    $a.Insert(2,'x')

  • @mjolinor - so now you have a new way to do the same thing.  Sometimes its convenient and we can re-label the indexes if needed.

  • That's what I meant about about starting at 1, to emulate  1-based (instead of 0-based) indexing.  

  • Rob, what is the Invoke method doing? Why is the $a a script block (enclosed in curly braces)? Which classes' Invoke method is this?

  • @JunnB

    PS > $a = 1,2,3,4

    PS > $a.GetType()

    IsPublic IsSerial Name                                     BaseType

    -------- -------- ----                                     --------

    True     True     Object[]                                 System.Array

    PS > $a = {$a}.invoke()

    PS > $a.GetType()

    IsPublic IsSerial Name                                     BaseType

    -------- -------- ----                                     --------

    True     True     Collection`1                             System.Object

    PS > {$a}.invokeReturnAsIs()

    Try it.