Passing Powershell Arrays by-reference not working as intended - arrays

Arrays by reference
Working fine!
The normal way to pass an Array by-reference in PowerShell seems to work fine:
Function Swap-Array ($theArray, $theArrayB, [int]$indexToSwap) {
$temp = $theArrayA[$indexToSwap];
$theArrayA[$indexToSwap] = $theArrayB[$indexToSwap];
$theArrayB[$indexToSwap] = $temp;
}
$a = #(1,2,3,4)
$b = #(3,2,4,1)
$a
$b
Swap-Array $a, $b, 2
$a
$b
Output:
a
-
1
2
3
4
b
-
3
2
4
1
a
-
1
2
4
3
b
-
3
2
3
1
The problem
Adding Objects
The issue arises when the array by-refernece is a container of PSObjects that's not static, and I am attempting to add a new record. Modifying the existing records seems to be fine!
Function Swap-Apples($objectA, $objectB, $indexToSwap) {
$temp = $objectA[$indexToSwap].Apples;
$objectA[$indexToSwap].Apples = $objectB[$indexToSwap].Apples;
$objectB[$indexToSwap].Apples = $temp;
}
Function Swap-Oranges($objectA, $objectB, $indexToSwap) {
$temp = $objectA[$indexToSwap].Oranges;
$objectA[$indexToSwap].Oranges = $objectB[$indexToSwap].Oranges;
$objectB[$indexToSwap].Oranges = $temp;
}
<# heres the problematic bit #>
Function Add-Fruit ($object, [int]$howManyApples, [int]$howManyOranges) {
$hAdd = #{
Apples=$howManyApples
Oranges=$howManyOranges
}
$hToAdd = New-Object -TypeName PSObject -Property $hAdd;
$object += $hToAdd;
}
$a = #();
$b = #();
$a1 = #{
Apples=3
Oranges=2
}
$b1 = #{
Apples=5
Oranges=7
}
$a2 = #{
Apples=6
Oranges=3
}
$b2 = #{
Apples=1
Oranges=5
}
$aObject1 = New-Object -TypeName PSObject -Property $a1;
$bObject1 = New-Object -TypeName PSObject -Property $b1;
$aObject2 = New-Object -TypeName PSObject -Property $a2;
$bObject2 = New-Object -TypeName PSObject -Property $b2;
$a += $aObject1; $a += $aObject2;
$b += $bObject1; $b += $aObject2;
Write-Host "Values of A";
$a | Format-List
Write-Host "Values of B";
$b | Format-List
Write-Host "Now lets make a trade`!";
Swap-Apples $a $b 0
Swap-Oranges $a $b 1
Write-Host "Values of A";
$a | Format-List
Write-Host "Values of B";
$b | Format-List
Write-Host "Hey, I brought more fruit for A`!";
Add-Fruit -object $a -howManyApples 5 -howManyOranges 2
Write-Host "Values of A";
$a | Format-List
Write-Host "I brought more fruit for B too`!";
Add-Fruit -object $b -howManyApples 5 -howManyOranges 3
Write-Host "Values of B";
$b | Format-List
Output
Values of A
Oranges : 2
Apples : 3
Oranges : 3
Apples : 6
Values of B
Oranges : 7
Apples : 5
Oranges : 3
Apples : 6
Now lets make a trade!
Values of A
Oranges : 2
Apples : 5
Oranges : 3
Apples : 6
Values of B
Oranges : 7
Apples : 3
Oranges : 3
Apples : 6
Hey, I brought more fruit for A!
Values of A
Oranges : 2
Apples : 5
Oranges : 3
Apples : 6
I brought more fruit for B too!
Values of B
Oranges : 7
Apples : 3
Oranges : 3
Apples : 6
The Swap-Apples and Swap-Oranges Functions seem to work fine. The program falls apart at the last segment, trying to give both A and B more fruit ! This would otherwise normally work in Local Scope. I feel like this is falling apart due to by-reference passing.
How would I go about fixing the problem at the end of this program ?

