Getting a collection of index values using a LINQ query
Asked Answered
C

6

17

Is there a better way to do this?

string[] s = {"zero", "one", "two", "three", "four", "five"};

var x = 
s
.Select((a,i) => new {Value = a, Index = i})
.Where(b => b.Value.StartsWith("t"))
.Select(c => c.Index);

i.e. I'm looking for a more efficient or more elegant way to get the positions of the items matching the criteria.

Crossruff answered 26/10, 2008 at 2:0 Comment(0)
B
33

You could easily add your own extension method:

public static IEnumerable<int> IndexesWhere<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
    int index=0;
    foreach (T element in source)
    {
        if (predicate(element))
        {
            yield return index;
        }
        index++;
    }
}

Then use it with:

string[] s = {"zero", "one", "two", "three", "four", "five"};
var x = s.IndexesWhere(t => t.StartsWith("t"));
Barta answered 26/10, 2008 at 7:15 Comment(0)
E
6

If you're just using the example as a way to learn LINQ, ignore this post.


It's not clear to me that LINQ is actually the best way to do this. The code below seems like it would be more efficient since no new anonymous type needs to be created. Granted, your example may be contrived and the technique might be more useful in a different context, for example in a data structure where it could take advantage of an index on value, but the code below is reasonably straight-forward, understandable (no thought required) and arguably more efficient.

string[] s = {"zero", "one", "two", "three", "four", "five"};
List<int> matchingIndices = new List<int>();

for (int i = 0; i < s.Length; ++i) 
{
   if (s[i].StartWith("t"))
   {
      matchingIndices.Add(i);
   }
}
Extricate answered 26/10, 2008 at 3:10 Comment(2)
Thanks for the answer. I agree that this would be more efficient. As you guessed this is a simplified version of something more complex that "has" to be done in LINQ in this particular case.Crossruff
And that exact code only works when you're using List instead of an arbitrary IEnumerable. Otherwise you'd have to use foreach and a separate local variable.Barta
A
5

Seems fine to me. You might save a couple characters by changing the select to:

.Select((Value, Index) => new {Value, Index})
Ameline answered 26/10, 2008 at 2:3 Comment(2)
Thanks - I didn't know you could do that - I thought you had to reassign.Crossruff
It's called a "projection initializer" - it basically takes the last subexpression (which must be a field or property) within the expression and uses that for the name. So you could do x.GetFoo().Bar and that would be equivalent to Bar=x.GetFoo().Bar.Barta
O
2

There is also FindIndex method in Collection List for which you create a delete method which can return the index from the collection. you can refer to the following link in msdn http://msdn.microsoft.com/en-us/library/x1xzf2ca.aspx.

Outgrowth answered 16/4, 2009 at 13:53 Comment(0)
E
1

How about this? It's similar to the original poster's but I first select the indexes and then build a collection which matches the criteria.

var x = s.Select((a, i) => i).Where(i => s[i].StartsWith("t"));

This is a tad less efficient than some of the other answers as the list is fully iterated over twice.

Entranceway answered 24/6, 2009 at 3:12 Comment(0)
C
0

I discussed this interesting problem with a colleague and at first I thought JonSkeet's solution was great, but my colleague pointed out one problem, namely that if the function is an extension to IEnumerable<T>, then it can be used where a collection implements it.

With an array, it's safe to say the order produced with foreach will be respected (i.e. foreach will iterate from first to last), but it would not necessarily be the case with other collections (List, Dictionary, etc), where foreach would not reflect necessarily "order of entry". Yet the function is there, and it can be misleading.

In the end, I ended up with something similar to tvanfosson's answer, but as an extension method, for arrays:

public static int[] GetIndexes<T>(this T[]source, Func<T, bool> predicate)
{
    List<int> matchingIndexes = new List<int>();

    for (int i = 0; i < source.Length; ++i) 
    {
        if (predicate(source[i]))
        {
            matchingIndexes.Add(i);
        }
    }
    return matchingIndexes.ToArray();
}

Here's hoping List.ToArray will respect the order for the last operation...

Chromogen answered 20/10, 2011 at 0:7 Comment(3)
"Yet the function is there, and it can be misleading." - but that would be the same for any of the other LINQ methods that use indexes, e.g. the .Select((item, index) => . I don't think it's necessarily anything wrong with Jon's function as long as you understand what you're getting.Eminence
@Eminence I've gathered more experience with LINQ and the IEnumerable interface since that answer. I agree that if you know what you're doing, and if the function is defined, there won't be surprises. One potential hazard would be to get the indexes off an IEnumerable produced with AsParallel. Those indexes will reflect the state of how the collection was produced, but producing it again is almost guaranteed to give a different result. The indexes will refer to something that does not exist. In team work (or reusing the function later), if that behaviour isn't clear, you're gonna be in trouble.Chromogen
I still think that the order isn't likely to change unless you modify the collection, though, and that you ought not use the indexes unless it makes sense for the structure. But I hadn't considered AsParallel - neat, thanks.Eminence

© 2022 - 2024 — McMap. All rights reserved.