Bob2Chiv
Bob2Chiv

Reputation: 1968

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.

Upvotes: 40

Views: 65453

Answers (11)

Theodor Zoulias
Theodor Zoulias

Reputation: 43996

Here is a LINQ extension method that returns the current item, along with the previous and the next. It yields ValueTuple<T, T, T> values to avoid allocations. The source is enumerated once.

/// <summary>
/// Projects each element of a sequence into a tuple that includes the previous
/// and the next element.
/// </summary>
public static IEnumerable<(T Previous, T Current, T Next)> WithPreviousAndNext<T>(
    this IEnumerable<T> source, T firstPrevious = default, T lastNext = default)
{
    ArgumentNullException.ThrowIfNull(source);
    (T Previous, T Current, bool HasPrevious) queue = (default, firstPrevious, false);
    foreach (var item in source)
    {
        if (queue.HasPrevious)
            yield return (queue.Previous, queue.Current, item);
        queue = (queue.Current, item, true);
    }
    if (queue.HasPrevious)
        yield return (queue.Previous, queue.Current, lastNext);
}

Usage example:

var source = Enumerable.Range(1, 5);
Console.WriteLine($"Source: {String.Join(", ", source)}");
var result = source.WithPreviousAndNext(firstPrevious: -1, lastNext: -1);
Console.WriteLine($"Result: {String.Join(", ", result)}");

Output:

Source: 1, 2, 3, 4, 5  
Result: (-1, 1, 2), (1, 2, 3), (2, 3, 4), (3, 4, 5), (4, 5, -1)  

To get the previous and the next of a specific item, you could use tuple deconstruction:

var (previous, current, next) = myIEnumerable
    .WithPreviousAndNext()
    .First(e => e.Current.UniqueObjectID == myItem.UniqueObjectID);

Upvotes: 3

Fidel
Fidel

Reputation: 7427

I use the following technique:

var items = new[] { "Bob", "Jon", "Zac" };

var sandwiches = items
                    .Sandwich()
                    .ToList();

Which produces this result:

enter image description here

Notice that there are nulls for the first Previous value, and the last Next value.

It uses the following extension method:

public static IEnumerable<(T Previous, T Current, T Next)> Sandwich<T>(this IEnumerable<T> source, T beforeFirst = default, T afterLast = default)
{
    var sourceList = source.ToList();

    T previous = beforeFirst;
    T current = sourceList.FirstOrDefault();

    foreach (var next in sourceList.Skip(1))
    {
        yield return (previous, current, next);

        previous = current;
        current = next;
    }

    yield return (previous, current, afterLast);
}

Upvotes: 1

jwize
jwize

Reputation: 4175

Here are some extension methods as promised. The names are generic and reusable with any type simple and there are lookup overloads to get at the item needed to get the next or previous items. I would benchmark the solutions and then see where you could squeeze cycles out.

 public static class ExtensionMethods  
{
    public static T Previous<T>(this List<T> list, T item) { 
        var index = list.IndexOf(item) - 1;
        return index > -1 ? list[index] : default(T);
    }
    public static T Next<T>(this List<T> list, T item) {
        var index = list.IndexOf(item) + 1;
        return index < list.Count() ? list[index] : default(T);
    }
    public static T Previous<T>(this List<T> list, Func<T, Boolean> lookup) { 
        var item = list.SingleOrDefault(lookup);
        var index = list.IndexOf(item) - 1;
        return index > -1 ? list[index] : default(T);
    }
    public static T Next<T>(this List<T> list, Func<T,Boolean> lookup) {
        var item = list.SingleOrDefault(lookup);
        var index = list.IndexOf(item) + 1;
        return index < list.Count() ? list[index] : default(T);
    }
    public static T PreviousOrFirst<T>(this List<T> list, T item) { 
        if(list.Count() < 1) 
            throw new Exception("No array items!");

        var previous = list.Previous(item);
        return previous == null ? list.First() : previous;
    }
    public static T NextOrLast<T>(this List<T> list, T item) { 
        if(list.Count() < 1) 
            throw new Exception("No array items!");
        var next = list.Next(item);
        return next == null ? list.Last() : next;
    }
    public static T PreviousOrFirst<T>(this List<T> list, Func<T,Boolean> lookup) { 
        if(list.Count() < 1) 
            throw new Exception("No array items!");
        var previous = list.Previous(lookup);
        return previous == null ? list.First() : previous;
    }
    public static T NextOrLast<T>(this List<T> list, Func<T,Boolean> lookup) { 
        if(list.Count() < 1) 
            throw new Exception("No array items!");
        var next = list.Next(lookup);
        return next == null ? list.Last() : next;
    }
}

And you can use them like this.

