Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get previous and next item in a IEnumerable using LINQ

I have an IEnumerable of a custom type. (That I've gotten from a SelectMany)

I also have an item (myItem) in that IEnumerable that I desire the previous and next item from the IEnumerable.

Currently, I'm doing the desired like this:

var previousItem = myIEnumerable.Reverse().SkipWhile(      i => i.UniqueObjectID != myItem.UniqueObjectID).Skip(1).FirstOrDefault(); 

I can get the next item by simply ommitting the .Reverse.

or, I could:

int index = myIEnumerable.ToList().FindIndex(      i => i.UniqueObjectID == myItem.UniqueObjectID) 

and then use .ElementAt(index +/- 1) to get the previous or next item.

  1. Which is better between the two options?
  2. Is there an even better option available?

"Better" includes a combination of performance (memory and speed) and readability; with readability being my primary concern.

like image 250
Bob2Chiv Avatar asked Jan 06 '12 15:01

Bob2Chiv


People also ask

Can you use LINQ on IEnumerable?

All LINQ methods are extension methods to the IEnumerable<T> interface. That means that you can call any LINQ method on any object that implements IEnumerable<T> . You can even create your own classes that implement IEnumerable<T> , and those classes will instantly "inherit" all LINQ functionality!

Does LINQ select return new object?

While the LINQ methods always return a new collection, they don't create a new set of objects: Both the input collection (customers, in my example) and the output collection (validCustomers, in my previous example) are just sets of pointers to the same objects.

Why does LINQ return IEnumerable?

LINQ queries return a lazily evaluated IEnumerable<T> . The query is performed upon enumeration. Even if your source IEnumerable<T> had millions of records the above query would be instantaneous. Edit: Think of LINQ queries as creating a pipeline for your result rather than imperatively creating the result.


2 Answers

First off

"Better" includes a combination of performance (memory and speed)

In general you can't have both, the rule of thumb is, if you optimise for speed, it'll cost memory, if you optimise for memory, it'll cost you speed.

There is a better option, that performs well on both memory and speed fronts, and can be used in a readable manner (I'm not delighted with the function name, however, FindItemReturningPreviousItemFoundItemAndNextItem is a bit of a mouthful).

So, it looks like it's time for a custom find extension method, something like . . .

public static IEnumerable<T> FindSandwichedItem<T>(this IEnumerable<T> items, Predicate<T> matchFilling) {     if (items == null)         throw new ArgumentNullException("items");     if (matchFilling == null)         throw new ArgumentNullException("matchFilling");      return FindSandwichedItemImpl(items, matchFilling); }  private static IEnumerable<T> FindSandwichedItemImpl<T>(IEnumerable<T> items, Predicate<T> matchFilling) {     using(var iter = items.GetEnumerator())     {         T previous = default(T);         while(iter.MoveNext())         {             if(matchFilling(iter.Current))             {                 yield return previous;                 yield return iter.Current;                 if (iter.MoveNext())                     yield return iter.Current;                 else                     yield return default(T);                 yield break;             }             previous = iter.Current;         }     }     // If we get here nothing has been found so return three default values     yield return default(T); // Previous     yield return default(T); // Current     yield return default(T); // Next } 

You can cache the result of this to a list if you need to refer to the items more than once, but it returns the found item, preceded by the previous item, followed by the following item. e.g.

var sandwichedItems = myIEnumerable.FindSandwichedItem(item => item.objectId == "MyObjectId").ToList(); var previousItem = sandwichedItems[0]; var myItem = sandwichedItems[1]; var nextItem = sandwichedItems[2]; 

The defaults to return if it's the first or last item may need to change depending on your requirements.

Hope this helps.

like image 52
Binary Worrier Avatar answered Sep 27 '22 19:09

Binary Worrier


For readability, I'd load the IEnumerable into a linked list:

var e = Enumerable.Range(0,100); var itemIKnow = 50; var linkedList = new LinkedList<int>(e); var listNode = linkedList.Find(itemIKnow); var next = listNode.Next.Value; //probably a good idea to check for null var prev = listNode.Previous.Value; //ditto 
like image 22
spender Avatar answered Sep 27 '22 17:09

spender