Powershell- How to identify via variable, the matched string index on a array - powershell

$array = #('blue','red','purple','pink')
$array2 = #('brown','red','black','yellow')
$array | ForEach-Object {
if ($array2 -contains $_) {
Write-Host "`$array2 contains the `$array1 string [$_]"
}
}
how to get the index of the match string?

While PowerShell's -in / -contains operators allow you to test for containment of a given value in a collection (whether a given value is an element of the collection), there is no direct support for getting an element's index using only PowerShell's own features.
For .NET arrays (such as the ones created in your question[1]) you can use their .IndexOf() instance method, which uses case-SENSITIVE comparison based on the current culture; e.g.:
$array.IndexOf('red') # -> 1; case-SENSITIVE, current-culture comparison
Note that PowerShell itself is generally case-INSENSITIVE, and with -eq (and in other contexts) uses the invariant culture for comparison.
A case-INSENSITIVE solution based on the invariant culture, using the Array type's static [Array]::FindIndex() method:
$array = 'blue', 'ReD', 'yellow'
[Array]::FindIndex($array, [Predicate[string]] { 'red' -eq $args[0] }) # -> 1
Note that by delegating to a PowerShell script block ({ ... }) in which each element ($args[0]) is tested against the target value with -eq, you implicitly get PowerShell's case-insensitive, culture-invariant behavior.
Alternatively, you could use the -ceq operator for case-sensitive (but still culture-invariant) matching.
($args[0].Equals('red', 'CurrentCulture') would give you behavior equivalent to the .IndexOf() solution above).
Generally, this approach enables more sophisticated matching techniques, such as by using the regex-based -match operator, or the wildcard-based -like operator.
The above solutions find the index of the first matching element, if any.
To find the index of the last matching element, if any, use:
.LastIndexOf()
[Array]::FindLastIndex()
Note: While there is an [Array]::FindAll() method for returning all elements that meet a given predicate (criterion), there is no direct method for finding all indices.
[1] Note that you do not need #(), the array-subexpression operator to create an array from individually enumerated elements: enumerating them with ,, the array constructor operator alone is enough:
$array = 'blue','red','purple','pink'

Looks like a homework exercise to me. In any case, as mentioned, things are a lot easier if you format your code properly. It's also easier if you name your variables rather than relying on $_, because it changes as it goes through a nested loop.
There are also other ways to do this - do you want the index number or the contents? I assumed the latter
$array = #('blue','red','purple','pink')
$array2 = #('brown','red','black','yellow')
ForEach ($a in $array) {
if ($array2 -contains $a) {
Write-Host "`$array2 contains the `$array1 string $a"
}
}
$array2 contains the $array1 string red

You can try something with an index counter you can use. If $array2.ToLower() contains that element.ToLower(), then loop through that second array to find out where that element actually is.
Note that this is not going to work for large amount of arrays as the time it will take to go through would get larger and larger. But, for small samples like this one, it works fine.
$array = 'blue','Red','purple','pink', 'browN'
$array2 = 'brown','rEd','black','yellow'
$array | ForEach-Object {
if ($array2.ToLower() -contains $_.ToLower()) {
$index = 0
foreach($arrElement in $array2) {
#$index++ # based on index starting with 1
if ($arrElement -eq $_) {
Write-Host "`$array2 contains the `$array1 string [$_] at index: $index"
}
$index++ # based on index starting with 0
}
}
}
# produces output
$array2 contains the $array1 string [Red] at index: 1
$array2 contains the $array1 string [browN] at index: 0
If there are duplicates in the $array2, you'll get two separate lines that would show each index entry.
$array = 'blue','Red','purple','pink', 'browN'
$array2 = 'brown','rEd','black','yellow', 'red'
#Output would be with above code:
$array2 contains the $array1 string [Red] at index: 1
$array2 contains the $array1 string [Red] at index: 4
$array2 contains the $array1 string [browN] at index: 0

You could also do a for loop using an index counter:
$array = 'blue','red','purple','pink', 'black'
$array2 = 'brown','red','black','yellow', 'red'
for ($i = 0; $i -lt $array2.Count; $i++) {
if ($array -contains $array2[$i]) {
Write-Host "`$array2 contains the the string '$($array2[$i])' at index: $i"
}
}
Result:
$array2 contains the the string 'red' at index: 1
$array2 contains the the string 'black' at index: 2
$array2 contains the the string 'red' at index: 4

