IndexOf() or .FindIndex() case-insensitive - powershell

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

Related

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.

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

$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
}
}

PS Object unescape character

I have small error when running my code. I assign a string to custom object but it's parsing the string by itself and throwing an error.
Code:
foreach ($item in $hrdblistofobjects) {
[string]$content = Get-Content -Path $item
[string]$content = $content.Replace("[", "").Replace("]", "")
#here is line 43 which is shown as error as well
foreach ($object in $listofitemsdb) {
$result = $content -match $object
$OurObject = [PSCustomObject]#{
ObjectName = $null
TestObjectName = $null
Result = $null
}
$OurObject.ObjectName = $item
$OurObject.TestObjectName = $object #here is line 52 which is other part of error
$OurObject.Result = $result
$Resultsdb += $OurObject
}
}
This code loads an item and checks if an object exists within an item. Basically if string part exists within a string part and then saves result to a variable. I am using this code for other objects and items but they don't have that \p part which I am assuming is the issue. I can't put $object into single quotes for obvious reasons (this was suggested on internet but in my case it's not possible). So is there any other option how to unescape \p? I tried $object.Replace("\PMS","\\PMS") but that did not work either (this was suggested somewhere too).
EDIT:
$Resultsdb = #(foreach ($item in $hrdblistofobjects) {
[string]$content = Get-Content -Path $item
[string]$content = $content.Replace("[", "").Replace("]", "")
foreach ($object in $listofitemsdb) {
[PSCustomObject]#{
ObjectName = $item
TestObjectName = $object
Result = $content -match $object
}
}
}
)
$Resultsdb is not defined as an array, hence you get that error when you try to add one object to another object when that doesn't implement the addition operator.
You shouldn't be appending to an array in a loop anyway. That will perform poorly, because with each iteration it creates a new array with the size increased by one, copies all elements from the existing array, puts the new item in the new free slot, and then replaces the original array with the new one.
A better approach is to just output your objects in the loop and collect the loop output in a variable:
$Resultsdb = foreach ($item in $hrdblistofobjects) {
...
foreach ($object in $listofitemsdb) {
[PSCustomObject]#{
ObjectName = $item
TestObjectName = $object
Result = $content -match $object
}
}
}
Run the loop in an array subexpression if you need to ensure that the result is an array, otherwise it will be empty or a single object when the loop returns less than two results.
$Resultsdb = #(foreach ($item in $hrdblistofobjects) {
...
})
Note that you need to suppress other output on the default output stream in the loop, so that it doesn't pollute your result.
I changed the match part to this and it's working fine $result = $content -match $object.Replace("\PMS","\\PMS").
Sorry for errors in posting. I will amend that.

Converting strings to timespans, $PSItem in 'switch'?

I have a bunch of strings, in the form of:
'3m 36s', '24m 38s', '59s'
, to be converted to timespans. My current "solution" is:
'3m 36s', '24m 38s', '59s' |ForEach-Object {
$s = 0
$m = 0
$h = 0
$PSItem.Split(' ') |ForEach-Object {
$item = $PSItem
switch ($PSItem[-1])
{
's'
{
$s = $item.TrimEnd('s')
}
'm'
{
$m = $item.TrimEnd('m')
}
'h'
{
$h = $item.TrimEnd('h')
}
Default
{
Write-Error 'Ooops...' -ErrorAction Stop
}
}
}
$timespan = New-TimeSpan -Hours $h -Minutes $m -Seconds $s
# ToString() is used just to get some easy to read output
$timespan.ToString()
}
While it seems to work for me, I have two issues with the above:
Is the general approach
ForEach -> Split(' ') -> ForEach -> switch
OK-ish? Are there any alternative/better ways of doing the conversion?
I tried using $PSItem in the switch
It seems that the switch construct has it's "own pipeline"
# $item = $PSItem
switch ($PSItem[-1])
{
's'
{
$PSItem
}
}
-- in the above $PSItem evaluates to 's'(, 'm', the value matched). What is actually going on? (internaly?)
I would take one ForEach loop out of things by performing that loop with the Switch command. Here's what I'd end up with:
'3m 36s', '59s', '24m 38s' |%{
$TSParams = #{}
Switch($_.Split()){
{$_[-1] -eq 's'}{$TSParams.Add('Seconds', ([int]$_.trim('s')))}
{$_[-1] -eq 'm'}{$TSParams.Add('Minutes', ([int]$_.trim('m')))}
{$_[-1] -eq 'h'}{$TSParams.Add('Hours', ([int]$_.trim('h')))}
}
New-TimeSpan #TSParams
}
For each string it creates an empty hashtable, then loops through each item of the Split() method, adding the appropriate time to the hashtable. Then it splats that to the New-TimeSpan command, and moves to the next item in the ForEach loop. I tried it locally and had some issues initially when the numbers did not cast as an int, and it tried to convert them to a DateTime, which is why I type cast them in the above code.

Compare objects based on subset of properties

Say I have 2 powershell hashtables one big and one small and, for a specific purpose I want to say they are equal if for the keys in the small one, the keys on the big hastable are the same.
Also I don't know the names of the keys in advance. I can use the following function that uses Invoke-Expression but I am looking for nicer solutions, that don't rely on this.
Function Compare-Subset {
Param(
[hashtable] $big,
[hashtable] $small
)
$keys = $small.keys
Foreach($k in $keys) {
$expression = '$val = $big.' + "$k" + ' -eq ' + '$small.' + "$k"
Invoke-Expression $expression
If(-not $val) {return $False}
}
return $True
}
$big = #{name='Jon'; car='Honda'; age='30'}
$small = #{name = 'Jon'; car='Honda'}
Compare-Subset $big $small
A simple $true/$false can easily be gotten. This will return $true if there are no differences:
[string]::IsNullOrWhiteSpace($($small|Select -Expand Keys|Where{$Small[$_] -ne $big[$_]}))
It checks for all keys in $small to see if the value of that key in $small is the same of the value for that key in $big. It will only output any values that are different. It's wrapped in a IsNullOrWhitespace() method from the [String] type, so if any differences are found it returns false. If you want to list differences just remove that method.
This could be the start of something. Not sure what output you are looking for but this will output the differences between the two groups. Using the same sample data that you provided:
$results = Compare-Object ($big.GetEnumerator() | % { $_.Name }) ($small.GetEnumerator() | % { $_.Name })
$results | ForEach-Object{
$key = $_.InputObject
Switch($_.SideIndicator){
"<="{"Only reference object has the key: '$key'"}
"=>"{"Only difference object has the key: '$key'"}
}
}
In primetime you would want something different but just to show you the above would yield the following output:
Only reference object has the key: 'age'