9

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?

mklement0
  • 382,024
  • 64
  • 607
  • 775
ALIENQuake
  • 520
  • 3
  • 12
  • 28

3 Answers3

18

You can do this:

$b = "A","D","B","D","C","E","D","F" 

(0..($b.Count-1)) | where {$b[$_] -eq 'D'}

1
3
6
Matt
  • 45,022
  • 8
  • 78
  • 119
mjolinor
  • 66,130
  • 7
  • 114
  • 135
  • 1
    i like it, nice and short. only thing that bugs me is the redundant pair of parenthesis (`0..($b.count-1) | `) will do it too – Paul Dec 29 '14 at 15:28
  • Attempting to use the static methods of `[array]` do not have the simplicity that this does. @Paul never hurt anyone to be explicit. – Matt Dec 29 '14 at 15:28
  • mjolinor Thank you @Matt Besides that this answer is really nice, do you know how to do this by using methods? – ALIENQuake Dec 29 '14 at 16:31
  • @ALIENQuake This is answer needs to loop as well but is far more elegant. I posted an answer using an `[array]` method but only because you asked. – Matt Dec 29 '14 at 17:34
  • 1
    @Matt May I propose a lite improvement? Instead of ($b.count-1) we can use [array]::LastIndexOf($b,"D") - it will save us some time when we would searching 100k array and the lat element will be at 1001 or so. – ALIENQuake Dec 30 '14 at 10:58
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
mklement0
  • 382,024
  • 64
  • 607
  • 775
2

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.

Matt
  • 45,022
  • 8
  • 78
  • 119
  • Also good how running `[array]::IndexOf` shows all the method available as well. – RoadRunner Apr 07 '20 at 12:28
  • 1
    @RoadRunner to be specific it would show all the available overloads for that particular static method. If you wanted to see all the static methods you could do `[array] | Get-Member -Static` – Matt Apr 07 '20 at 19:09