Find closest numerical value in Powershell arrays - 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.

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).

Powershell use values of one array as headings for another array

I have two arrays in Powershell:
$headings = ['h1', 'h2']
$values = [3, 4]
It is guaranteed that both arrays have the same length. How can I create an array where the values of $headings become the headings of the $values array?
I want to be able to do something like this:
$result['h2'] #should return 4
Update:
The arrays $headings and $values are of type System.Array.
As stated above you'll need a PowerShell hashtable. By the way array in PowerShell are defined via #(), see about_arrays for further information.
$headings = #('h1', 'h2')
$values = #(3, 4)
$combined = #{ }
if ($headings.Count -eq $values.Count) {
for ($i = 0; $i -lt $headings.Count; $i++) {
$combined[$headings[$i]] = $values[$i]
}
}
else {
Write-Error "Lengths of arrays are not the same"
}
$combined
Dumping the content of combined returns:
$combined
Name Value
---- -----
h2 4
h1 3
Try something like this :
$hash = [ordered]#{ h1 = 3; h2 = 4}
$hash["h1"] # -- Will give you 3
## Next Approach
$headings = #('h1', 'h2') #array
$values = #(3, 4) #array
If($headings.Length -match $values.Length)
{
For($i=0;$i -lt $headings.Length; $i++)
{
#Formulate your Hashtable here like the above and then you would be able to pick it up
#something like this in hashtable variable $headings[$i] = $values[$i]
}
}
PS: I am just giving you the logical headstart and helping you with the hashtable part. Code is upto you.

how to create bunch of arrays inside a loop in powershell

