Why cant I store a hashtable in an arraylist? - loops

This is my code
$allTests = New-Object System.Collections.ArrayList
$singleTest = #{}
$singleTest.add("Type", "Human")
1..10 | foreach {
$singleTest.add("Count", $_)
$singleTest.add("Name", "FooBar...whatever..$_")
$singleTest.add("Age", $_)
$allTests.Add($singleTest) | out-null
$singleTest.remove("Count")
$singleTest.remove("Name")
$singleTest.remove("Age")
}
From my understanding my loop should be adding a copy of the hashtable to the arraylist everytime it gets to
$allTests.Add($singleTest) | out-null
the loop continues on, removes some keys and this paves the way for the next iteration of the loop . Thats not what happening, its like the add command is only adding a reference to the hashtable.
If I check the final value of
$allTests
this gets returned
Name Value
---- -----
Type Human
Type Human
Type Human
Type Human
Type Human
Type Human
Type Human
Type Human
Type Human
Type Human
How do I fix this so a actual copy of the hashtable is stored in the array list ?
I'm looking for an ouput like
$allTests[0]
Name Value
---- -----
Count 1
Name FooBar...whatever..1
Age 1
Type Human
$allTests[1]
Name Value
---- -----
Count 2
Name FooBar...whatever..2
Age 2
Type Human

Hashtables are references, when you create one object all further operations are against that one hashtable, including trying to retrieve that information.
You can declate a new hashtable each run of the loop to get around this.
$allTests = New-Object System.Collections.ArrayList
1..10 | foreach {
$singleTest = #{}
$singleTest.add("Type", "Human")
$singleTest.add("Count", $_)
$singleTest.add("Name", "FooBar...whatever..$_")
$singleTest.add("Age", $_)
$allTests.Add($singleTest) | Out-Null
}
or even this to cut out some bloat.
$allTests = New-Object System.Collections.ArrayList
1..10 | foreach {
$allTests.Add(#{
Type = "Human"
Count = $_
Name = "FooBar...Whatever..$_"
Age = $_
}) | Out-Null
}
Both of these answers will give you the expected output.

#ConnorLSW's answer is spot on functionally.
I have another solution for you that gives you more flexibility. I find myself building custom objects that share some fields so instead of making new objects every run of the loop you could define the base object outside the loop just as you are now and then inside the loop you can change a property value for that instance and then add it to your collection like this:
$allTests.Add($singleTest.Psobject.Copy())
This copys the contents to a new object before inserting it. Now you are not referencing the same object as you are changing during the next iteration of the loop.

Since hash tables are passed by reference, you're just adding multiple references to the same hash table to your arraylist. You need to create a new copy of the hash table and then add that to your array list.
One option is to use the hash table .clone() method when you want to save a copy to the arraylist.
$allTests.Add($singleTest.clone()) | out-null

Related

Powershell Compare 2 Arrays of Hashtables based on a property value

I have one array of hashtables like the one below:
$hashtable1 = #{}
$hashtable1.name = "aaa"
$hashtable1.surname =#()
$hashtable1.surname += "bbb"
$hashtable2 = #{}
$hashtable2.name = "aaa"
$hashtable2.surname =#()
$hashtable2.surname += "ccc"
$hashtable3 = #{}
$hashtable3.name = "bbb"
$hashtable3.surname = #()
$hashtable3.surname += "xxx"
$A = #($hashtable1; $hashtable2; $hashtable3)
I need to iterate though the array and I need to find out duplicates based on hashtable[].name
Then I need to group those hashtable.surname to hashtable[].surname so that the result will be an array of hashtables that will group all for name all the surnames:
$hashtable1.name = "aaa"
$hashtable1.surname = ("bbb","ccc")
$hashtable3.name = "bbb"
$hashtable3.surname = ("xxx")
I was looking into iterating to empty array
+
I have found this link:
powershell compare 2 arrays output if match
but I am not sure on how to reach into the elements of the hashtable.
My options:
I was wondering if -contain can do it.
I have read about compare-object but I am not sure it can be done like that.
(It looks a bit scary in the moment)
I am on PS5.
Thanks for your help,
Aster
You can group your array items by the names using a scriptblock like so.
Once grouped, you can easily build your output to do what you seek.
#In PS 7.0+ you can use Name directly but earlier version requires the use of the scriptblock when dealing with arrays of hashtables.
$Output = $A | Group-Object -Property {$_.Name} | % {
[PSCustomObject]#{
Name = $_.Name
Surname = $_.Group.Surname | Sort-Object -Unique
}
}
Here is the output variable content.
Name Surname
---- -------
aaa {bbb, ccc}
bbb xxx
Note
Improvements have been made in PS 7.0 that allows you to use simply the property name (eg: Name) in Group-Object for arrays of hashtables, just like you would do for any other arrays type. For earlier version though, these particular arrays must be accessed by passing the property in a scriptblock, like so: {$_.Name}
References
MSDN - Group_Object
SS64 - Group Object
Dr Scripto - Use a Script block to create custom groupings in PowerShell

