Comparing values between arrays is very slow - arrays

I am a novice Powershell user and would like help with the following:
I am comparing the values in one one array with that of another. If they match, I write the value to a cell, if there is no match, the cell is highlighted red. However, with only two small arrays (each ~200 values) the search takes hours. There must be better way, please help.
$ArrFinal = $arrA + $arrB + $arrC + $arrD
$ArrFinal = $ArrFinal | select -uniq | sort-object
for ($k=1; $k -lt $ArrFinal.length; $k++)
{
for ($j=1; $j -lt $arrA.length; $j++)
{
if($ArrFinal[$k] -like $arrA[$j])
{
$cells.item($k+1,2)=$arrA[$j]
$cells.item($k+1,2).Interior.ColorIndex = 2
break
}
else
{
$cells.item($k+1,2).Interior.ColorIndex = 3
}
}
}

Assuming you're talking about Excel here: don't color each cell separately. Set ColorIndex to 3 once for the entire range and only change a cell's color when you actually change its value. Better yet, use a conditional format that will color empty cells differently from non-empty cells.
Also I'd drop the inner loop. You want to check if the 2nd array contains the value from the 1st one, so you can just use the -contains operator and write the value from the 1st array to the cell ($ArrFinal[$k] and $arrA[$j] are equal after all).
$ArrFinal = $arrA + $arrB + $arrC + $arrD | select -uniq | sort-object
for ($k=1; $k -lt $ArrFinal.length; $k++) {
if ($arrA -contains $ArrFinal[$k]) {
$cells.Item($k+1, 2) = $ArrFinal[$k]
$cells.Item($k+1, 2).Interior.ColorIndex = 2
}
}

Related

String comparison in PowerShell doesn't seem to work

I am trying to compare strings from one array to every string from the other. The current method works, as I have tested it with simpler variables and it retrieves them correctly. However, the strings that I need it to compare don't seem to be affected by the comparison.
The method is the following:
$counter = 0
for ($num1 = 0; $num1 -le $serverid_oc_arr.Length; $num1++) {
for ($num2 = 0; $num2 -le $moss_serverid_arr.Length; $num2++) {
if ($serverid_oc_arr[$num1] -eq $moss_serverid_arr[$num2]) {
break
}
else {
$counter += 1
}
if ($counter -eq $moss_serverid_arr.Length) {
$unmatching_serverids += $serverid_oc_arr[$num1]
$counter = 0
break
}
}
}
For each string in the first array it is iterating between all strings in the second and comparing them. If it locates equality, it breaks and goes to the next string in the first array. If it doesn't, for each inequality it is adding to the counter and whenever the counter hits the length of the second array (meaning no equality has been located in the second array) it adds the corresponding string to a third array that is supposed to retrieve all strings that don't match to anything in the second array in the end. Then the counter is set to 0 again and it breaks so that it can go to the next string from the first array.
This is all okay in theory and also works in practice with simpler strings, however the strings that I need it to work with are server IDs and look like this:
289101b4-3e6c-4495-9c67-f317589ba92c
Hence, the script seems to completely ignore the comparison and just puts all the strings from the first array into the third one and retrieves them at the end (sometimes also some random strings from both first and second array).
Another method I tried with similar results was:
$unmatching_serverids = $serverid_oc_arr | Where {$moss_serverid_arr -NotContains $_}
Can anyone spot any mistake that I may be making anywhere?
The issue with your code is mainly the use of -le instead of -lt, collection index starts at 0 and the collection's Length or Count property starts at 1. This was causing $null values being added to your result collection.
In addition to the above, $counter was never getting reset if the following condition was not met:
if ($counter -eq $moss_serverid_arr.Length) { ... }
You would need to a add a $counter = 0 outside inner for loop to prevent this.
Below you can find the same, a little bit improved, algorithm in addition to a test case that proves it's working.
$ran = [random]::new()
$ref = [System.Collections.Generic.HashSet[int]]::new()
# generate a 100 GUID collection
$arr1 = 1..100 | ForEach-Object { [guid]::NewGuid() }
# pick 90 unique GUIDs from arr1
$arr2 = 1..90 | ForEach-Object {
do {
$i = $ran.Next($arr1.Count)
} until($ref.Add($i))
$arr1[$i]
}
$result = foreach($i in $arr1) {
$found = foreach($z in $arr2) {
if ($i -eq $z) {
$true; break
}
}
if(-not $found) { $i }
}
$result.Count -eq 10 # => Must be `$true`
As side, the above could be reduced to this using the .ExceptWith(..) method from HashSet<T> Class:
$hash = [System.Collections.Generic.HashSet[guid]]::new([guid[]]$arr1)
$hash.ExceptWith([guid[]]$arr2)
$hash.Count -eq 10 # => `$true`
The working answer that I found for this is the below:
$unmatching_serverids = #()
foreach ($item in $serverid_oc_arr)
{
if ($moss_serverid_arr -NotContains $item)
{
$unmatching_serverids += $item
}
}
No obvious differences can be seen between it and the other methods (especially for the for-loop, this is just a simplified variant), but somehow this works correctly.