I'm writing a script to fetch values for a set of components and storing the fetched values in an array. As there are many components I created a loop and tried to create an array name along with its iteration value (fi1, fi2, fi3, etc.) like this.
Here is the code:
function fiswitchinfo {
Param ($sheet, [string]$text, $iter)
$($fi+($iter)) = #()
$max = $sheet.UsedRange.Rows.Count
for ($i=1; $i -lt 900 ; $i++) {
$rows = $sheet.Cells.Item($i, 2).EntireRow
$cell_info = $sheet.Cells.Item($i, 2)
$cell = $cell_info.Address($false, $false)
if ($rows.hidden -eq $false) {
$cell_info = $sheet.Cells.Item($i, 2).Text
if ($cell_info -ne "" -and $cell_info.Contains($text) -eq "True") {
$cell = $cell -split "(?<=[A-Z])(?=\d)"
[int]$curline = $cell[1]
$component = $sheet.Cells.Item($curline, 2).Text
$compip = $sheet.Cells.Item($curline, 3).Text
$row = $sheet.Cells.Item($curline, 2).EntireRow
$cellinfo = $sheet.Cells.Item($curline, 2).text
if ($row.Hidden -ne "True" -and $cellinfo -ne $null) {
Write-Host $component $compip
$script:fi+$iter += $compip
}
}
}
}
}
fiswitchinfo $worksheet_3 "Fabric Interconnect 01 Cluster IP" 1
fiswitchinfo $worksheet_3 "Fabric Interconnect 01 A" 1
fiswitchinfo $worksheet_3 "Fabric Interconnect 01 B" 1
I'm not quite sure what you expect $($fi+($iter)) or $script:fi+$iter to do, but I'm pretty certain they won't do whatever it is you expect.
To have a function create an array of arrays in a loop and then return it you'd do something like this:
function fiswitchinfo {
...
$arr = #()
for ($i=1; $i -lt 900 ; $i++) {
...
$arr += ,$compip
}
return ,$arr
}
$fi1 = fiswitchinfo ...
$fi2 = fiswitchinfo ...
...
The leading comma in the statements $arr += ,$compip and return ,$arr is the unary array construction operator, which prevents PowerShell from unrolling the arrays. $arr += ,$compip appends $compip to $arr as a nested array (thus making $arr a jagged array) instead of appending the elements of $compip to $arr. return ,$arr ensures that $arr is returned to the caller as-is (thus preserving the array even if it's empty).

Array to read different values

How can I input multiple values in PowerShell and have those values stored in a variable?
For example:
$value = Read-Host "Enter the value's" #I need 15 values to be entered.
And then recall them e.g:
$value[0] = 1233
$value[1] = 2345
To do this you could declare an array #() and then use a loop and the addition operator to add items to the array (stopping when a blank value is submitted):
$values = #()
Do{
$value = read-host "Enter a value"
if ($value) {$values += $value}
}Until (-not $value)
You can then retrieve values as you described via the index with square brackets []:
$values #returns all values
$values[3] #returns the fourth value (if you entered four or more)
Beware that arrays start from 0, so the first item is [0], second is [1] etc. With PowerShell you can also use negative numbers to work through the array backwards, so [-1] is the last item, [-2] the second to last, etc.
Stores the readin values in an array:
$values = #()
$i = $null
while ($i -ne "q") {
if ($i -ne $null) {
# Attach value to array
$values += $i
}
$i = Read-Host "Enter value (stop with q)"
}
# Print each value in a seperate line
$values | % { Write-Host $_}
# Print type -> to visualize that it is an array
$values.GetType()
# Several values can be retrieved via index operator
$values[0]

How to fill an array efficiently in Powershell

I want to fill up a dynamic array with the same integer value as fast as possible using Powershell.
The Measure-Command shows that it takes 7 seconds on my system to fill it up.
My current code (snipped) looks like:
$myArray = #()
$length = 16385
for ($i=1;$i -le $length; $i++) {$myArray += 2}
(Full code can be seen on gist.github.com or on superuser)
Consider that $length can change. But for better understanding I chose a fixed length.
Q: How do I speed up this Powershell code?
You can repeat arrays, just as you can do with strings:
$myArray = ,2 * $length
This means »Take the array with the single element 2 and repeat it $length times, yielding a new array.«.
Note that you cannot really use this to create multidimensional arrays because the following:
$some2darray = ,(,2 * 1000) * 1000
will just create 1000 references to the inner array, making them useless for manipulation. In that case you can use a hybrid strategy. I have used
$some2darray = 1..1000 | ForEach-Object { ,(,2 * 1000) }
in the past, but below performance measurements suggest that
$some2darray = foreach ($i in 1..1000) { ,(,2 * 1000) }
would be a much faster way.
Some performance measurements:
Command Average Time (ms)
------- -----------------
$a = ,2 * $length 0,135902 # my own
[int[]]$a = [System.Linq.Enumerable]::Repeat(2, $length) 7,15362 # JPBlanc
$a = foreach ($i in 1..$length) { 2 } 14,54417
[int[]]$a = -split "2 " * $length 24,867394
$a = for ($i = 0; $i -lt $length; $i++) { 2 } 45,771122 # Ansgar
$a = 1..$length | %{ 2 } 431,70304 # JPBlanc
$a = #(); for ($i = 0; $i -lt $length; $i++) { $a += 2 } 10425,79214 # original code
Taken by running each variant 50 times through Measure-Command, each with the same value for $length, and averaging the results.
Position 3 and 4 are a bit of a surprise, actually. Apparently it's much better to foreach over a range instead of using a normal for loop.
Code to generate above chart:
$length = 16384
$tests = '$a = ,2 * $length',
'[int[]]$a = [System.Linq.Enumerable]::Repeat(2, $length)',
'$a = for ($i = 0; $i -lt $length; $i++) { 2 }',
'$a = foreach ($i in 1..$length) { 2 }',
'$a = 1..$length | %{ 2 }',
'$a = #(); for ($i = 0; $i -lt $length; $i++) { $a += 2 }',
'[int[]]$a = -split "2 " * $length'
$tests | ForEach-Object {
$cmd = $_
$timings = 1..50 | ForEach-Object {
Remove-Variable i,a -ErrorAction Ignore
[GC]::Collect()
Measure-Command { Invoke-Expression $cmd }
}
[pscustomobject]#{
Command = $cmd
'Average Time (ms)' = ($timings | Measure-Object -Average TotalMilliseconds).Average
}
} | Sort-Object Ave* | Format-Table -AutoSize -Wrap
Avoid appending to an array in a loop. It's copying the existing array to a new array with each iteration. Do this instead:
$MyArray = for ($i=1; $i -le $length; $i++) { 2 }
Using PowerShell 3.0 you can use (need .NET Framework 3.5 or upper):
[int[]]$MyArray = ([System.Linq.Enumerable]::Repeat(2, 65000))
Using PowerShell 2.0
$AnArray = 1..65000 | % {2}
It is not clear what you are trying. I tried looking at your code. But, $myArray +=2 means you are just adding 2 as the element. For example, here is the output from my test code:
$myArray = #()
$length = 4
for ($i=1;$i -le $length; $i++) {
Write-Host $myArray
$myArray += 2
}
2
2 2
2 2 2
Why do you need to add 2 as the array element so many times?
If all you want is just fill the same value, try this:
$myArray = 1..$length | % { 2 }
If you need it really fast, then go with ArrayLists and Tuples:
$myArray = New-Object 'Collections.ArrayList'
$myArray = foreach($i in 1..$length) {
[tuple]::create(2)
}
and if you need to sort it later then use this (normally a bit slower):
$myArray = New-Object 'Collections.ArrayList'
foreach($i in 1..$length) {
$myArray.add(
[tuple]::create(2)
)
}
both versions are in the 20ms range for me ;-)

Resources