This is a practical example that uses BinarySearch and relies on your look-up array being sorted by the property of "interest".
Uses IComparer to force case insensitivity
# BinarySearch needs a sorted array
$mySortedArray = Get-ChildItem $env:TEMP | Sort-Object -Property Name
# Provide files available on your machine
$anotherArray = #(
'mat-debug-23484.log'
'MSIaa547.LOG'.ToLower()
)
foreach ($item in $anotherArray) {
$index = $null
# BinarySearch defaults to being case sensitive
$index = [array]::BinarySearch($mySortedArray.Name, $item,[Collections.CaseInsensitiveComparer]::Default)
# If no matches found index will be negative
if ($index -ge 0) {
Write-Host ('Index {0} filename {1} found!' -f $index, $mySortedArray[$index].Name) -ForegroundColor Green
}
}
# Adjusted to meet your example code
$array = #('blue','red','purple','pink')
$array2 = #('brown','red','black','yellow') | Sort-Object
$array | ForEach-Object {
$currentObject = $_
$index = $null
$index = [array]::BinarySearch($array2, $currentObject, [System.Collections.CaseInsensitiveComparer]::Default)
if ($index -ge 0) {
Write-Host ('Index={0} array2 value="{1}" found!' -f $index, $array2[$index]) -ForegroundColor Green
}
}

Related

How to compare values in arrays and add the value from one array to another

I've got 2 arrays:
$array1 = ('1','2','7','9')
$array2 = ('7','9','1','2')
These collections are dynamically changed and I need to add the values from the first array to second. I need to create a condition if.... and check... if these arrays are equals - do nothing.
But, if the first array has new value for example it becomes $array1 = ('1','2','7','9', '6') and second array doesn't have such value
then I need to add this '6' new value to the second array...so the second array will become $array2 = ('7','9','1','2', '6')... please help to achieve it.
Please note that values in 2 arrays could be in different order it doesn't matter, the goal is to have the same values inside 2 arrays.
Merge the two and sort out non-unique values:
$array2 = #($array1;$array2) |Sort-Object -Unique
For large arrays you may want to add all items from both arrays to a HashSet instead - it'll only store distinct values, so the resulting set will correspond to the new value of $array2:
$set = [System.Collections.Generic.HashSet[psobject]]::new()
$array1 |% { [void]$set.Add($_) }
$array2 |% { [void]$set.Add($_) }
$array2 = #($set)
If you want to retain the order in $array2 and append those elements (in order) from $array1 that aren't yet in $array2, you can use Compare-Object:
$array1 = '1','2','7','9','6'
$array2 = '7','9','1','2'
$array2 +=
(Compare-Object -PassThru $array1 $array2 | Where-Object SideIndicator -eq '<=')
Note: Compare-Object, like PowerShell in general, is case-insensitive by default; add -CaseSensitive if needed.
You can use IsSupersetOf and UnionWith from HashSet<T>:
$array1 = [string[]] ('1','2','7','9','new value')
$array2 = [System.Collections.Generic.HashSet[string]] ('7','9','1','2')
if(-not $array2.IsSupersetOf($array1)) {
$array2.UnionWith($array1)
}
$array2
In case you need to have an actual array back you can cast [object[]] or [string[]] back depending on your need:
[string[]] $array2
Worth mentioning that hashsets are case sensitive by default, in case you need a case-insensitive hashset you can instantiate it using a case-insensitive comparer:
$array2 = [System.Collections.Generic.HashSet[string]]::new(
[string[]]('7','9','1','2'),
[System.StringComparer]::OrdinalIgnoreCase
)
You could build on top of this solution, by creating a function.
This code block lists array 1 and array 2
It uses the -notcontains cmdlet to check if the elements in array are in array2 and if not, then appends the element in array 2.
$array = #('1','2','7','9', '15')
$array2 = #('1','2','7','9')
Write-Output "$array `n"
Write-Output "$array2 `n"
$array | ForEach-Object {
if ($array2 -notcontains $_) {
$array2 += $_
}
}
Write-Output "$array2"
Hope this helps!

Find index of array where condition is true in PowerShell [duplicate]

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

IndexOf() or .FindIndex() case-insensitive