The Solution
Dynamic ArrayLists
In PowerShell, you can create arrays of both fixed-size and dynamically allocated. If I would like to give both A and B more objects, I have to tell PowerShell that this is an ArrayList and not the typical standard Array.
This means I cannot declare my array like this:
$a = #();
$b = #();
This type in .NET is called System.Collections.ArrayList and can be passed by-reference in a PowerShell program, like so:
$a = New-Object -TypeName 'System.Collections.ArrayList';
$b = New-Object -TypeName 'System.Collections.ArrayList';
Now its not fixed-size, so I can add records anywhere in my program at my leasure, even by reference!
Here is the solution:
Function Swap-Apples($objectA, $objectB, $indexToSwap) {
$temp = $objectA[$indexToSwap].Apples;
$objectA[$indexToSwap].Apples = $objectB[$indexToSwap].Apples;
$objectB[$indexToSwap].Apples = $temp;
}
Function Swap-Oranges($objectA, $objectB, $indexToSwap) {
$temp = $objectA[$indexToSwap].Oranges;
$objectA[$indexToSwap].Oranges = $objectB[$indexToSwap].Oranges;
$objectB[$indexToSwap].Oranges = $temp;
}
<# ArrayList! #>
Function Add-Fruit ([System.Collections.ArrayList]$object, [int]$howManyApples, [int]$howManyOranges) {
$hAdd = #{
Apples=$howManyApples
Oranges=$howManyOranges
}
$hToAdd = New-Object -TypeName PSObject -Property $hAdd;
<# We have to call the ArrayList Add method to add to our dynamic object #>
$object.Add($hToAdd);
}
$a = New-Object -TypeName 'System.Collections.ArrayList';
$b = New-Object -TypeName 'System.Collections.ArrayList';
$a1 = #{
Apples=3
Oranges=2
}
$b1 = #{
Apples=5
Oranges=7
}
$a2 = #{
Apples=6
Oranges=3
}
$b2 = #{
Apples=1
Oranges=5
}
$aObject1 = New-Object -TypeName PSObject -Property $a1;
$bObject1 = New-Object -TypeName PSObject -Property $b1;
$aObject2 = New-Object -TypeName PSObject -Property $a2;
$bObject2 = New-Object -TypeName PSObject -Property $b2;
<# Here we call the ArrayList Add method #>
$a.Add($aObject1); $a.Add($aObject2);
$b.Add($bObject1); $b.Add($aObject2);
Write-Host "Values of A";
$a | Format-List
Write-Host "Values of B";
$b | Format-List
Write-Host "Now lets make a trade`!";
Swap-Apples $a $b 0
Swap-Oranges $a $b 1
Write-Host "Values of A";
$a | Format-List
Write-Host "Values of B";
$b | Format-List
Write-Host "Hey, I brought more fruit for A`!";
Add-Fruit -object $a -howManyApples 5 -howManyOranges 2
Write-Host "Values of A";
$a | Format-List
Write-Host "I brought more fruit for B too`!";
Add-Fruit -object $b -howManyApples 5 -howManyOranges 3
Write-Host "Values of B";
$b | Format-List
And the (somewhat) intended output (see below):
0
1
0
1
Values of A
Oranges : 2
Apples : 3
Oranges : 3
Apples : 6
Values of B
Oranges : 7
Apples : 5
Oranges : 3
Apples : 6
Now lets make a trade!
Values of A
Oranges : 2
Apples : 5
Oranges : 3
Apples : 6
Values of B
Oranges : 7
Apples : 3
Oranges : 3
Apples : 6
Hey, I brought more fruit for A!
Values of A
Oranges : 2
Apples : 5
Oranges : 3
Apples : 6
Oranges : 2
Apples : 5
I brought more fruit for B too!
Values of B
Oranges : 7
Apples : 3
Oranges : 3
Apples : 6
Oranges : 3
Apples : 5
Extra output ?
If you observed in the output:
0
1
0
1
The Add Method of ArrayList returns the index of the record you Added. Since it returns this value it drops out of your pipe into Std-Out, so make sure you direct it accordingly.
If you don't need this output in your program like I don't here, pipe it to the null device, like so:
$a.Add($aObject1) | Out-Null; $a.Add($aObject2) | Out-Null;
$b.Add($bObject1) | Out-Null; $b.Add($aObject2) | Out-Null;
And here's the final program/output:
Code: (Arraytest.ps1)
Function Swap-Apples($objectA, $objectB, $indexToSwap) {
$temp = $objectA[$indexToSwap].Apples;
$objectA[$indexToSwap].Apples = $objectB[$indexToSwap].Apples;
$objectB[$indexToSwap].Apples = $temp;
}
Function Swap-Oranges($objectA, $objectB, $indexToSwap) {
$temp = $objectA[$indexToSwap].Oranges;
$objectA[$indexToSwap].Oranges = $objectB[$indexToSwap].Oranges;
$objectB[$indexToSwap].Oranges = $temp;
}
<# ArrayList! #>
Function Add-Fruit ([System.Collections.ArrayList]$object, [int]$howManyApples, [int]$howManyOranges) {
$hAdd = #{
Apples=$howManyApples
Oranges=$howManyOranges
}
$hToAdd = New-Object -TypeName PSObject -Property $hAdd;
<# We have to call the ArrayList Add method to add to our dynamic object #>
$object.Add($hToAdd) | Out-Null;
}
$a = New-Object -TypeName 'System.Collections.ArrayList';
$b = New-Object -TypeName 'System.Collections.ArrayList';
$a1 = #{
Apples=3
Oranges=2
}
$b1 = #{
Apples=5
Oranges=7
}
$a2 = #{
Apples=6
Oranges=3
}
$b2 = #{
Apples=1
Oranges=5
}
$aObject1 = New-Object -TypeName PSObject -Property $a1;
$bObject1 = New-Object -TypeName PSObject -Property $b1;
$aObject2 = New-Object -TypeName PSObject -Property $a2;
$bObject2 = New-Object -TypeName PSObject -Property $b2;
<# Here we call the ArrayList Add method #>
$a.Add($aObject1) | Out-Null; $a.Add($aObject2) | Out-Null;
$b.Add($bObject1) | Out-Null; $b.Add($aObject2) | Out-Null;
Write-Host "Values of A";
$a | Format-List
Write-Host "Values of B";
$b | Format-List
Write-Host "Now lets make a trade`!";
Swap-Apples $a $b 0
Swap-Oranges $a $b 1
Write-Host "Values of A";
$a | Format-List
Write-Host "Values of B";
$b | Format-List
Write-Host "Hey, I brought more fruit for A`!";
Add-Fruit -object $a -howManyApples 5 -howManyOranges 2
Write-Host "Values of A";
$a | Format-List
Write-Host "I brought more fruit for B too`!";
Add-Fruit -object $b -howManyApples 5 -howManyOranges 3
Write-Host "Values of B";
$b | Format-List
Output:
Values of A
Oranges : 2
Apples : 3
Oranges : 3
Apples : 6
Values of B
Oranges : 7
Apples : 5
Oranges : 3
Apples : 6
Now lets make a trade!
Values of A
Oranges : 2
Apples : 5
Oranges : 3
Apples : 6
Values of B
Oranges : 7
Apples : 3
Oranges : 3
Apples : 6
Hey, I brought more fruit for A!
Values of A
Oranges : 2
Apples : 5
Oranges : 3
Apples : 6
Oranges : 2
Apples : 5
I brought more fruit for B too!
Values of B
Oranges : 7
Apples : 3
Oranges : 3
Apples : 6
Oranges : 3
Apples : 5

