PowerShell function for adding elements to an array - arrays

I'm still quite new to PowerShell and am trying to create a few functions that weaves together for creating and administrating an array. And I'm having some problems with getting one of these functions to work as intended.
I need the second function (AddToArray) to add an element to the specified index. None of the existing elements can be overwritten or removed.
For example, if I have an array with four indexes and all have the value 5 and I call the function AddToArray 2 4. I need the function to write for in the third index and move the existing ones one down step, so the array now looks like this:
5
5
4
5
5
This is my code so far that shows my CreateArray function and the little code piece for AddToArray function. I've been trying for a while now, but I just can't see the solution.
function CreateArray($Item1, $Item2)
{
$arr = New-Object Array[] $Item1;
# Kontrollerar om $Item2 har fått någon input och skriver in det i arrayen
if ($Item2)
{
for($i = 0; $i -lt $arr.length; $i++)
{
$arr[$i] = $Item2;
}
}
# Standard värde på arrayens index om inget värde anges vid funktionens anrop
else
{
$Item2 = "Hej $env:username och välkommen till vårat script!";
for($i = 0; $i -lt $arr.length; $i++)
{
$arr[$i] = $Item2;
}
}
$script:MainArray = $arr;
}
function AddToArray ($index, $add)
{
$MainArray[$index] = $add;
}

Arrays in .NET don't directly support insertion and they are normally fixed size. PowerShell does allow for easy array resizing but if the array gets large and you're appending (causing a resize) a lot, the performance can be bad.
One easy way to do what you want is to create a new array from the pieces e.g.:
if ($index -eq 0) {
$MainArray = $add,$MainArray
}
elseif ($index -eq $MainArray.Count - 1) {
$MainArray += $add
}
else {
$MainArray = $MainArray[0..($index-1)], $add, $MainArray[$index..($MainArray.Length-1)]
}
But that is kind of a spew. I would use a List for this, which supports insertion and is more efficient than an array.
$list = new-object 'System.Collections.Generic.List[object]'
$list.AddRange((1,2,3,4,5))
$list.Insert(2,10)
$list
And if you really need an array, call the $list.ToArray() method when you're done manipulating the list.

