1

I have been developing a nested dictionary data structure to build a list of files to work with later in the code. Currently I am using this successfully to create the empty data structure.

$manageLocalAssets = @{
        resources = @{
            extra  = New-Object Collections.ArrayList
            new    = New-Object Collections.ArrayList
            skip   = New-Object Collections.ArrayList
            update = New-Object Collections.ArrayList
        }
        definitions = @{
            extra  = New-Object Collections.ArrayList
            new    = New-Object Collections.ArrayList
            skip   = New-Object Collections.ArrayList
            update = New-Object Collections.ArrayList
        }
    }

This works, but I would like to use an Ordered Dictionary rather than Hash Tables, so I can loop through the data in the order defined. Also, I need to support PS2.0 (don't ask) so I can't use the [ordered] type.

I have tried

[Collections.Specialized.OrderedDictionary]$manageLocalAssets = @{
        resources = @{
            extra  = New-Object Collections.ArrayList
            new    = New-Object Collections.ArrayList
            skip   = New-Object Collections.ArrayList
            update = New-Object Collections.ArrayList
        }
        definitions = @{
            extra  = New-Object Collections.ArrayList
            new    = New-Object Collections.ArrayList
            skip   = New-Object Collections.ArrayList
            update = New-Object Collections.ArrayList
        }
    }

and I have tried

$manageLocalAssets = [Collections.Specialized.OrderedDictionary]@{
        resources = @{
            extra  = New-Object Collections.ArrayList
            new    = New-Object Collections.ArrayList
            skip   = New-Object Collections.ArrayList
            update = New-Object Collections.ArrayList
        }
        definitions = @{
            extra  = New-Object Collections.ArrayList
            new    = New-Object Collections.ArrayList
            skip   = New-Object Collections.ArrayList
            update = New-Object Collections.ArrayList
        }
    }

Both fail to cast the hash table to an ordered dictionary. Is my only option something ugly (IMO) like this

$manageLocalAssets = New-Object Collections.Specialized.OrderedDictionary
$manageLocalAssets.Add('resources', (New-Object Collections.Specialized.OrderedDictionary))
$manageLocalAssets.resources.Add('extra', (New-Object Collections.ArrayList))

[ss64][1] has me thinking -property might be the answer, and this seems to be close

$manageLocalAssets = New-Object Collections.Specialized.OrderedDictionary -property @{
        resources = New-Object Collections.Specialized.OrderedDictionary -property @{
            extra  = New-Object Collections.ArrayList
            new    = New-Object Collections.ArrayList
            skip   = New-Object Collections.ArrayList
            update = New-Object Collections.ArrayList
        }
        definitions = New-Object Collections.Specialized.OrderedDictionary -property @{
            extra  = New-Object Collections.ArrayList
            new    = New-Object Collections.ArrayList
            skip   = New-Object Collections.ArrayList
            update = New-Object Collections.ArrayList
        }
    }

But it fails to create the nested keys, extra, skip, etc. Am I going in the right direction, or am I better off just admitting a bunch of separate New-Object and .Add lines are the way to go?

EDIT #1: I had a thought, since at the deepest level the data structure is identical, I tried this

$assets = New-Object Collections.Specialized.OrderedDictionary -property @{
        extra  = New-Object Collections.ArrayList
        new    = New-Object Collections.ArrayList
        skip   = New-Object Collections.ArrayList
        update = New-Object Collections.ArrayList
    }
    $manageLocalAssets = New-Object Collections.Specialized.OrderedDictionary -property @{
        resources = $assets
        definitions = $assets
    }

and the odd things is, the error is

New-Object : The member "skip" was not found for the specified .NET object.
At \\Mac\Support\Px Tools\Dev 4.0\Resources\PxContext_Machine.ps1:413 char:15
+ ...   $assets = New-Object Collections.Specialized.OrderedDictionary -pro ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [New-Object], InvalidOperationException
    + FullyQualifiedErrorId : InvalidOperationException,Microsoft.PowerShell.Commands.NewObjectCommand

New-Object : The member "resources" was not found for the specified .NET object.
At \\Mac\Support\Px Tools\Dev 4.0\Resources\PxContext_Machine.ps1:419 char:26
+ ... calAssets = New-Object Collections.Specialized.OrderedDictionary -pro ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [New-Object], InvalidOperationException
    + FullyQualifiedErrorId : InvalidOperationException,Microsoft.PowerShell.Commands.NewObjectCommand
  [1]: https://ss64.com/ps/new-object.html

413 is the $assets = New-Object Collections.Specialized.OrderedDictionary -property @{ line, while 419 is the $manageLocalAssets = New-Object Collections.Specialized.OrderedDictionary -property @{ line.

So, it seems like extra and new are being handled at first, then skip fails in the first assignment, and resources fails right off the bat in the second assignment.

EDIT #2: So, breaking out the repeating part as a different variable lead me to this

$assets = New-Object Collections.Specialized.OrderedDictionary
$assets.Add('extra', (New-Object Collections.ArrayList))
$assets.Add('new', (New-Object Collections.ArrayList))
$assets.Add('skip', (New-Object Collections.ArrayList))
$assets.Add('update', (New-Object Collections.ArrayList))

$manageLocalAssets = New-Object Collections.Specialized.OrderedDictionary
$manageLocalAssets.Add('resources', $assets)
$manageLocalAssets.Add('definitions', $assets)

Which is actually fewer lines, and will be even more efficient as I add to $manageLocalAssets. It doesn't look visually like my other data structure initializations, but I guess in time I will get used to that. And it lets me use a loop structure like this to work with the data in the desired order.

foreach ($asset in $manageLocalAssets.keys) {
    foreach ($key in ($manageLocalAssets.$asset).keys) {
        Write-Host "$asset $key"
        foreach ($file in $manageLocalAssets.$asset.$key) {
            Write-Host " $file"
        }
    }
}

Not "ideal", but then ideal here means "familiar", and that's not a great reason to make decisions.

EDIT #3: Nope. I was wrong. This actually makes the content of both $manageLocalAssets.resources and $manageLocalAssets.definitions the same, the $assets variable. Seems I am still looking.

Gordon
  • 6,257
  • 6
  • 36
  • 89
  • PS has no special support for `OrderedDictionary` so this will continue to be painful no matter how you slice it. Consider having a regular hash table and extending it by tacking on a property that holds the keys in order and enumerating only that; this way you get the best of both worlds (sort of). You can wrap this in a little constructor function that eats an array. – Jeroen Mostert Jan 25 '19 at 11:50
  • @JeroenMostert, I know I have no native support, but I can use the .NET types. Using the ugly way to build the data structure works fine, without resorting to a native hash table and a sorting property. It's just the ugly structure creation I would like to find a solution to. In the end, an ugly construction and readable use seems better than an elegant construction and fussy use where I need to sort by the property first, every time I use the variable. – Gordon Jan 25 '19 at 11:55
  • Enumerating an `OrderedDictionary` or `Hashtable` gives you `KeyValuePair`s. I rarely enumerate them that way, instead looping over the `.Keys`, which would work just as well when looping over `.OrderedKeys`. This does assume you only construct it once and don't add new keys later; if that's your scenario it's much less attractive. – Jeroen Mostert Jan 25 '19 at 11:57
  • Using `-Property` will not work here, because as the name says, this is used to initialize the object's properties, not keys. – marsze Jan 25 '19 at 11:59
  • You could also do this for ordered loop: `foreach($key in ("extra", "new", ...)){$unordered[$key]}` – marsze Jan 25 '19 at 14:09

1 Answers1

1

I suppose backward-compatibility is never pretty? This is the best I could come up with, coming somewhat close to a structured definition (using a simple array):

$definition = ((
"resources", (
    "extra",
    "new",
    "skip",
    "update"
)), (
"definitions", (
    "extra",
    "new",
    "skip",
    "update"
)))
$manageLocalAssets = New-Object System.Collections.Specialized.OrderedDictionary
$definition | foreach {
    $sub = New-Object System.Collections.Specialized.OrderedDictionary
    $_[1] | foreach {
        $sub.Add($_, (New-Object Collections.ArrayList))
    }
    $manageLocalAssets.Add($_[0], $sub)
}

Of course, you could put the actual values (the ArrayLists) in the array definition too, but I suppose that would just be a lot more to write and not look so pretty.

Alternative solution, using a function to create the ordered dictionary with variable levels from a definition. Somewhat more verbose but re-usable:

function New-OrderedDictionary ($definition) {
    $dict = New-Object System.Collections.Specialized.OrderedDictionary
    foreach ($item in $definition) {
        $key, $value = $item[0, 1]
        # you might want to change this check,
        # depending on how you want to built the definition
        if ($value.Count -gt 0) {
            $value = New-OrderedDictionary $value
        }
        $dict.Add($key, $value)
    }
    return $dict
}

$manageLocalAssets = New-OrderedDictionary ((
    "resources", (
        ("extra" , (New-Object System.Collections.Specialized.OrderedDictionary)),
        ("new"   , (New-Object System.Collections.Specialized.OrderedDictionary)),
        ("skip"  , (New-Object System.Collections.Specialized.OrderedDictionary)),
        ("update", (New-Object System.Collections.Specialized.OrderedDictionary))
    )), (
    "definitions", (
        ("extra" , (New-Object System.Collections.Specialized.OrderedDictionary)),
        ("new"   , (New-Object System.Collections.Specialized.OrderedDictionary)),
        ("skip"  , (New-Object System.Collections.Specialized.OrderedDictionary)),
        ("update", (New-Object System.Collections.Specialized.OrderedDictionary))
)))
marsze
  • 15,079
  • 5
  • 45
  • 61
  • 1
    This confused me until I realized that the first line is standalone and `((` began a brand new pipeline. It confused me because I know `OrderedDictionary` has no constructor taking an `IEnumerable` (a lot of collection types do). I humbly suggest inserting a single line break and/or turning the initialization into a cmdlet for better readability (and reuse). – Jeroen Mostert Jan 25 '19 at 12:06
  • @JeroenMostert I wanted to make it extra-short and save as many lines as possible, while still keeping the structured key definition. I put that array into seperate variable now, that hopefully makes it more readable. I don't think making a cmdlet is appropriate, as this is a very special case (2 levels deep with ArrayLists etc.) but I agree one could put this into a function. – marsze Jan 25 '19 at 12:22
  • Well, you could make it recursive and support any number of levels that way (assuming the lowest level is always a list), but yeah, that gets more complicated. When I say "cmdlet" though, I just mean a local one, not something that gets put in a module and used generally. But yes, a plain old function that doesn't use `[CmdletBinding()]` would also do. – Jeroen Mostert Jan 25 '19 at 12:24
  • @JeroenMostert I decided to give an example for that anyways, so thanks. – marsze Jan 25 '19 at 12:45