It works to me:
Function Swap-Array ($theArrayA, $theArrayB, [int]$indexToSwap)
{
$temp = $theArrayA[$indexToSwap]
$theArrayA[$indexToSwap] = $theArrayB[$indexToSwap]
$theArrayB[$indexToSwap] = $temp
}
$a = #(1,2,3,4)
$b = #(3,2,4,1)
$a
$b
Swap-Array $a $b 2
$a
$b
Just remove the comas.

Related

Sorting version numbers in an array isn't accurate with two digit numbers

I am trying to sort version numbers descendingly, however versions like "W2018.1.10" are below versions like "W2018.1.2" despite 10 being bigger than 2.
$request = 'place from where I pull the version list'
$content = Invoke-RestMethod -Uri $request -Method GET
$versionarray = $content.versionlist[0].Versionen -split ", "
$sortedArray = $versionArray + "W2018.2.10.1" | sort -Descending
Write-Output $sortedArray
The current array looks like this:
W2019.2.8.7
W2019.2.8.6
W2019.2.8.5
W2019.2.8.3
W2018.2.10.1
It's supposed to look like this:
W2018.2.10.1
W2019.2.8.7
W2019.2.8.6
W2019.2.8.5
W2019.2.8.3
To illustrate several methods:
## Q:\Test\2019\04\04\SO_55519038.ps1
## $ToNatural from Roman Kuzmin source <https://stackoverflow.com/a/5429048/6811411>
$ToNatural = { [regex]::Replace($_, '\d+', { $args[0].Value.PadLeft(20,"0") }) }
$versionarray = ("W2018.2.1.7",
"W2019.2.8.7",
"W2019.2.9.7",
"W2019.2.10.7",
"W2019.2.1.7")
"`nCasting all digits to version type"
$versionarray | ForEach-Object { [version]($_ -replace '^W')} | Sort
"`nSorting with `$ToNatural`n"
$versionarray | Sort $ToNatural
"`nBuilding a [PSCustomObject] with last 3 dot seperated numbers cast to version"
$versionarray | ForEach-Object {
[PSCustomObject]#{
VersionString = $_
Version = [version]($_ -split '\.',2)[1]
}
} | Sort Version,VersionString | ft
Sample output:
> Q:\Test\2019\04\04\SO_55519038.ps1
Casting all digits to version type
Major Minor Build Revision
----- ----- ----- --------
2018 2 1 7
2019 2 1 7
2019 2 8 7
2019 2 9 7
2019 2 10 7
Sorting with $ToNatural
W2018.2.1.7
W2019.2.1.7
W2019.2.8.7
W2019.2.9.7
W2019.2.10.7
Building a [PSCustomObject] with last 3 dot seperated numbers cast to version
VersionString Version
------------- -------
W2018.2.1.7 2.1.7
W2019.2.1.7 2.1.7
W2019.2.8.7 2.8.7
W2019.2.9.7 2.9.7
W2019.2.10.7 2.10.7
Very dirty :)
$x2 = #("W2019.2.8.7","W2019.2.9.7","W2018.2.1.7","W2020.1.1.7")
$hash = #{}
foreach ($x in $x2)
{
$x -match "(w\d*\.)(\d*.\d*.\d*)"
$property = [ordered]#{
'fullname' = $matches[0]
'sort' = $matches[2]
}
$res = New-Object -TypeName psobject -Property $property
$hash.add($res.fullname,$res.sort)
}
$res2 = ($hash |Sort-Object -Property value).Keys
$res2

