0

I'm trying to programmatically combine an unknown number of hashtables into a larger hashtable. Each individual table will have the same keys. I tried just appending it, but it throws an error about duplicate keys.

ForEach ($Thing in $Things){
  $ht1 = New-Object psobject @{
    Path = $Thing.path
    Name = $Thing.name
  }
  $ht2 += $ht1
}

That throws the error

Item has already been added. Key in dictionary: 'Path'  Key being added: 'Path'

The end result would be that later I can say

ForEach ($Item in $ht2){
  write-host $Item.path
  write-host $Item.name
}
Dave
  • 167
  • 3
  • 18
  • Since a hashtable MUST have unique keys you can't combine them when duplicate keys are present (how would you imagine looking up a value if they key isn't unique?) – bluuf Jan 16 '21 at 07:38
  • Does this answer your question? [Merging hashtables in PowerShell: how?](https://stackoverflow.com/questions/8800375/merging-hashtables-in-powershell-how) – iRon Jan 16 '21 at 09:03
  • As with [avoiding the increase assignment operator (`+=`) to build a collection](https://stackoverflow.com/a/60708579/1701026), I recommend you to avoid this operator also for building hashtables as it is exponential expensive. – iRon Jan 16 '21 at 09:10
  • 1
    I just noticed that your title is very confusing as you actually not adding [`hashtable`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_hash_tables) but a [`pscustomobject`](https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-pscustomobject). You might just add a collection like this: `$ht2 += New-Object psobject @{ Path = $Thing.path Name = $Thing.name }`, but again, you should avoid the `+=` meaning that is better also stream the initial part of the object table. – iRon Jan 16 '21 at 09:37
  • 1
    In other words: is `$ht2` a **Hashtable** or an **Object Collection**? what does `$ht2.GetType().Name` (before you add anything) return? `Hashtable` or `Object[]`, or ... ??? – iRon Jan 16 '21 at 10:06
  • 1
    Your final bit of code reveals how you intend to use this data. From that, I under that what you really want is a relation, or some representation of a relation in powershell terms. I suggest you go for an array of hashtables. That way, each hashtables can represent a couple and the array can represent a relation. – Walter Mitty Jan 16 '21 at 11:11
  • @WalterMitty that seems to be the correct answer for what I was needing. Putting them in an array got me the results I needed. Thank you. – Dave Jan 17 '21 at 14:24

4 Answers4

1

Converting my comment to an answer.

What you probably want to create is an array of hashtables. Each item in the array can have its own value for each key. This structure can be used in the way you indicate in your query at the end of your post.

Try this:

$things = gci $home

$ht2 = @()       # create empty array

ForEach ($Thing in $Things){
  $ht1 = New-Object psobject @{
    Path = $Thing.PSpath
    Name = $Thing.name
  }
  $ht2 += $ht1
}

$ht2

Note that I changed .path to .PSpath in order to make the example work. Note that $ht2 gets initialized to an empty array before looping.

Walter Mitty
  • 18,205
  • 2
  • 28
  • 58
0

Adding an element to a hash table indeed fails if there already is an element with same key. To overcome that, first see if the element exists. Like so,

if(-not $ht2.ContainsKey($thing.name)){
    $ht2.Add($thing.name, $thing.value)
}

It is also possible to use item property [] for the check, since it returns $null for non-existing element. Like so,

if($ht2[$thing.name] -eq $null){
    $ht2.Add($thing.name, $thing.value)
}
vonPryz
  • 22,996
  • 7
  • 54
  • 65
0

If this really is about merging Hashtables as the title says, then basically you have two options to add the entries from the second hashtable into the first.

1. Use the static method Add() (first item 'wins')

This has already been explained in vonPryz's answer, but in short:

Adding an entry in a hashtable with a key that already exists in the hash, the Add() method will throw an exception, because all keys in a hash must be unique.
To overcome that, you need to check if an entry with that key exists and if so, do not add the entry from the second hash.

    foreach ($key in $secondHash.Keys) {
        if (!$firstHash.Contains($key)) {
            # only add the entry if the key (name) did not already exist
            $firstHash.Add($key, $secondHash[$key])  
        }
    }

This way, all entries already in the first hashtable will NOT get overwritten and duplicate entries from the second hash are discarded.

2. Overwriting/adding regardless of the existance (last item 'wins')

You can also opt to 'merge' the entries without the need for checking like this:

    foreach ($key in $secondHash.Keys) {
        # always add the entry even if the key (name) already exist
        $firstHash[$key] = $secondHash[$key]
    }

This way, if an entry already existed in the first hash, its value will be overwritten with the value from the second hash. If the entry did not already exist, it is simply added to the first hashtable.

But, what if you want to merge without skipping or overwriting existing values?

In that case, you need to come up with some method of creating a unique key for the entry to add.
Something like this:

    foreach ($key in $secondHash.Keys) {
        if ($firstHash.Contains($key)) {
            # ouch, the key already exists.. 
            # now, we only want to add this if the value differs (no point otherwise)
            if ($firstHash[$key] -ne $secondHash[$key]) {
                # add the entry, but create a new unique key to store it under first
                # this example just adds a number between brackets to the key
                $num = 1
                do {
                    $newKey = '{0}({1})' -f $key, $num++
                } until (!$firstHash.Contains($newKey))
                # we now have a unique new key, so add it
                $firstHash[$newKey] = $secondHash[$key]
            }
        }
        else {
            # no worries, the key is unique
            $firstHash[$key] = $secondHash[$key]
        }
    }
Theo
  • 57,719
  • 8
  • 24
  • 41
0

Turns out what I needed for my results was an array of hashtables, not a hashtable of hashtables, as pointed out by @WalterMitty. My final code was:

#varibale name ht2 kept for clarity in how it relates to original question

$ht2 = @()

ForEach ($Thing in $Things){
  $ht1 = New-Object psobject @{
    Path = $Thing.path
    Name = $Thing.name
  }
  $ht2 += $ht1
}
Dave
  • 167
  • 3
  • 18
  • @Theo i'm still not 100% clear on the difference between those two, so I apologize for any mixup there. Been reading up on it. As for accepting his answer, initially it was just a comment. I saw he made it an answer and gave it a check. – Dave Jan 19 '21 at 00:42