var previous = list.Previous(obj);
var next = list.Next(obj);
var previousWithLookup = list.Previous((o) => o.LookupProperty == otherObj.LookupProperty);
var nextWithLookup = list.Next((o) => o.LookupProperty == otherObj.LookupProperty);
var previousOrFirst = list.PreviousOrFirst(obj);
var nextOrLast = list.NextOrLast(ob);
var previousOrFirstWithLookup = list.PreviousOrFirst((o) => o.LookupProperty == otherObj.LookupProperty);
var nextOrLastWithLookup = list.NextOrLast((o) => o.LookupProperty == otherObj.LookupProperty);

Upvotes: 2

HedgeHogFace
HedgeHogFace

Reputation: 106

I thought I would try to answer this using Zip from Linq.

string[] items = {"nought","one","two","three","four"};

var item = items[2];

var sandwiched =
    items
        .Zip( items.Skip(1), (previous,current) => new { previous, current } )
        .Zip( items.Skip(2), (pair,next) => new { pair.previous, pair.current, next } )     
        .FirstOrDefault( triplet => triplet.current == item );

This will return a anonymous type {previous,current,next}. Unfortunately this will only work for indexes 1,2 and 3.

string[] items = {"nought","one","two","three","four"};

var item = items[4]; 

var pad1 = Enumerable.Repeat( "", 1 );
var pad2 = Enumerable.Repeat( "", 2 );

var padded = pad1.Concat( items );
var next1 = items.Concat( pad1 );
var next2 = items.Skip(1).Concat( pad2 );

var sandwiched =
    padded
        .Zip( next1, (previous,current) => new { previous, current } )
        .Zip( next2, (pair,next) => new { pair.previous, pair.current, next } )
        .FirstOrDefault( triplet => triplet.current == item );

This version will work for all indexes. Both version use lazy evaluation courtesy of Linq.

Upvotes: 2

Binary Worrier
Binary Worrier

Reputation: 51739

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.

Upvotes: 41

PHeiberg
PHeiberg

Reputation: 29851

By creating an extension method for establishing context to the current element you can use a Linq query like this:

var result = myIEnumerable.WithContext()
    .Single(i => i.Current.UniqueObjectID == myItem.UniqueObjectID);
var previous = result.Previous;
var next = result.Next;

The extension would be something like this:

public class ElementWithContext<T>
{
    public T Previous { get; private set; }
    public T Next { get; private set; }
    public T Current { get; private set; }

    public ElementWithContext(T current, T previous, T next)
    {
        Current = current;
        Previous = previous;
        Next = next;
    }
}

public static class LinqExtensions
{
    public static IEnumerable<ElementWithContext<T>> 
        WithContext<T>(this IEnumerable<T> source)
    {
        T previous = default(T);
        T current = source.FirstOrDefault();

        foreach (T next in source.Union(new[] { default(T) }).Skip(1))
        {
            yield return new ElementWithContext<T>(current, previous, next);
            previous = current;
            current = next;
        }
    }
}

Upvotes: 14

Bartosz W&#243;jtowicz
Bartosz W&#243;jtowicz

Reputation: 1401

If you need it for every element in myIEnumerable I’d just iterate through it keeping references to the 2 previous elements. In the body of the loop I'd do the processing for the second previous element and the current would be its descendant and first previous its ancestor.

If you need it for only one element I'd choose your first approach.

Upvotes: 0

msarchet
msarchet

Reputation: 15242

You are really over complicating things:

Sometimes just a for loop is going to be better to do something, and I think provide a clearer implementation of what you are trying to do/

var myList = myIEnumerable.ToList();

for(i = 0; i < myList.Length; i++)
{
   if(myList[i].UniqueObjectID == myItem.UniqueObjectID) 
   {
      previousItem = myList[(i - 1) % (myList.Length - 1)];
      nextItem = myList[(i + 1) % (myList.Length - 1)];
   }
} 

Upvotes: 3

spender
spender

Reputation: 120538

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

Upvotes: 23

Lasse Espeholt
Lasse Espeholt

Reputation: 17792

CPU

Depends entirely on where the object is in the sequence. If it is located at the end I would expect the second to be faster with more than a factor 2 (but only a constant factor). If it is located in the beginning the first will be faster because you don't traverse the whole list.

Memory

The first is iterating the sequence without saving the sequence so the memory hit will be very small. The second solution will take as much memory as the length of the list * references + objects + overhead.

Upvotes: 2

Patrick McDonald
Patrick McDonald

Reputation: 65471

You could cache the enumerable in a list

var myList = myIEnumerable.ToList()

iterate over it by index

for (int i = 0; i < myList.Count; i++)

then the current element is myList[i], the previous element is myList[i-1], and the next element is myList[i+1]

(Don't forget about the special cases of the first and last elements in the list.)

Upvotes: 4

Related Questions