I am trying to validate some XML with verbose logging of issues, including required order of attributes and miscapitalization. If the required order of attributes is one, two, three and the XML in question has one, three, two I want to log it. And if an attributes is simply miscapitalized, say TWO instead of two I want to log that as well.
Currently I have two arrays, $ordered with the names of the attributes as they should be (correct capitalization) and $miscapitalized with the names of the miscapitalized attributes.
So, given attributes of one, three, TWO and required order of one, two, three
$ordered = one, two, three
$miscapitalized = TWO
From here I want to append the miscapitalizion, so a new variable
$logged = one, two (TWO), three
I can get the index of $ordered where the miscapitalization occurs with
foreach ($attribute in $ordered) {
if ($attribute -iin $miscapitalized) {
$indexOrdered = [array]::IndexOf($ordered, $attribute)
}
}
However, I can't get the index in $miscapitalized based on the (correctly capitalized) $attribute. I tried
$miscapitalized = #('one', 'two', 'three')
$miscapitalized.IndexOf('TWO')
which doesn't work because .IndexOf() is case sensitive. I found this that says [Collections.Generic.List[Object]] will work, so I thought perhaps Generic.List was where the functionality came from. So I tried
$miscapitalized = [System.Collections.Generic.List[String]]#('one', 'two', 'three')
$miscapitalized.FindIndex('TWO')
Which throws
Cannot find an overload for "FindIndex" and the argument count: "1".
That led me to this that says I need an actual predicate type, not just a string. At which point I am in WAY over my head, and the only thing that I could come up with is $miscapitalized.FindIndex([System.Predicate]::new('TWO')) which doesn't work. I suspect a Predicate could/should be a regex somehow, but I can't seem to find anything that points me in the right direction, or at least that I can understand and recognize that it is pointing me in the right direction. I also found https://www.powershellstation.com/2010/05/18/passing-predicates-as-parameters-in-powershell/ that talks about a code block as predicate, but I am not clear that it's the same usage of the term predicate (it is a widely used term) nor can I grok how to even make a code block that would be helpful here.
I did come up with this approach, which uses the same foreach search in $miscapitalized as in $ordered and it does work. But I wonder if there is a more graceful approach that doesn't require nested loops. Plus, understanding Predicate as it applies here seems useful, as well as (possibly) how a codeblock might be used.
$ordered = #('one', 'two', 'three')
$miscapitalized = #('TWO')
$replacements = [System.Collections.Specialized.OrderedDictionary]::new()
foreach ($orderedAttribute in $ordered) {
if ($orderedAttribute -iin $miscapitalized) {
$indexOrdered = [array]::IndexOf($ordered, $orderedAttribute)
foreach ($miscapitalizedAttribute in $miscapitalized) {
if (($miscapitalizedAttribute -iin $ordered) -and ($miscapitalizedAttribute -ieq $orderedAttribute) -and ($miscapitalizedAttribute -cne $orderedAttribute)) {
#$indexMiscapitalized = [array]::IndexOf($miscapitalized, $miscapitalizedAttribute)
$replacements.Add($indexOrdered, "$orderedAttribute ($miscapitalizedAttribute)")
}
}
}
}
if ($replacements.Count -gt 0) {
foreach ($index in $replacements.Keys) {
$ordered[$index] = $replacements.$index
}
}
$ordered
EDIT: Based on comments below, I have tried this
$ordered = #('one', 'two', 'three')
$miscapitalized = #('TWO', 'Three')
$replacements = [System.Collections.Specialized.OrderedDictionary]::new()
foreach ($orderedAttribute in $ordered) {
if ($orderedAttribute -iin $miscapitalized) {
$indexOrdered = [array]::IndexOf($ordered, $orderedAttribute)
if ($indexMiscapitalized = $miscapitalized.FindIndex({param($s) $s -eq $orderedAttribute})) {
$replacements.Add($indexOrdered, "$orderedAttribute ($($miscapitalized[$indexMiscapitalized]))")
}
}
}
if ($replacements.Count -gt 0) {
foreach ($index in $replacements.Keys) {
$ordered[$index] = $replacements.$index
}
}
$ordered
Which gets the last one (three/Three) but is missing two/TWO. But lots of possible solutions to try tomorrow, since there will be something to learn from each one.
You can substitute a scriptblock for the predicate required by FindIndex():
PS ~> $miscapitalized = [System.Collections.Generic.List[String]]#('one', 'two', 'three')
PS ~> $predicate = {param($s) $s -eq 'TWO'}
PS ~> $miscapitalized.FindIndex($predicate)
1
This will work as expected since PowerShell's -eq operator is case-insensitive by default.
Perhaps, you're overthinking this. You could use Compare-Object to do all the hard work and then you can inspect results and log them accordingly:
# Reference array for attributes order and capitalization
[array]$reference = #(
'one'
'two'
'three'
'four'
)
# Example XML
[xml]$xml = '<foo one="1" oNe="oNe" thrEE="thrEE" two="2">dummy</foo>'
# Compare XML attributes to refrerence array
# -SyncWindow 0 - Order of items in the array matters
# https://stackoverflow.com/questions/40507552/powershell-order-sensitive-compare-objects-diff
Compare-Object -ReferenceObject $reference -DifferenceObject $xml.foo.Attributes.Name -SyncWindow 0 -CaseSensitive -includeEqual
This will produce:
InputObject SideIndicator
----------- -------------
one ==
oNe =>
two <=
thrEE =>
three <=
two =>
four <=
As you can see, the one attribute is in at the correct index (==) and properly cased. We also have additional oNe attribute, that is out of place.
You could also group the Compare-Object result and produce hashtable, which you can use for advanced logging. You could do all kinds of lookups and comparisons using SideIndicator and InputObject properties.
$group = Compare-Object -ReferenceObject $reference -DifferenceObject $xml.foo.Attributes.Name -SyncWindow 0 -CaseSensitive -includeEqual |
Group-Object -Property InputObject -AsHashTable -AsString
$group
Result
four {#{InputObject=four; SideIndicator=<=}}
one {#{InputObject=one; SideIndicator===}, #{InputObject=oNe; SideIndicator==>}}
thrEE {#{InputObject=thrEE; SideIndicator==>}, #{InputObject=three; SideIndicator=<=}}
two {#{InputObject=two; SideIndicator=<=}, #{InputObject=two; SideIndicator==>}}
In this case hashtable keys will be case-insensitive, so you can do stuff like this:
foreach ($r in $reference) {
$ret = $group.$r | Where-Object {
$_.SideIndicator -ne '==' -and $_.InputObject -cne $r
} | Select-Object -ExpandProperty InputObject |
ForEach-Object {
'Index of {0}: {1}' -f $_, $xml.foo.Attributes.Name.IndexOf($_)
}
if ($ret) {
#{ $r = $ret }
}
}
Name Value
---- -----
one Index of "oNe": 1
three Index of "thrEE": 2
You could add a small helper function that finds the index case-insensitive:
function Find-Index {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string[]]$Array,
[Parameter(Mandatory = $true, Position = 1)]
[string]$Value
)
for ($i = 0; $i -lt $Array.Count; $i++) {
if ($Array[$i] -eq $Value) { return $i }
}
-1
# or combine the elements with some unlikely string
# convert that to lowercase and split on the same unlikely string
# then use regular IndexOf() against the value which is also lower-cased:
# (($Array -join '~#~').ToLowerInvariant() -split '~#~').IndexOf($Value.ToLowerInvariant())
}
Then below that, use it like this:
# if any of the below arrays has only one item, wrap it inside #()
$ordered = 'one','two','three'
$miscapitalized = 'One','TWO'
$logged = foreach ($item in $ordered) {
$index = Find-Index $miscapitalized $item
if ($index -ge 0) {
'{0} ({1})' -f $item, $miscapitalized[$index]
}
else { $item }
}
$logged -join ','
Output
one (One),two (TWO),three

Casting Object to String Array Powershell

I want to create an array of strings instead of a variable object so that I can use the "contains" keyword on each index of the array.
$myArray = Get-ADDomain
The above creates an object, which is not what I want. I also tried
[string[]] $myArray = Get-ADDomain
But after that, $myArray only contains one string and it is the first non-empty property of Get-ADDomain, in my case "ComputersContainer". What should I do to receive an array of strings where each string is a different property, such as
$myArray[0] = "AllowedDNSSuffixes = {}"
PowerShell will always return objects by design of course, and specifying that [string[]], does not really change that.
For what you are trying to use, you have to force the array creation. The below is just one way, but I am sure others will have more elegant ways of doing this as well. Though I am curious why one would want to do this, this way. But, hey, that's just me.
# Create an empty array
$DomainData = #()
# Get all the data points for the utilized cmdlet, split on a common delimiter for the array
[string[]]$DomainData = (Get-ADDomain | Select *) -split ';'
# Display the array count
$DomainData.Count
34
# validate getting a value from the array by using an index number
$Item = $DomainData[17]
NetBIOSName=CONTOSO
[array]::IndexOf($DomainData, $Item)
17
# Use that element number to validate the use of the contains comparison operator
0..($DomainData.Count - 1) | %{ If($DomainData[$_] -contains $item){"Index key is $_ contains a value of $Item"} }
Index key is 17 contains a value of NetBIOSName=CONTOSO
# Use the previous with a partial string for a comparison, -contains cannot be used, like or match has to be used
# From the documentation:
# -Contains
# Description: Containment operator. Tells whether a collection of reference values includes a single test value.
$Item = '*domain*'
0..($DomainData.Count - 1) | %{ If($DomainData[$_] -like $item){"Index key is $_ like a value of $Item"} }
Index key is 1 like a value of *domain*
Index key is 6 like a value of *domain*
Index key is 7 like a value of *domain*
Index key is 8 like a value of *domain*
Index key is 18 like a value of *domain*
Index key is 20 like a value of *domain*
You cannot cast a PSObject directly to a string array like that.
However, this can be accomplished rather easily.
To get an array of string from the object
$myArray = Get-ADDomain
# You can use a standard array #() but these tends to be slower for bigger amount of data
$outArray = New-Object -TypeName System.Collections.Generic.List[String]
#To add just the value
$myArray.psobject.properties | Foreach { $outArray.Add($_.Value) }
# To add Name = {Value} instead
$myArray.psobject.properties | Foreach { $outArray.Add("$($_.Name) = {$($_.Value)}") }
Using an hasthable instead:
$myArray = Get-ADDomain
$hashtable = #{}
$myArray.psobject.properties | Foreach { $hashtable[$_.Name] = $_.Value }
# If you need to do something with the key
Foreach ($key in $hashtable.Keys) {
$Value = $hashtable[$key]
if ($value -like '*prod*') {
Write-Host $key
}
}

Powershell array of arrays [duplicate]

This question already has answers here:
Powershell create array of arrays
(3 answers)
Closed 5 years ago.
This is building $ret into a long 1 dimensional array rather than an array of arrays. I need it to be an array that is populated with $subret objects. Thanks.
$ret = #()
foreach ($item in $items){
$subret = #()
$subRet = $item.Name , $item.Value
$ret += $subret
}
there might be other ways but arraylist normally works for me, in this case I would do:
$ret = New-Object System.Collections.ArrayList
and then
$ret.add($subret)
The suspected preexisting duplicate question is indeed a duplicate:
Given that + with an array as the LHS concatenates arrays, you must nest the RHS with the unary form of , (the array-construction operator) if it is an array that should be added as a single element:
# Sample input
$items = [pscustomobject] #{ Name = 'n1'; Value = 'v1'},
[pscustomobject] #{ Name = 'n2'; Value = 'v2'}
$ret = #() # create an empty *array*
foreach ($item in $items) {
$subret = $item.Name, $item.Value # use of "," implicitly creates an array
$ret += , $subret # unary "," creates a 1-item array
}
# Show result
$ret.Count; '---'; $ret[0]; '---'; $ret[1]
This yields:
2
---
n1
v1
---
n2
v2
The reason the use of [System.Collections.ArrayList] with its .Add() method worked too - a method that is generally preferable when building large arrays - is that .Add() only accepts a single object as the item to add, irrespective of whether that object is a scalar or an array:
# Sample input
$items = [pscustomobject] #{ Name = 'n1'; Value = 'v1'},
[pscustomobject] #{ Name = 'n2'; Value = 'v2'}
$ret = New-Object System.Collections.ArrayList # create an *array list*
foreach ($item in $items) {
$subret = $item.Name, $item.Value
# .Add() appends whatever object you pass it - even an array - as a *single* element.
# Note the need for $null = to suppress output of .Add()'s return value.
$null = $ret.Add($subret)
}
# Produce sample output
$ret.Count; '---'; $ret[0]; '---'; $ret[1]
The output is the same as above.
Edit
It is more convoluted to create an array of tuples than fill an array with PsObjects containing Name Value as the two properties.
Select the properties you want from $item then add them to the array
$item = $item | select Name, Value
$arr = #()
$arr += $item
You can reference the values in this array by doing this
foreach($obj in $arr)
{
$name = $obj.Name
$value = $obj.Value
# Do actions with the values
}