Arrays don't have an .insert() method, but collections do. An easy way to produce a collection from an array is to use the .invoke() method of scriptblock:
$array = 5,5,4,5,5
$collection = {$array}.invoke()
$collection
$collection.GetType()
5
5
4
5
5
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Collection`1 System.Object
Now you can use the .insert() method to insert an element at an arbitrary index:
$collection.Insert(2,3)
$collection
5
5
3
4
5
5
If you need it to be an array again, an easy way to convert it back to an array is to use the pipeline:
$collection | set-variable array
$array
$array.GetType()
5
5
3
4
5
5
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array

Related

Tuples/ArrayList of pairs

I'm essentially trying to create a list of pairs which is proving frustratingly difficult
Note before anyone mentions Hashtables that there will be duplicates which I don't care about.
For example, if I do
$b = #{"dog" = "cat"}
I get
Name Value
---- -----
dog cat
which is good. However, I'm then unable to add the likes of
$b += #{"dog" = "horse"}
Item has already been added. Key in dictionary: 'dog' Key being added: 'dog'
I'm just trying to create a table of data which I can add to using something like .Add() or +=.
I believe a list of hashtables should accomplish what you are after.
$ht = #{foo='bar'}
$list = [System.Collections.Generic.List[hashtable]]::new()
$list.Add($ht)
$list.Add($ht)
$list.Add($ht)
$list
If you want a list of tuples I'd recommend actually building a list of tuples:
$list = #()
$tuple1 = New-Object 'Tuple[String,String]' 'dog', 'cat'
$tuple2 = New-Object 'Tuple[String,String]' 'dog', 'horse'
$list += $tuple1
$list
# Output:
#
# Item1 Item2 Length
# ----- ----- ------
# dog cat 2
$list += $tuple2
$list
# Output:
#
# Item1 Item2 Length
# ----- ----- ------
# dog cat 2
# dog horse 2
Note that in this example $list is a regular array, meaning that, as #mklement0 pointed out in his answer, appending to it with the += assignment operator will re-create the array with size increased by 1, put the new item in the new empty slot, then replace the original array. For a small number of append operations this usually isn't a big issue, but with increasing number of append operations the performance impact becomes significant.
Using an ArrayList instead of a plain array avoids this issue:
$list = New-Object Collections.ArrayList
$tuple1 = New-Object 'Tuple[String,String]' 'dog', 'cat'
$tuple2 = New-Object 'Tuple[String,String]' 'dog', 'horse'
$list.Add($tuple1) | Out-Null
$list
# Output:
#
# Item1 Item2 Length
# ----- ----- ------
# dog cat 2
$list.Add($tuple2) | Out-Null
$list
# Output:
#
# Item1 Item2 Length
# ----- ----- ------
# dog cat 2
# dog horse 2
The Add() method of ArrayList objects outputs the index where the item was appended. Out-Null suppresses that output that is in most cases undesired. If you want to work with these index numbers you can collect them in a variable instead of discarding them ($i = $list.Add($t1)).
If you want to avoid having to specify the type of a new tuple all the time you can wrap it into a reusable function like this:
function New-Tuple {
Param(
[Parameter(Mandatory=$true)]
[ValidateCount(2,2)]
[string[]]$Values
)
New-Object 'Tuple[String,String]' $Values
}
$tuple1 = New-Tuple 'dog', 'cat'
$tuple2 = New-Tuple 'dog', 'horse'
or, in a more generic way, like this:
function New-Tuple {
Param(
[Parameter(
Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true
)]
[ValidateCount(2,20)]
[array]$Values
)
Process {
$types = ($Values | ForEach-Object { $_.GetType().Name }) -join ','
New-Object "Tuple[$types]" $Values
}
}
$tuples = ('dog', 'cat'), ('dog', 'horse') | New-Tuple
Persistent13's helpful answer offers an effective and efficient solution - albeit a slightly obscure one.
Creating an array of hashtables is the simplest solution, though it's important to note that "extending" an array means implicitly recreating it, given that arrays are fixed-size data structures, which can become a performance problem with many iterations:
# Using array-construction operator ",", create a single-element array
# containing a hashtable
$b = , #{ dog = "cat"}
# "Extend" array $b by appending another hashtable.
# Technically, a new array must be allocated that contains the original array's
# elements plus the newly added one.
$b += #{ dog = "horse"}
This GitHub issue discusses a potential future enhancement to make PowerShell natively support an efficiently extensible list-like data type or for it to even default to such a data type. (As of Windows PowerShell v5.1 / PowerShell Core 6.2.0, PowerShell defaults to fixed-size arrays, of type [object[]]).
Thanks to mklement0 as below is probably the simplest solution
$b = , #{ dog = "cat"}
$b += #{ dog = "horse"}
And Persistent13's method also yields an easy route
$ht = #{foo='bar'}
$list = [System.Collections.Generic.List[hashtable]]::new()
$list.Add($ht)
$list.Add($ht)
$list.Add($ht)
$list
I suppose i was just surprised that there wasn't a more ingrained way to get what i feel is a pretty basic/standard object type

How to insure json stays array

I want to remove an array element from json array (PSObject) if value matches as follows:
$code = 12345
$myObject = #{ ArrayPair= #(#{ code = 12345; perm = "RW" }, #{ code = 23456; perm = "RW" })}
if ($true) { # $revoke
$myObject.ArrayPair = $myObject.ArrayPair | Where-Object -FilterScript {$_.code -ne $code}
}
At the start ArrayPair has 2 array elements, after executing the filter, ArrayPair is no longer an array but rather an object with two elements. How can I keep it as an array so I can continue to add new pairs to the array?
json Values before and After removal:
Before value:
{"ArrayPair": [{"perm": "RW","code": 12345},{"perm": "RW","code": 23456}]}
After Value removal
{"ArrayPair": { "perm": "RW", "code": 23456 }}
You can force the object to stay an array like this:
$code = 12345
$myObject = #{ ArrayPair= #(#{ code = 12345; perm = "RW" }, #{ code = 23456; perm = "RW" })}
[array]$myObject.ArrayPair = $myObject.ArrayPair | Where-Object -FilterScript {$_.code -ne $code}
$myObject.ArrayPair.GetType()
#Returns
#IsPublic IsSerial Name BaseType
#-------- -------- ---- --------
#True True Object[] System.Array
To add additional entries to your array you have to try it like this:
$myObject.ArrayPair += #{code = 2134; perm= "RR"}
This way you can add entries to the array and the result looks like this:
PS C:\> $myObject.ArrayPair
Name Value
---- -----
code 23456
perm RW
code 2134
perm RR
Please be aware that += doens't really add objects to the array, but instead recreates the array with new values.
If you try to add the objects via $myObject.ArrayPair.Add(#{code = 2134; perm= "RR"}) you get an error.
Please take a look at this answer for further explanations:
PowerShell Array.Add vs +=
I while deleting elements if there was just one left, I found that I had to double force the object to make sure that it remained an array type:
[array]$temp = $result.data.app.roles.admin | Where-Object -FilterScript {$_.club -ne $ClubNo}
$result.data.app.roles.admin = [array]($temp)

Array.Find and IndexOf for multiple elements that are exactly the same object

I have trouble of getting index of the current element for multiple elements that are exactly the same object:
$b = "A","D","B","D","C","E","D","F"
$b | ? { $_ -contains "D" }
Alternative version:
$b = "A","D","B","D","C","E","D","F"
[Array]::FindAll($b, [Predicate[String]]{ $args[0] -contains "D" })
This will return:
D
D
D
But this code:
$b | % { $b.IndexOf("D") }
Alternative version:
[Array]::FindAll($b, [Predicate[String]]{ $args[0] -contains "D" }) | % { $b.IndexOf($_) }
Returns:
1
1
1
so it's pointing at the index of the first element. How to get indexes of the other elements?
You can do this:
$b = "A","D","B","D","C","E","D","F"
(0..($b.Count-1)) | where {$b[$_] -eq 'D'}
1
3
6
mjolinor's answer is conceptually elegant, but slow with large arrays, presumably due to having to build a parallel array of indices first (which is also memory-inefficient).
It is conceptually similar to the following LINQ-based solution (PSv3+), which is more memory-efficient and about twice as fast, but still slow:
$arr = 'A','D','B','D','C','E','D','F'
[Linq.Enumerable]::Where(
[Linq.Enumerable]::Range(0, $arr.Length),
[Func[int, bool]] { param($i) $arr[$i] -eq 'D' }
)
While any PowerShell looping solution is ultimately slow compared to a compiled language, the following alternative, while more verbose, is still much faster with large arrays:
PS C:\> & { param($arr, $val)
$i = 0
foreach ($el in $arr) { if ($el -eq $val) { $i } ++$i }
} ('A','D','B','D','C','E','D','F') 'D'
1
3
6
Note:
Perhaps surprisingly, this solution is even faster than Matt's solution, which calls [array]::IndexOf() in a loop instead of enumerating all elements.
Use of a script block (invoked with call operator & and arguments), while not strictly necessary, is used to prevent polluting the enclosing scope with helper variable $i.
The foreach statement is faster than the Foreach-Object cmdlet (whose built-in aliases are % and, confusingly, also foreach).
Simply (implicitly) outputting $i for each match makes PowerShell collect multiple results in an array.
If only one index is found, you'll get a scalar [int] instance instead; wrap the whole command in #(...) to ensure that you always get an array.
While $i by itself outputs the value of $i, ++$i by design does NOT (though you could use (++$i) to achieve that, if needed).
Unlike Array.IndexOf(), PowerShell's -eq operator is case-insensitive by default; for case-sensitivity, use -ceq instead.
It's easy to turn the above into a (simple) function (note that the parameters are purposely untyped, for flexibility):
function get-IndicesOf($Array, $Value) {
$i = 0
foreach ($el in $Array) {
if ($el -eq $Value) { $i }
++$i
}
}
# Sample call
PS C:\> get-IndicesOf ('A','D','B','D','C','E','D','F') 'D'
1
3
6
You would still need to loop with the static methods from [array] but if you are still curious something like this would work.
$b = "A","D","B","D","C","E","D","F"
$results = #()
$singleIndex = -1
Do{
$singleIndex = [array]::IndexOf($b,"D",$singleIndex + 1)
If($singleIndex -ge 0){$results += $singleIndex}
}While($singleIndex -ge 0)
$results
1
3
6
Loop until a match is not found. Assume the match at first by assigning the $singleIndex to -1 ( Which is what a non match would return). When a match is found add the index to a results array.

PowerShell array unshift

After reading this helpful article on the Windows PowerShell Blog, I realized I could "shift" off the first part of an array, but didn't know how to "unshift" or push elements onto the front of an array in PowerShell.
I am creating an array of hash objects, with the last read item pushed onto the array first. I'm wondering if there is a better way to accomplish this.
## Create a list of files for this collection, pushing item on top of all other items
if ($csvfiles[$collname]) {
$csvfiles[$collname] = #{ repdate = $date; fileobj = $csv }, $csvfiles[$collname] | %{$_}
}
else {
$csvfiles[$collname] = #{ repdate = $date; fileobj = $csv }
}
A couple of things to note:
I need to unroll the previous array element with a foreach loop %{$_}, because merely referencing the array would create a nested array structure. I have to have all the elements at the same level.
I need to differentiate between an empty array and one that contains elements. If I attempt to unroll an empty array, it places a $null element at the end of the array.
Thoughts?
PS: The reason the empty hash element produces a NULL value is that $null is treated as a scalar in PowerShell. For details, see https://connect.microsoft.com/PowerShell/feedback/details/281908/foreach-should-not-execute-the-loop-body-for-a-scalar-value-of-null.
ANSWER:
Looks like the best solution is to pre-create the empty array when necessary, rather than code around the $null issue. Here's the rewrite using a .NET ArrayList and a native PoSh array.
if (!$csvfiles.ContainsKey($collname)) {
$csvfiles[$collname] = [System.Collections.ArrayList]#()
}
$csvfiles[$collname].insert(0, #{ repdate = $repdate; fileobj = $csv })
## NATIVE POSH SOLUTION...
if (!$csvfiles.ContainsKey($collname)) {
$csvfiles[$collname] = #()
}
$csvfiles[$collname] = #{ repdate = $repdate; fileobj = $csv }, `
$csvfiles[$collname] | %{$_}
You might want to use ArrayList objects instead, as they support insertions at arbitrary locations. For example:
# C:\> $a = [System.Collections.ArrayList]#(1,2,3,4)
# C:\> $a.insert(0,6)
# C:\> $a
6
1
2
3
4
You can simply use a plus operator:
$array = #('bar', 'baz')
$array = #('foo') + $array
Note: this re-creates actually creates a new array instead of changing the existing one (but the $head, $tail = $array way of shifting you refer to works extactly in the same way).