Expand property in hashtable

I'm having a simple issue with a script, where I want to run a GCI against a remote server, issue is, the value is combined with another hashtable property, so the GCI fails.
The script reads entries from a two-column .csv, the headers are "server" and "platform"
Here's what I've got:
$ShortDate = (Get-Date).ToString('MM/dd/yyyy')
$CheckServer = #{}
$serverObjects = #() # create a list of server objects
Import-Csv $Dir\Servers.csv | ForEach {
$CheckServer.Server = $_.Server
$CheckServer.Platform = $_.Platform
if (GCI \\$_.Server\c$\log\Completed_Summary_*.html -EA 0 | where {$.LastWriteTime -ge "$ShortDate"}) {
Write-Host "FOUND"
} # end of IF GCI
} # end of For-Each
$serverObjects += New-Object -TypeName PSObject -Property $CheckServer
The problem is that the entry for $_.Server should be SERVER1, SERVER2, SERVER3, etc, all the entries in the servers.csv, instead, the values for both $_.Server and $_.Platform are combined. Such as:
Write-Host "Checking" \\#{Server=SERVER1; Platform=PLATFORM_1}.Server\c$\log\Completed_Summary_*.html
it should show as follows:
Write-Host "Checking" \\SERVER1\log\Completed_Summary_*.html
How do I un-combine them so that the GCI command works?
PowerShell only does simple variable expansion inside strings. For more complex expressions like index operations or accessing object properties/methods it would insert the stringified value of the array or object variable and leave the rest of the operation untouched.
Demonstration:
PS C:\> $array = 23, 42
PS C:\> Write-Host "some $array[1] or other"
some 23 42[1] or other
PS C:\> $object = New-Object -Type PSObject -Property #{Foo=23; Bar=42}
PS C:\> Write-Host "some $object.Foo or other"
some #{Bar=42; Foo=23}.Foo or other
To avoid this you need to either:
assign the resulting value to a variable first and use that variable in the string:
$value = $array[5]
Write-Host "some $value or other"
$value = $object.Foo
Write-Host "some $value or other"
use a subexpression ($(...)):
Write-Host "some $($array[5]) or other"
Write-Host "some $($object.Foo) or other"
use the format operator (-f):
Write-Host "some {0} or other" -f $array[5]
Write-Host "some {0} or other" -f $object.Foo
modify like it
$ShortDate = Get-Date -Hour 0 -Minute 0 -Second 0
$CheckServer = #{}
$serverObjects = #() # create a list of server objects
$Dir="C:\temp"
Import-Csv $Dir\Servers.csv | ForEach {
$CheckServer.Server = $_.Server
$CheckServer.Platform = $_.Platform
if (GCI "\\$($_.Server)\c$\log\Completed_Summary_*.html" -EA 0 | where {$_.LastWriteTime -ge $ShortDate})
{
Write-Host "FOUND"
}
}

