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

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.

Related

PowerShell: Intersection of more than two arrays

Using PowerShell, I have 14 arrays of strings. Some of the arrays are empty. How would I get the intersection (all elements that exist in all of the arrays) of these arrays (excluding the arrays that are empty)? I am trying to avoid comparing two arrays at a time.
Some of the arrays are empty, so I do not want to include those in my comparisons. Any ideas on how I would approach this? Thank you.
$a = #('hjiejnfnfsd','test','huiwe','test2')
$b = #('test','jnfijweofnew','test2')
$c = #('njwifqbfiwej','test','jnfijweofnew','test2')
$d = #('bhfeukefwgu','test','dasdwdv','test2','hfuweihfei')
$e = #('test','ddwadfedgnh','test2')
$f = #('test','test2')
$g = #('test','bjiewbnefw','test2')
$h = #('uie287278hfjf','test','huiwhiwe','test2')
$i = #()
$j = #()
$k = #('jireohngi','test','gu7y8732hbj','test2')
$l = #()
$m = #('test','test2')
$n = #('test','test2')
My attempt to solve this (although it does not check for empty arrays):
$overlap = Compare-Object $a $b -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $c -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $d -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $e -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $f -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $g -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $h -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $i -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $j -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $k -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $l -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $m -PassThru -IncludeEqual -ExcludeDifferent
$overlap = Compare-Object $overlap $n -PassThru -IncludeEqual -ExcludeDifferent
My desired result is that test and test2 appear in $overlap. This solution does not work because it does not check if the array it is comparing is empty.
Note: The following assumes that no individual array contains the same string more than once (more work would be needed to address that).
$a = #('hjiejnfnfsd','test','huiwe','test2')
$b = #('test','jnfijweofnew','test2')
$c = #('njwifqbfiwej','test','jnfijweofnew','test2')
$d = #('bhfeukefwgu','test','dasdwdv','test2','hfuweihfei')
$e = #('test','ddwadfedgnh','test2')
$f = #('test','test2')
$g = #('test','bjiewbnefw','test2')
$h = #('uie287278hfjf','test','huiwhiwe','test2')
$i = #()
$j = #()
$k = #('jireohngi','test','gu7y8732hbj','test2')
$l = #()
$m = #('test','test2')
$n = #('test','test2')
$allArrays = $a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l, $m, $n
# Initialize a hashtable in which we'll keep
# track of unique strings and how often they occur.
$ht = #{}
# Loop over all arrays.
$nonEmptyArrayCount = 0
foreach ($arr in $allArrays) {
# Loop over each non-empty array's elements.
if ($arr.Count -gt 0) {
++$nonEmptyArrayCount
foreach ($el in $arr) {
# Add each string and increment its occurrence count.
$ht[$el] += 1
}
}
}
# Output all strings that occurred in every non-empty array
$ht.GetEnumerator() |
Where-Object Value -eq $nonEmptyArrayCount |
ForEach-Object Key
The above outputs those strings that are present in all of the non-empty input arrays:
test2
test
You're close. Excluding empty arrays from comparison is essential because the intersection of an empty array and any other array is an empty array, and once $overlap contains an empty array that will be the final result regardless of what subsequent arrays contain.
Here's your code with the non-empty check and rewritten using loops...
$a = #('hjiejnfnfsd', 'test', 'huiwe', 'test2')
$b = #('test', 'jnfijweofnew', 'test2')
$c = #('njwifqbfiwej', 'test', 'jnfijweofnew', 'test2')
$d = #('bhfeukefwgu', 'test', 'dasdwdv', 'test2', 'hfuweihfei')
$e = #('test', 'ddwadfedgnh', 'test2')
$f = #('test', 'test2')
$g = #('test', 'bjiewbnefw', 'test2')
$h = #('uie287278hfjf', 'test', 'huiwhiwe', 'test2')
$i = #()
$j = #()
$k = #('jireohngi', 'test', 'gu7y8732hbj', 'test2')
$l = #()
$m = #('test', 'test2')
$n = #('test', 'test2')
# Create an array of arrays $a through $n
$arrays = #(
# 'a'..'n' doesn't work in Windows PowerShell
# Define both ends of the range...
# 'a' → [String]
# 'a'[0] → [Char]
# [Int32] 'a'[0] → 97 (ASCII a)
# ...and cast each element back to a [Char]
[Char[]] ([Int32] 'a'[0]..[Int32] 'n'[0]) |
Get-Variable -ValueOnly
)
# Initialize $overlap to the first non-empty array
for ($initialOverlapIndex = 0; $initialOverlapIndex -lt $arrays.Length; $initialOverlapIndex++)
{
if ($arrays[$initialOverlapIndex].Length -gt 0)
{
break;
}
}
<#
Alternative:
$initialOverlapIndex = [Array]::FindIndex(
$arrays,
[Predicate[Array]] { param($array) $array.Length -gt 0 }
)
#>
$overlap = $arrays[$initialOverlapIndex]
for ($comparisonIndex = $initialOverlapIndex + 1; $comparisonIndex -lt $arrays.Length; $comparisonIndex++)
# Alternative: foreach ($array in $arrays | Select-Object -Skip $initialOverlapIndex)
{
$array = $arrays[$comparisonIndex]
if ($array.Length -gt 0)
{
$overlap = Compare-Object $overlap $array -PassThru -IncludeEqual -ExcludeDifferent
}
}
$overlap
...which outputs...
test
test2
Here is a solution using a Hashset. A Hashset is a collection that stores only unique items and provides fast lookup, which makes it a good choice for intersection calculation. It even has a method IntersectWith which accepts any enumerable type (such as an array) as argument. The method modifies the original Hashset so that it contains only the elements which are contained in both the Hashset and the argument passed to the method.
# Test input
$a = #() # I changed this to empty array for demonstration purposes
$b = #('test','jnfijweofnew','test2')
$c = #('njwifqbfiwej','test','jnfijweofnew','test2')
$d = #('bhfeukefwgu','test','dasdwdv','test2','hfuweihfei')
$e = #('test','ddwadfedgnh','test2')
$f = #('test','test2')
$g = #('test','bjiewbnefw','test2')
$h = #('uie287278hfjf','test','huiwhiwe','test2')
$i = #()
$j = #()
$k = #('jireohngi','test','gu7y8732hbj','test2')
$l = #()
$m = #('test','test2')
$n = #('test','test2')
# Create a variable with a type-constraint
[Collections.Generic.Hashset[object]] $overlap = $null
# For each of the arrays...
($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l, $m, $n).
Where{ $_.Count -gt 0 }. #... except the empty ones
ForEach{
# If the Hashset has not been initialized yet
if( $null -eq $overlap ) {
# Create the initial hashset from the first non-empty array.
$overlap = $_
}
else {
# Hashset is already initialized, calculate the intersection with next non-empty array.
$overlap.IntersectWith( $_ )
}
}
$overlap # Output
Output:
test
test2
Remarks:
To filter out empty arrays (or in general any kind of collection), we check its Count member, which gives the number of elements.
.Foreach and .Where are PowerShell intrinsic methods. These can be faster than the ForEach-Object and Where-Object commands, especially when working directly with collections (as opposed to output of another command). The automatic variable $_ represents the current element of the collection, as usual.
This code using pipeline commands is functionally the same:
[Collections.Generic.Hashset[object]] $overlap = $null
$a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l, $m, $n |
Where-Object Count -gt 0 |
ForEach-Object{
if( $null -eq $overlap ) {
$overlap = $_
}
else {
$overlap.IntersectWith( $_ )
}
}
In if( $null -eq $overlap ) it is very important that $null is on the left-hand-side of the -eq operator. If it were on the right-hand-side, PowerShell would do an element-wise comparison with $null to filter elements instead of checking if the variable $overlap itself is $null (see About Comparison Operators)
In the line $overlap = $_ PowerShell automatically converts the current array into a Hashset, because we have set a type constraint using [Collections.Generic.Hashset[object]] $overlap before and array is convertible to Hashset (see About Variables).
String comparison of a Hashset is case-sensitive by default. To make it case-insensitive, convert each string to lowercase like this:
[Collections.Generic.Hashset[object]] $overlap = $null
($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l, $m, $n).
Where{ $_.Count -gt 0 }.
ForEach{
if( $null -eq $overlap ) {
$overlap = $_.ToLower()
}
else {
$overlap.IntersectWith( $_.ToLower() )
}
}
This uses member access enumeration to call the String.ToLower() method for each element of the input arrays.
With the first variant, inserting a linebreak before Where and ForEach is not really necessary, but improves code readability (note that you can't insert a linebreak before .Where and .ForEach, because this confuses the PowerShell parser).

Array of variables in PowerShell has null members

I have a PowerShell script, where I want to make sure certain variables have value before proceeding.
So I have the following:
$dataRow = $sheet.Cells.Find($country).Row
$serverCol = $sheet.Cells.Find($serverString).Column
$databaseCol = $sheet.Cells.Find($databaseString).Column
$userCol = $sheet.Cells.Find($userString).Column
$passwordCol = $sheet.Cells.Find($passString).Column
$partnerCol = $sheet.Cells.Find($partnerString).Column
#All variables in this array are required. If one is empty - the script cannot continue
$requiredVars = #($dataRow, $serverCol, $databaseCol, $userCol, $passwordCol, $partnerCol)
But when I foreach over the array like so:
foreach ($var in $requiredVars)
{
Write-Host DataRow = ($dataRow -eq $var)
Write-Host ServerCol = ($serverCol -eq $var)
Write-Host DatabaseCol = ($databaseCol -eq $var)
Write-Host UserCol = ($userCol -eq $var)
Write-Host PasswordCol = ($passwordCol -eq $var)
Write-Host PartnerCol = ($partnerCol -eq $var)
if ($var -eq $null)
{
[System.Windows.Forms.MessageBox]::Show("No data found for given string!")
$excel.Quit()
return
}
}
I always get the MessageBox. I added the "Write-Host" part to see the value of each variable, then changed it to see which variable was null but all variables have values in them and all the checks you see here return "False".
I'd like to know what I'm doing wrong and if the $requiredVars array only copies values, not references or something.
Instead of using separate variables, you may consider using a Hashtable to store them all.
This makes checking the individual items a lot simpler:
# get the data from Excel and store everything in a Hashtable
# to use any of the items, use syntax like $excelData.passwordCol or $excelData['passwordCol']
$excelData = #{
'dataRow' = $sheet.Cells.Find($country).Row
'serverCol' = $sheet.Cells.Find($serverString).Column
'databaseCol' = $sheet.Cells.Find($databaseString).Column
'userCol' = $sheet.Cells.Find($userString).Column
'passwordCol' = $sheet.Cells.Find($passString).Column
'partnerCol' = $sheet.Cells.Find($partnerString).Column
}
# check all items in the hash. If any item is $null then exit
foreach ($item in $excelData.Keys) {
# or use: if ($null -eq $excelData[$item])
if (-not $excelData[$item]) {
[System.Windows.Forms.MessageBox]::Show("No data found for item $item!")
$excel.Quit()
# IMPORTANT: clean-up used COM objects from memory when done with them
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($sheet) | Out-Null
# Your code doesn't show this, but you'll have a $workbook object in there too
# [System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
return
}
}
One way to directly solve your question is this:
$a = "foo"
$b = "bar"
$c = $null
$requiredVariables = $a, $b, $c
# How many total entries in array?
($requiredVariables).Count
# How many of them have a value?
($requiredVariables | Where-Object {$_}).Count
# So one option for a single check would be:
if (($requiredVariables.Count) -ne ($requiredVariables | Where-Object {$_}).Count) {
Write-Warning "Not all values provided"
}
However an alternative [and better] approach is to make your code in to a function that includes parameter validation
function YourCustomFunction {
Param (
[ValidateNotNullOrEmpty()]
$a
,
[ValidateNotNullOrEmpty()]
$b
,
[ValidateNotNullOrEmpty()]
$c
)
Process {
Write-Output "Your function code goes here..."
}
}
# Call your function with the params
YourCustomFunction -a $a -b $b -c $c
Example output:
Test-YourCustomFunction: Cannot validate argument on parameter 'c'. The argument is null or empty. Provide an argument that is not null or empty, and
then try the command again.
At line:39 char:48