PowerShell - Create an array that ignores duplicate values

Curious if there a construct in PowerShell that does this?
I know you can do this:
$arr = #(1,1,1,1,2,2,2,2,3,3,3,3,3,4,4,4,4,4)
$arr = $arr | Get-Unique
But seems like performance-wise it would be better to ignore the value as you are entering it into the array instead of filtering out after the fact.
If are you inserting a large number of items in to an array (thousands) the performance does drop, because the array needs to be reinitialized every time you add to it so it may be better in your case, performance wise, to use something else.
Dictionary, or HashTable could be a way. Your single dimensional unique array could be retrieved with $hash.Keys For example:
$hash = ${}
$hash.Set_Item(1,1)
$hash.Set_Item(2,1)
$hash.Set_Item(1,1)
$hash.Keys
1
2
If you use Set_Item, the key will be created or updated but never duplicated. Put anything else for the value if you're not using it, But maybe you'll have a need for a value with your problem too.
You could also use an Arraylist:
Measure-Command -Expression {
$bigarray = $null
$bigarray = [System.Collections.ArrayList]#()
$bigarray = (1,1,1,1,2,2,2,2,3,3,3,3,3,4,4,4,4,4)
$bigarray | select -Unique
}
Time passed:
TotalSeconds : 0,0006581
TotalMilliseconds : 0,6581
Measure-Command -Expression {
$array = #(1,1,1,1,2,2,2,2,3,3,3,3,3,4,4,4,4,4)
$array | select -Unique
}
Time passed:
TotalSeconds : 0,0009261
TotalMilliseconds : 0,9261

How to index into PSObject Array of Hashtables

I'm working in powershell with an imported CSV. I have a data set like this:
ID First_Name Last_Nam
-- ---------- --------
2314 Kenny Anderson
21588 Mark Anderson
2547 Ben Andrews
5797 Benjamin Armour
Except with 2000 people and many more columns. Currently the data is stored as a series of hashes in a #{ID = "..",First_Name:"..",Last_Name:".."} and these are stored in a System Object array. I want to store each hash as an index in an array. I want to get the hashtable at that index, but I don't know how to into the System.Object Array. Here's my code:
$csv = import-csv $csv_name
$row = #(0)*csv.length
$hash = #{}
for($i =0; $i -lt $csv.length; $i++){
$row[$i] += $csv[$i]
}
#error: cannot convert "#{ID : "..", First_Name: "..", Last_Name:".." to Systems.Int32
for($i =0; $i -lt $csv.length; $i++){
$csv[$i].psobject.properties | Foreach { $hash[$_.Name] = $_.Value }
$row[$i]+=$hash
}
#error: Cannot convert systems.collections.hashtable into Systems.Int32
I'm looking for a way to index the array so I can get the hashtable at that index. the The first one, with pointers to the hashtables accessible through the array, is what I think would be the easiest to manipulate. If there's an easier way to get a specific hashtable just from the System.Object[] itself, please tell me.
I should add I don't know the names or amount of the columns in advance.
The return value of Import-Csv is not an array of [hashtable]s, it's an array of [psobject]s, where each column is a property.
There's no need to build any kind of array to get an individual object by index.
$csv = import-csv $csv_name
$csv[0]
$csv[1].First_Name
# etc
The errors in your code have nothing to do with the question you posed though; you're getting errors trying to add objects to an array, because you're actually trying to add an object or hashtable to an existing array element of type integer.
You don't need to precreate the array with a bunch of zeros, so this line isn't needed:
$row = #(0)*csv.length
Instead, you can create an empty array:
$row = #()
Then, you can just add to the array itself:
$row += $hash