Find missing rows in Powershell 2D arrays

I have 2 arrays, and each array has 2 fields ('item' and 'price' for example).
The following is the get-member result on 1 of my arrays (actually both arrays have the same structure)
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
item NoteProperty System.String field1=computer
price NoteProperty System.String field2=2000
I need to find the items in array $shopA where the items is not found in array $shopB. I am now using 2 loops to find the missing item.
$missing = #()
foreach ($itemA in $shopA) {
$found = 0
foreach ($itemB in $shopB) {
if ($itemB.item -eq $itemA.item) {
$found = 1
}
}
if ($found = 0) {
$missing += $itemA
}
}
This method works for me but my 2 arrays are quite large and I want a quicker method than looping thru the whole array...
I have been finding a better way to do this and the compare-object almost does the job but all the examples seem to work for single dimension array only.
Thanks
From what I can see you do have two 1D arrays, despite you claiming the opposite.
A naïve way of finding the missing items would be
$missing = $shopA | ? { $x = $_; !($shopB | ? {$_.item -eq $x.item})}
However, this will always be O(n²); to make it quicker you can collect all items from $shopB in a hastable first, which makes checking for existence O(1), not O(n):
$hash = #{}
$shopB | %{ $hash[$_.item] = 1 }
$missing = $shopA | ?{ !$hash.ContainsKey($_.item) }
Something like this?
$shopA= #(1, 2, 3, 4)
$shopB= #(4, 3, 5, 6)
$shopB | where { $shopA -notcontains $_ }

Resources