In PowerShell is there a way to return the index of an array with substring of a string

Is there another way return the indices of every instance of a substring in an array besides old fashioned looping through indices?
$myarray = #('herp','dederp','dedoo','flerp')
$substring = 'erp'
$indices = #()
for ($i=0; $i -lt $myarray.length; $i++) {
if ($myarray[$i] -match $substring){
$indices = $indices + $i
}
}
$thisiswrong = #($myarray.IndexOf($substring))
The conditional inside that kind of for loop is kinda cumbersome, and $thisiswrong only ever gets a value of [-1]
You can use LINQ (adapted from this C# answer):
$myarray = 'herp', 'dederp', 'dedoo', 'flerp'
$substring = 'erp'
[int[]] $indices = [Linq.Enumerable]::Range(0, $myarray.Count).
Where({ param($i) $myarray[$i] -match $substring })
$indices receives 0, 1, 3.
As for what you tried:
$thisiswrong = #($myarray.IndexOf($substring))
System.Array.IndexOf only ever finds one index and matches entire elements, literally and case-sensitively in the case of strings.
There's a more PowerShell-like, but much slower alternative, as hinted at by js2010's answer; you can take advantage of the fact that the match-information objects that Select-String outputs have a .LineNumber property, reflecting the 1-based index of the position in the input collection - even if the input doesn't come from a file:
$myarray = 'herp', 'dederp', 'dedoo', 'flerp'
$substring = 'erp'
[int[]] $indices =
($myarray | Select-String $substring).ForEach({ $_.LineNumber - 1 })
Note the need to subtract 1 from each .LineNumber value to get the 0-based array indices, and the use of the .ForEach() array method, which performs better than the ForEach-Object cmdlet.
If it was a file...
get-content file | select-string erp | select line, linenumber
Line LineNumber
---- ----------
herp 1
dederp 2
flerp 4

Query PSCustomObject Array for row with largest value

I'm trying to find the row with an attribute that is larger than the other row's attributes. Example:
$Array
Name Value
---- ----
test1 105
test2 101
test3 512 <--- Selects this row as it is the largest value
Here is my attempt to '1 line' this but It doesn't work.
$Array | % { If($_.value -gt $Array[0..($Array.Count)].value){write-host "$_.name is the largest row"}}
Currently it outputs nothing.
Desired Output:
"test1 is the largest row"
I'm having trouble visualizing how to do this efficiently with out some serious spaghetti code.
You could take advantage of Sort-Object to rank them by the property "Value" like this
$array = #(
[PSCustomObject]#{Name='test1';Value=105}
[PSCustomObject]#{Name='test2';Value=101}
[PSCustomObject]#{Name='test3';Value=512}
)
$array | Sort-Object -Property value -Descending | Select-Object -First 1
Output
Name Value
---- -----
test3 512
To incorporate your write host you can just run the one you select through a foreach.
$array | Sort-Object -Property value -Descending |
Select-Object -First 1 | Foreach-Object {Write-host $_.name,"has the highest value"}
test3 has the highest value
Or capture to a variable
$Largest = $array | Sort-Object -Property value -Descending | Select-Object -First 1
Write-host $Largest.name,"has the highest value"
test3 has the highest value
PowerShell has many built in features to make tasks like this easier.
If this is really an array of PSCustomObjects you can do something like:
$Array =
#(
[PSCustomObject]#{ Name = 'test1'; Value = 105 }
[PSCustomObject]#{ Name = 'test2'; Value = 101 }
[PSCustomObject]#{ Name = 'test3'; Value = 512 }
)
$Largest = ($Array | Sort-Object Value)[-1].Name
Write-host $Largest,"has the highest value"
This will sort your array according to the Value property. Then reference the last element using the [-1] syntax, then return the name property of that object.
Or if you're a purist you can assign the variable like:
$Largest = $Array | Sort-Object Value | Select-Object -Last 1 -ExpandProperty Name
If you want the whole object just remove .Name & -ExpandProperty Name respectively.
Update:
As noted PowerShell has some great tools to help with common tasks like sorting & selecting data. However, that doesn't mean there's never a need for looping constructs. So, I wanted to make a couple of points about the OP's own answer.
First, if you do need to reference array elements by index use a traditional For loop, which might look something like:
For( $i = 0; $i -lt $Array.Count; ++$i )
{
If( $array[$i].Value -gt $LargestValue )
{
$LargestName = $array[$i].Name
$LargestValue = $array[$i].Value
}
}
$i is commonly used as an iteration variable, and within the script block is used as the array index.
Second, even the traditional loop is unnecessary in this case. You can stick with the ForEach loop and track the largest value as and when it's encountered. That might look something like:
ForEach( $Row in $array )
{
If( $Row.Value -gt $LargestValue )
{
$LargestName = $Row.Name
$LargestValue = $Row.Value
}
}
Strictly speaking you don't need to assign the variables beforehand, though it may be a good practice to precede either of these with:
$LargestName = ""
$LargestValue = 0
In these examples you'd have to follow with a slightly modified Write-Host command
Write-host $LargestName,"has the highest value"
Note: Borrowed some of the test code from Doug Maurer's Fine Answer. Considering our answers were similar, this was just to make my examples more clear to the question and easier to test.
Figured it out, hopefully this isn't awful:
$Count = 1
$CurrentLargest = 0
Foreach($Row in $Array) {
# Compare This iteration vs the next to find the largest
If($Row.value -gt $Array.Value[$Count]){$CurrentLargest = $Row}
Else {$CurrentLargest = $Array[$Count]}
# Replace the existing largest value with the new one if it is larger than it.
If($CurrentLargest.Value -gt $Largest.Value){ $Largest = $CurrentLargest }
$Count += 1
}
Write-host $Largest.name,"has the highest value"
Edit: its awful, look at the other answers for a better way.