Quick File to Hashtable in PowerShell

Given an array of key-value pairs (for example read in through ConvertFrom-StringData), is there a streamlined way of turning this into a Hashtable or similar to allow quick lookup? I.e. a way not requiring me to loop through the array and manually build up the hashtable myself.
Example data
10.0.0.1=alice.example.com
10.0.0.2=bob.example.com
Example usage
$names = gc .\data.txt | ConvertFrom-StringData
// $names is now Object[]
$map = ?
// $map should now be Hashtable or equivalent
echo $map['10.0.0.2']
// Output should be bob.example.com
Basically what I'm looking for is a, preferably, built-in file-to-hashtable function. Or an array-to-hashtable function.
Note: As #mjolnior explained, I actually got hash tables, but an array of single value ones. So this was fixed by reading the file -raw and hence didn't require any array to hashtable conversion. Updated the question title to match that.
Convertfrom-Stringdata does create a hash table.
You need to give it the key-value pairs as a single multi-line string (not a string array)
$map = Get-Content -raw .\data.txt | ConvertFrom-StringData
$map['10.0.0.2']
bob.example.com
When you use Get-Content without the -Raw switch, you're giving ConvertFrom-StringData an array of single-line strings, and it's giving you back an array of single-element hash tables:
$map = Get-Content .\data.txt | ConvertFrom-StringData
$map.gettype()
$Map[0].GetType()
$map[0]
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
True True Hashtable System.Object
Key : 10.0.0.1
Value : alice.example.com
Name : 10.0.0.1
I usually do the following to create a hashtable from a list of key/value pairs:
$hash = #{}
Get-Content 'C:\input.txt' | Where-Object {
$_ -like '*=*'
} | ForEach-Object {
$key, $value = $_ -split '\s*=\s*', 2
$hash[$key] = $value
}
This might not be what you're looking for, but it avoids converting the whole thing into a hash.
$content = #("10.0.0.1=alice.example.com","10.0.0.2=bob.example.com");
$content | ForEach-Object {
$keyval = $_.split("=");
if ($keyval[0] -eq "10.0.0.2") {
$keyval[1]
}
}
The output will be every value on the right side of the = where the left side matches that IP.

Powershell array values being overwritten

I'm at a bit of a loss to understand why this code isn't doing what I think it should.
This is part of a larger plan but in essence I'm attempting to fill part of an array with entries from a text file, though I've replaced that bit with a hard coded array. The end result is the same.
$users = 12345,23456,34567,45678,56789,67890
$a = New-Object PSCustomObject
$a = $a|Select ID
$collection = #()
$users|%{
$a.ID = $_
$collection += $a
}
$collection|ft -a
This outputs the following:
ID
--
67890
67890
67890
67890
67890
67890
If you output the array to screen as it gets built, you can watch the values get replaced each time with the most recent entry.
What's the fault? Is there something unusual with the way I'm initialising the $a variable or the array?
It's doing that because there is only one object ($a), and all your loop is doing is changing the value of the one property (ID), and adding another reference to it to the array.
You need to create a new object for each cycle of the loop:
$users = 12345,23456,34567,45678,56789,67890
$a = New-Object PSCustomObject
$a = $a|Select ID
$collection = #()
$users|%{
$a = New-Object PSCustomObject
$a = $a|Select ID
$a.ID = $_
$collection += $a
}
$collection|ft -a
PS is adding a reference to the object itself into the array, rather than the object contents at the time it's added. Changing the value of $a.ID at any point changes the contents displayed by $collection.
To fix this, you can move the initialization for $a within the % statement like so:
$users = 12345,23456,34567,45678,56789,67890
$collection = #()
$users|%{
$a = New-Object PSCustomObject
$a = $a|Select ID
$a.ID = $_
$collection += $a
}
$collection|ft -a
or just simply add $a.ID to $collection if you only want the ID value in that array.
Not quite sure what you're trying to accomplish here, really... but what if you try it like this?
$collection += $a.ID
You create one object ($a). You then add this one object to $collection multiple times. Each time you add it you change its .ID property value. So at the end you have $collection holding many copies of this one object. So you get many copies of that one ID value. Create a new object each time inside the loop if you want multiple objects with different values.

Resources