Passing Powershell Arrays by-reference not working as intended

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.

Find closest numerical value in Powershell arrays

Q: I'm looking for a more elegant way to get the closest match of a numerical from an array.
I may have over-complicated things here
Input
## A given array A where to search in
$a = (16/10),(16/9),(4/3),(5/4),(21/10),(21/9)
## A given value B which should be searched for in array A (closest match)
$b = 16/11
Desired output
"Closest value to 16/11 is 4/3"
My current code to solve the problem
## New array C for the differences of array A - B
$c = $a | %{ [math]::abs($_ - $b) }
## Measure array C to get lowest value D
$d = $c | measure -Minimum
## Get position E of value D in array C
$e = [array]::IndexOf($c, $d.minimum)
## Position E correlates to array A
echo "Closest value to $b is $($a[$e])
Remarks
It don't has to be an array if a hash table or something else suits better
My current code outputs decimals like 1.33333 instead of fractions 4/3. It would be nice to output the fraction
Short code is always better
$a = (16/10),(16/9),(4/3),(5/4),(21/10),(21/9)
$b = 16/11
$oldval = $b - $a[0]
$Final = $a[0]
if($oldval -lt 0){$oldval = $oldval * -1}
$a | %{$val = $b - $_
if($val -lt 0 ){$val = $val * -1}
if ($val -lt $oldval){
$oldval = $val
$Final = $_} }
Write-host "$Final is the closest to $b"
$diff = $b - $a[0]
$min_index = 0
for ($i = 1; $i -lt $a.count; $i++)
{
$new_diff = $b - $a[$i]
if ($new_diff -lt $diff)
{
$diff = $new_diff
$min_index = $i
}
}
Write-Output "Closest value to $b is $($a[$min_index])"
Powershell dose not support fraction values...
## A given array A where to search in
$a = '16/10','16/9','4/3','5/4','21/10','21/9'
## A given value B which should be searched for in array A (closest match)
$b = '16/11'
$numericArray = ($a | % { Invoke-Expression $_ })
$test = Invoke-Expression $b
$best = 0
$diff = [double]::MaxValue
for ($i = 1; $i -lt $numericArray.count; $i++) {
$newdiff = [math]::abs($numericArray[$i] - $test)
if ($newdiff -lt $diff) {
$diff = $newdiff
$best = $i
}
}
"Closest value to $b is $($a[$best])"
The big difference here is that the inputs are strings and not numbers, so we can preserve the fractions.
Security note: Don't use this if the inputs are from a user, as passing user-generated strings to Invoke-Expression is obviously a recipe for trouble.
## A given array A where to search in
$a = (16/10),(16/9),(4/3),(5/4),(21/10),(21/9)
## A given value B which should be searched for in array A (closest match)
$b = 16/11
<#Create a new-Object , we'll use this to store the results in the Foreach Loop#>
$values = [Pscustomobject] #{
'result' = #()
'a-values' = #()
}
foreach($aa in $a)
{ #round to 2 decimal places and then subtract
#store the result at the 'a-value' used to create that result
$values.result += [MATH]::Abs( [Math]::Round($B,2)`
-[Math]::Round($aa,2)`
)
$values."a-values" += $aa
}
# sort ascending and then this gets me the number closest to Zero
$lookFor = $($values.result | Sort-Object )[0]
#the result of the Number closest to $b = 16/11
$endresult = $values.'a-values'[ $values.result.indexof($lookfor) ]
Write-host "the number closest to '$b' is '$endresult'"
Remove-Variable values
Here's a oneliner sorting with distance function:
($a | Sort-Object { [Math]::abs($_ - $b) })[0]
Returning a fraction is not possible in this case because e.g. 16/10 is immediately converted to a decimal.

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
}

Resources