How to combine every element in an array with every element in multiple other arrays in Powershell

In Powershell, I am trying to combine the elements of several arrays to create an array of unique strings. I need to combine every element from each array with every element in the other arrays. It is difficult to concisely explain what I mean, so it may be easier to show.
I start with a 2d-array that looks something like this:
$array = #('this', ('A','B','C'), ('1','2','3','4'), 'that')
I need to create an array, whose contents will look like this:
thisA1that
thisA2that
thisA3that
thisA4that
thisB1that
thisB2that
thisB3that
thisB4that
thisC1that
thisC2that
thisC3that
thisC4that
The length and number of arrays in the original array are variable, and I don't know the order of the items in the original array.
So far, I've tried a few methods, but my logic has been wrong. Here was my first attempt at a solution:
$tempList = #()
#get total number of resources that will be created
$total = 1
for($i=0; $i -lt $array.Count; $i++){$total = $total * $array[$i].Count}
# build each resource from permutations of array parts
for ($i = 0; $i -lt $array.Count; $i++)
{
for ($j = 0; $j -lt $total; $j++)
{
$tempList += #('')
$idx = $total % $array[$i].Count
# item may either be string or an array. If string, just add it. If array, add the item at the index
if ($array[$i] -is [array]){ $tempList[$j] += $array[$i][$idx] }
else { $tempList[$j] += $array[$i] }
}
}
In this example, my logic with the modulus operator was wrong, so it would grab the only the first index of each array every time. Upon further consideration, even if I fix the modulus logic, the overall logic would still be wrong. For example, if the second two arrays were the same size, I would get 'A' paired with '1' each time, 'B' with '2', etc.
I'm sure there is a way to do this, but I simply can't seem to see it.
I think the answer's to use recursion so you can handle the fact that the array of arrays can be any length. This recursive function should:
take the first array from the array-of-arrays
loop through each item in that first array
if the first array is also the last array, just return each item.
if there are more arrays in the array-of-arrays then pass the remaining arrays to the recursive function
for each result from the recursive function, prefix the return value with the current item from the first array.
I think the code explains itself better than the above:
function Combine-Array {
[CmdletBinding()]
Param (
[string[][]]$Array
)
Process {
$current = $Array[0]
foreach ($item in $current) {
if ($Array.Count -gt 1) {
Combine-Array ([string[][]]#($Array | Select -Skip 1)) | %{'{0}{1}' -f $item, $_}
} else {
$item
}
}
}
}
Combine-Array #('this', ('A','B','C'), ('1','2','3','4'), 'that')
You can write pipeline function, which would add one more subarray into the mix. And then you call it as many times as many subarrays you have:
function CartesianProduct {
param(
[array] $a
)
filter f {
$i = $_
$a[$MyInvocation.PipelinePosition - 1] | ForEach-Object { $i + $_ }
}
Invoke-Expression ('''''' + ' | f' * $a.Length)
}
CartesianProduct ('this', ('A','B','C'), ('1','2','3','4'), 'that')

Empty value powershell array

I have a strange issue, this is my CSV:
Serveur;Carte;Cordon;IP;Mac;Vmnic ;Vmnic mac;Connect;Port
Dexter;eth1;405;172.16.5.117;00:24:e8:36:36:df;Vmnic0;00:50:56:56:36:df;sw-front-1;A1
Dexter;eth2;14;192.168.140.17;00:24:e8:36:36:e1;Vmnic1;00:50:56:56:36:e1; sw_eq_ds_1;3
;;;;;;;;
Gordon;eth1;404;172.16.5.124;b8:ac:6f:8d:ac:b4;Vmnic0;00:50:56:5d:ac:b4;;
Gordon;eth2;35;192.168.140.114;b8:ac:6f:8d:ac:b6;Vmnic1;00:50:56:5d:ac:b6;;
Gordon;eth3;254;192.168.33.10;b8:ac:6f:8d:ac:b8;Vmnic2;00:50:56:5d:ac:b8;;
So I imported it into an array with the following code:
$Serveur = #()
Import-Csv C:\Users\aasif\Desktop\myfile.csv -Delimiter ";" |`
ForEach-Object {
$Serveur += $_.Serveur
}
And to remove duplicate values I did this :
$Serveur = $Serveur | sort -uniq
So when I display my Array, I obtain these two values : Dexter and Gordon and a third null value
But I also get an empty value
The following code return 3
$Serveur.count
Why?
Thanks for your help
If you want exclude empty values you can do like this
$Serveur = $Serveur | ? { $_ } | sort -uniq
In case someone (like me) needs to remove empty elements from array, but without sorting:
$Serveur = $Serveur | Where-Object { $_ } | Select -Unique
You have an array with 3 elements, so the count is 3. The element you got from the line ;;;;;;;; isn't $null, but an empty string (""), so it counts as a valid element. If you want to omit empty elements from the array, filter them out as C.B. suggested.
On a more general note, I'd recommend against using the += operator. Each operation copies the entire array to a new array, which is bound to perform poorly. It's also completely unnecessary here. Simply echo the value of the field and assign the output as a whole back to a variable:
$csv = 'C:\Users\aasif\Desktop\myfile.csv'
$Serveur = Import-Csv $csv -Delim ';' | % { $_.Serveur } | ? { $_ } | sort -uniq

Resources