Move elements in an array of hashtables to another array in PowerShell

I'd like to move hashtables from one array to another.
Assuming that I have an array of hashtables:
PS> $a = #( #{s='a';e='b'}, #{s='b';e='c'}, #{s='b';e='d'} )
Name Value
---- -----
s a
e b
s b
e c
s b
e d
I can copy a selected set to another array:
PS> $b = $a | ? {$_.s -Eq 'b'}
Name Value
---- -----
s b
e c
s b
e d
Then remove b's items from a:
PS> $a = $a | ? {$b -NotContains $_}
Name Value
---- -----
s a
e b
Is there a more-succinct way of doing this?
PS 4.0 using Where method:
$b, $a = $a.Where({$_.s -Eq 'b'}, 'Split')
More info:
ForEach and Where magic methods
I would argue that doing two assignments with a filter and the inverted filter is the most straightforward way of doing this in PowerShell:
$b = $a | ? {$_.s -eq 'b'} # x == y
$a = $a | ? {$_.s -ne 'b'} # x != y, i.e. !(x == y)
You could wrap a function around this operation like this (using call by reference):
function Move-Elements {
Param(
[Parameter(Mandatory=$true)]
[ref][array]$Source,
[Parameter(Mandatory=$true)]
[AllowEmptyCollection()]
[ref][array]$Destination,
[Parameter(Mandatory=$true)]
[scriptblock]$Filter
)
$inverseFilter = [scriptblock]::Create("-not ($Filter)")
$Destination.Value = $Source.Value | Where-Object $Filter
$Source.Value = $Source.Value | Where-Object $inverseFilter
}
$b = #()
Move-Elements ([ref]$a) ([ref]$b) {$_.s -eq 'b'}
or like this (returning a list of arrays):
function Remove-Elements {
Param(
[Parameter(Mandatory=$true)]
[array]$Source,
[Parameter(Mandatory=$true)]
[scriptblock]$Filter
)
$inverseFilter = [scriptblock]::Create("-not ($Filter)")
$destination = $Source | Where-Object $Filter
$Source = $Source | Where-Object $inverseFilter
$Source, $destination
}
$a, $b = Remove-Elements $a {$_.s -eq 'b'}
or a combination of the above.

Comparing two arrays & get the values which are not common

i wanted a small logic to compare contents of two arrays & get the value which is not common amongst them using powershell
example if
$a1=#(1,2,3,4,5)
$b1=#(1,2,3,4,5,6)
$c which is the output should give me the value "6" which is the output of what's the uncommon value between both the arrays.
Can some one help me out with the same! thanks!
PS > $c = Compare-Object -ReferenceObject (1..5) -DifferenceObject (1..6) -PassThru
PS > $c
6
$a = 1..5
$b = 4..8
$Yellow = $a | Where {$b -NotContains $_}
$Yellow contains all the items in $a except the ones that are in $b:
PS C:\> $Yellow
1
2
3
$Blue = $b | Where {$a -NotContains $_}
$Blue contains all the items in $b except the ones that are in $a:
PS C:\> $Blue
6
7
8
$Green = $a | Where {$b -Contains $_}
Not in question, but anyways; Green contains the items that are in both $a and $b.
PS C:\> $Green
4
5
Note: Where is an alias of Where-Object. Alias can introduce possible problems and make scripts hard to maintain.
Addendum 12 October 2019
As commented by #xtreampb and #mklement0: although not shown from the example in the question, the task that the question implies (values "not in common") is the symmetric difference between the two input sets (the union of yellow and blue).
Union
The symmetric difference between the $a and $b can be literally defined as the union of $Yellow and $Blue:
$NotGreen = $Yellow + $Blue
Which is written out:
$NotGreen = ($a | Where {$b -NotContains $_}) + ($b | Where {$a -NotContains $_})
Performance
As you might notice, there are quite some (redundant) loops in this syntax: all items in list $a iterate (using Where) through items in list $b (using -NotContains) and visa versa. Unfortunately the redundancy is difficult to avoid as it is difficult to predict the result of each side. A Hash Table is usually a good solution to improve the performance of redundant loops. For this, I like to redefine the question: Get the values that appear once in the sum of the collections ($a + $b):
$Count = #{}
$a + $b | ForEach-Object {$Count[$_] += 1}
$Count.Keys | Where-Object {$Count[$_] -eq 1}
By using the ForEach statement instead of the ForEach-Object cmdlet and the Where method instead of the Where-Object you might increase the performance by a factor 2.5:
$Count = #{}
ForEach ($Item in $a + $b) {$Count[$Item] += 1}
$Count.Keys.Where({$Count[$_] -eq 1})
LINQ
But Language Integrated Query (LINQ) will easily beat any native PowerShell and native .Net methods (see also High Performance PowerShell with LINQ and mklement0's answer for Can the following Nested foreach loop be simplified in PowerShell?:
To use LINQ you need to explicitly define the array types:
[Int[]]$a = 1..5
[Int[]]$b = 4..8
And use the [Linq.Enumerable]:: operator:
$Yellow = [Int[]][Linq.Enumerable]::Except($a, $b)
$Blue = [Int[]][Linq.Enumerable]::Except($b, $a)
$Green = [Int[]][Linq.Enumerable]::Intersect($a, $b)
$NotGreen = [Int[]]([Linq.Enumerable]::Except($a, $b) + [Linq.Enumerable]::Except($b, $a))
SymmetricExceptWith
(Added 2022-05-02)
There is actually another way to get the symmetric difference which is using the SymmetricExceptWith method of the HashSet class, for a details see the specific answer from mklement0 on Find what is different in two very large lists:
$a = [System.Collections.Generic.HashSet[int]](1..5)
$b = [System.Collections.Generic.HashSet[int]](4..8)
$a.SymmetricExceptWith($b)
$NotGreen = $a # note that the result will be stored back in $a
Benchmark
(Updated 2022-05-02, thanks #Santiago for the improved benchmark script)
Benchmark results highly depend on the sizes of the collections and how many items there are actually shared. Besides there is a caveat with drawing conclussions on methods that use
lazy evaluation (also called deferred execution) as with LINQ and the SymmetricExceptWith where actually pulling the result (e.g. #($a)[0]) causes the expression to be evaluated and therefore might take longer than expected as nothing has been done yet other than defining what should be done. See also: Fastest Way to get a uniquely index item from the property of an array
Anyways, as an "average", I am presuming that half of each collection is shared with the other.
Test TotalMilliseconds
---- -----------------
Compare-Object 118.5942
Where-Object 275.6602
ForEach-Object 52.8875
foreach 25.7626
Linq 14.2044
SymmetricExce… 7.6329
To get a good performance comparison, caches should be cleared by e.g. starting a fresh PowerShell session.
[Int[]]$arrA = 1..1000
[Int[]]$arrB = 500..1500
Measure-Command {&{
$a = $arrA
$b = $arrB
Compare-Object -ReferenceObject $a -DifferenceObject $b -PassThru
}} |Select-Object #{N='Test';E={'Compare-Object'}}, TotalMilliseconds
Measure-Command {&{
$a = $arrA
$b = $arrB
($a | Where {$b -NotContains $_}), ($b | Where {$a -NotContains $_})
}} |Select-Object #{N='Test';E={'Where-Object'}}, TotalMilliseconds
Measure-Command {&{
$a = $arrA
$b = $arrB
$Count = #{}
$a + $b | ForEach-Object {$Count[$_] += 1}
$Count.Keys | Where-Object {$Count[$_] -eq 1}
}} |Select-Object #{N='Test';E={'ForEach-Object'}}, TotalMilliseconds
Measure-Command {&{
$a = $arrA
$b = $arrB
$Count = #{}
ForEach ($Item in $a + $b) {$Count[$Item] += 1}
$Count.Keys.Where({$Count[$_] -eq 1}) # => should be foreach($key in $Count.Keys) {if($Count[$key] -eq 1) { $key }} for fairness
}} |Select-Object #{N='Test';E={'foreach'}}, TotalMilliseconds
Measure-Command {&{
$a = $arrA
$b = $arrB
[Int[]]([Linq.Enumerable]::Except($a, $b) + [Linq.Enumerable]::Except($b, $a))
}} |Select-Object #{N='Test';E={'Linq'}}, TotalMilliseconds
Measure-Command {&{
$a = $arrA
$b = $arrB
($r = [System.Collections.Generic.HashSet[int]]::new($a)).SymmetricExceptWith($b)
}} |Select-Object #{N='Test';E={'SymmetricExceptWith'}}, TotalMilliseconds
Look at Compare-Object
Compare-Object $a1 $b1 | ForEach-Object { $_.InputObject }
Or if you would like to know where the object belongs to, then look at SideIndicator:
$a1=#(1,2,3,4,5,8)
$b1=#(1,2,3,4,5,6)
Compare-Object $a1 $b1
Try:
$a1=#(1,2,3,4,5)
$b1=#(1,2,3,4,5,6)
(Compare-Object $a1 $b1).InputObject
Or, you can use:
(Compare-Object $b1 $a1).InputObject
The order doesn't matter.
Your results will not be helpful unless the arrays are first sorted.
To sort an array, run it through Sort-Object.
$x = #(5,1,4,2,3)
$y = #(2,4,6,1,3,5)
Compare-Object -ReferenceObject ($x | Sort-Object) -DifferenceObject ($y | Sort-Object)
This should help, uses simple hash table.
$a1=#(1,2,3,4,5) $b1=#(1,2,3,4,5,6)
$hash= #{}
#storing elements of $a1 in hash
foreach ($i in $a1)
{$hash.Add($i, "present")}
#define blank array $c
$c = #()
#adding uncommon ones in second array to $c and removing common ones from hash
foreach($j in $b1)
{
if(!$hash.ContainsKey($j)){$c = $c+$j}
else {hash.Remove($j)}
}
#now hash is left with uncommon ones in first array, so add them to $c
foreach($k in $hash.keys)
{
$c = $c + $k
}

PowerShell array initialization

What's the best way to initialize an array in PowerShell?
For example, the code
$array = #()
for($i=0; $i -lt 5;$i++)
{
$array[$i] = $FALSE
}
generates the error
Array assignment failed because index '0' was out of range.
At H:\Software\PowerShell\TestArray.ps1:4 char:10
+ $array[$ <<<< i] = $FALSE
Here's two more ways, both very concise.
$arr1 = #(0) * 20
$arr2 = ,0 * 20
You can also rely on the default value of the constructor if you wish to create a typed array:
> $a = new-object bool[] 5
> $a
False
False
False
False
False
The default value of a bool is apparently false so this works in your case. Likewise if you create a typed int[] array, you'll get the default value of 0.
Another cool way that I use to initialze arrays is with the following shorthand:
> $a = ($false, $false, $false, $false, $false)
> $a
False
False
False
False
False
Or if you can you want to initialize a range, I've sometimes found this useful:
> $a = (1..5)
> $a
1
2
3
4
5
Hope this was somewhat helpful!
Yet another alternative:
for ($i = 0; $i -lt 5; $i++)
{
$arr += #($false)
}
This one works if $arr isn't defined yet.
NOTE - there are better (and more performant) ways to do this... see https://stackoverflow.com/a/234060/4570 below as an example.
The original example returns an error because the array is created empty, then you try to access the nth element to assign it a value.
The are a number of creative answers here, many I didn't know before reading this post. All are fine for a small array, but as n0rd points out, there are significant differences in performance.
Here I use Measure-Command to find out how long each initialization takes. As you might guess, any approach that uses an explicit PowerShell loop is slower than those that use .Net constructors or PowerShell operators (which would be compiled in IL or native code).
Summary
New-Object and #(somevalue)*n are fast (around 20k ticks for 100k elements).
Creating an array with the range operator n..m is 10x slower (200k ticks).
Using an ArrayList with the Add() method is 1000x slower than the baseline (20M ticks), as is looping through an already-sized array using for() or ForEach-Object (a.k.a. foreach,%).
Appending with += is the worst (2M ticks for just 1000 elements).
Overall, I'd say array*n is "best" because:
It's fast.
You can use any value, not just the default for the type.
You can create repeating values (to illustrate, type this at the powershell prompt: (1..10)*10 -join " " or ('one',2,3)*3)
Terse syntax.
The only drawback:
Non-obvious. If you haven't seen this construct before, it's not apparent what it does.
But keep in mind that for many cases where you would want to initialize the array elements to some value, then a strongly-typed array is exactly what you need. If you're initializing everything to $false, then is the array ever going to hold anything other than $false or $true? If not, then New-Object type[] n is the "best" approach.
Testing
Create and size a default array, then assign values:
PS> Measure-Command -Expression {$a = new-object object[] 100000} | Format-List -Property "Ticks"
Ticks : 20039
PS> Measure-Command -Expression {for($i=0; $i -lt $a.Length;$i++) {$a[$i] = $false}} | Format-List -Property "Ticks"
Ticks : 28866028
Creating an array of Boolean is bit little slower than and array of Object:
PS> Measure-Command -Expression {$a = New-Object bool[] 100000} | Format-List -Property "Ticks"
Ticks : 130968
It's not obvious what this does, the documentation for New-Object just says that the second parameter is an argument list which is passed to the .Net object constructor. In the case of arrays, the parameter evidently is the desired size.
Appending with +=
PS> $a=#()
PS> Measure-Command -Expression { for ($i=0; $i -lt 100000; $i++) {$a+=$false} } | Format-List -Property "Ticks"
I got tired of waiting for that to complete, so ctrl+c then:
PS> $a=#()
PS> Measure-Command -Expression { for ($i=0; $i -lt 100; $i++) {$a+=$false} } | Format-List -Property "Ticks"
Ticks : 147663
PS> $a=#()
PS> Measure-Command -Expression { for ($i=0; $i -lt 1000; $i++) {$a+=$false} } | Format-List -Property "Ticks"
Ticks : 2194398
Just as (6 * 3) is conceptually similar to (6 + 6 + 6), so ($somearray * 3) ought to give the same result as ($somearray + $somearray + $somearray). But with arrays, + is concatenation rather than addition.
If $array+=$element is slow, you might expect $array*$n to also be slow, but it's not:
PS> Measure-Command -Expression { $a = #($false) * 100000 } | Format-List -Property "Ticks"
Ticks : 20131
Just like Java has a StringBuilder class to avoid creating multiple objects when appending, so it seems PowerShell has an ArrayList.
PS> $al = New-Object System.Collections.ArrayList
PS> Measure-Command -Expression { for($i=0; $i -lt 1000; $i++) {$al.Add($false)} } | Format-List -Property "Ticks"
Ticks : 447133
PS> $al = New-Object System.Collections.ArrayList
PS> Measure-Command -Expression { for($i=0; $i -lt 10000; $i++) {$al.Add($false)} } | Format-List -Property "Ticks"
Ticks : 2097498
PS> $al = New-Object System.Collections.ArrayList
PS> Measure-Command -Expression { for($i=0; $i -lt 100000; $i++) {$al.Add($false)} } | Format-List -Property "Ticks"
Ticks : 19866894
Range operator, and Where-Object loop:
PS> Measure-Command -Expression { $a = 1..100000 } | Format-List -Property "Ticks"
Ticks : 239863
Measure-Command -Expression { $a | % {$false} } | Format-List -Property "Ticks"
Ticks : 102298091
Notes:
I nulled the variable between each run ($a=$null).
Testing was on a tablet with Atom processor; you would probably see faster speeds on other machines. [edit: About twice as fast on a desktop machine.]
There was a fair bit of variation when I tried multiple runs. Look for the orders of magnitude rather than exact numbers.
Testing was with PowerShell 3.0 in Windows 8.
Acknowledgements
Thanks to #halr9000 for array*n, #Scott Saad and Lee Desmond for New-Object, and #EBGreen for ArrayList.
Thanks to #n0rd for getting me to think about performance.
$array = 1..5 | foreach { $false }
Here's another idea. You have to remember, that it's .NET underneath:
$arr = [System.Array]::CreateInstance([System.Object], 5)
$arr.GetType()
$arr.Length
$arr = [Object[]]::new(5)
$arr.GetType()
$arr.Length
Result:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
5
True True Object[] System.Array
5
Using new() has one distinct advantage: when you're programming in ISE and want to create an object, ISE will give you hint with all paramer combinations and their types. You don't have that with New-Object, where you have to remember the types and order of arguments.
$array = #()
for($i=0; $i -lt 5; $i++)
{
$array += $i
}
The solution I found was to use the New-Object cmdlet to initialize an array of the proper size.
$array = new-object object[] 5
for($i=0; $i -lt $array.Length;$i++)
{
$array[$i] = $FALSE
}
If I don't know the size up front, I use an arraylist instead of an array.
$al = New-Object System.Collections.ArrayList
for($i=0; $i -lt 5; $i++)
{
$al.Add($i)
}
Here's another typical way:
$array = for($i = 0; $i -le 4; $i++) { $false }
Or try this an idea. Works with powershell 5.0+.
[bool[]]$tf=((,$False)*5)
$array = foreach($i in 1..5) { $false }

Resources