Add context to IEnumerable<> elements

by Alex Siepman 9. February 2015 20:13

Elements of an IEnumerable sequence do not know about the other elements. I often need to compare an element with a previous or next element. Sometimes I need other context like all the other elements, the previous elements or if an element is the last element. That is why I use an extension method that adds context to all elements.

Examples

Imagine you have class like this:

class President
{
    public string Name { get; private set; }
    public int Salary { get; private set; }

    public President(string name, int salary)
    {
        Name = name;
        Salary = salary;
    }

    public override string ToString()
    {
        return string.Format("{0} - {1}", Name, Salary);
    }
}

and a sequence like this:

var presidents = new List<President>
{
    new President( "Albert" , 100000),
    new President( "Bert" , 90000),
    new President( "Charly" , 105000),
    new President( "Dave" , 103000),
    new President( "Edward" , 104000),
    new President( "Fred" , 107000),
    new President( "Gilbert" , 110000),
    new President( "Henry" , 103000),
};

Now you can add context:

var presidentsWithContext = presidents.WithContext();

and use the context with LINQ.

var presidentsEarnedMoreThanPreviousPresident =
    presidentsWithContext.Where(p => !p.IsFirst && 
                                p.Previous.Salary < p.Current.Salary);

var presidentsEarnedMoreThanEverBefore =
    presidentsWithContext.Where(p => !p.IsFirst && 
                                p.Preceding.All(n => n.Salary < p.Current.Salary));

var presidentsEarnedUniqueSalary =
    presidentsWithContext.Where(p => p.Other.All(b => b.Salary != p.Current.Salary));

This is the class with the extension method that I just called:

/// <summary>
/// Contains extension methods for adding context information to Enumerated Elements
/// </summary>
public static class ContextEnumerator
{
    /// <summary>
    /// Creates a sequence providing the sequence context with each element
    /// </summary>
    /// <param name="source">The input sequence</param>
    /// <returns>An output sequence with information about the context of each element</returns>
    static public IEnumerable<ElementWithContext<T>> WithContext<T>(this IEnumerable<T> source)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }
        using (var enumerator = source.GetEnumerator())
        {
            // move to the first element
            if (!enumerator.MoveNext())
                yield break;

            T previous = default(T);
            T current = enumerator.Current;
            var index = 0;

            // Continue from the seceond element 
            while (enumerator.MoveNext())
            {
                T next = enumerator.Current;
                yield return new ElementWithContext<T>(previous, current, next,
                    index, source, false);

                previous = current;
                current = next;
                index++;
            }

            // Return the last element
            yield return new ElementWithContext<T>(previous, current, default(T),
                index, source, true);
        }
    }


    public static IEnumerable<T> CurrentValues<T>(this IEnumerable<ElementWithContext<T>> elementsWithContext)
    {
        if (elementsWithContext == null)
        {
            throw new ArgumentNullException("elementsWithContext");
        }
        return elementsWithContext.Select(e => e.Current);
    }
}

 And this class provides the info of an element:

/// <summary>
/// Represents an Enumerated Element with additional properties about its context in the sequence
/// </summary>
public class ElementWithContext<T> 
{
    internal ElementWithContext(T previous, T current, T next, 
                                int index, IEnumerable<T> all, bool isLast)
    {
        Current = current;
        Previous = previous;
        Next = next;
        Index = index;
        All = all;
        IsLast = isLast;
    }

    /// <summary>All elements in the original order</summary>
    public IEnumerable<T> All { get; private set; }

    /// <summary>The element before the current element</summary>
    public T Previous { get; private set; }

    /// <summary>The current element</summary>
    public T Current { get; private set; }

    /// <summary>The element after the current element</summary>
    public T Next { get; private set; }

    /// <summary>The index of the current element</summary>
    public int Index { get; private set; }

    /// <summary>True if this is the first element</summary>
    public bool IsFirst { get { return Index == 0; } }

    /// <summary>True if this is the last element</summary>
    public bool IsLast { get; private set; }

    /// <summary>The elements before the current element</summary>
    public IEnumerable<T> Preceding { get { return All.Take(Index); } }

    /// <summary>The elements after the current element</summary>
    public IEnumerable<T> Following { get { return All.Skip(Index + 1); } }

    /// <summary>All elements except the current element</summary>
    public IEnumerable<T> Other { get { return Preceding.Concat(Following); } }

    public override string ToString()
    {
        if (Current == null)
        {
            return "Current is null.";
        }
        return Current.ToString();
    }
}

As you can see, a lot of other properties are available like IsLast and Index.

Credits: Frank Bakker inspired me with an early version of this idea.

Comments (6) -

Arthur van Leeuwen Netherlands
2/11/2015 12:21:45 PM #

This breaks as soon as the IEnumerable passed in as source can not be enumerated more than once, or has different results when enumerating more than once. The enumerable should really be memoized for this to work.

Reply

Admin Netherlands
2/11/2015 10:29:07 PM #

I agree that there are situations where memorizing is needed. Memorizing has other disadvantages, so a second extension method (ToWithContextList()?) is an option in those cases. This give you the option to choose the best option based on the situation. In cases where you have a simple list or array that is not modified during the use of WithContext(), I still prefer this solution...

Reply

Vincenzo United States
2/11/2015 3:37:43 PM #

Honestly I cannot understand the utility of this. You’re just enhancing the Enumerable to become something like a List, but with less functionalities. Just use that!

The enumerables’s purpose ARE to be traversed in one direciton and one time only.

Reply

Thomas Levesque France
2/11/2015 9:12:17 PM #

> Honestly I cannot understand the utility of this. You’re just enhancing the Enumerable to become something like a List, but with less functionalities. Just use that!

It's not always possible to use a List; sometimes you get the data in IEnumerable form, and the data set can be too large to just call ToList() on it.

> The enumerables’s purpose ARE to be traversed in one direciton and one time only.

That's not its purpose, but it is indeed a recommended practice to enumerate Enumerables only once. And the code in this article does exactly that. It's an elegant way to provide access to the previous and next item in the sequence, which can be very useful.

Reply

Peter Ritchie Canada
2/12/2015 4:06:45 PM #

I think he means enumerator.  e.g. there is no IEnumerator.MovePrevious().  Although Reset shows it can be enumerated more than once.

Reply

Peter Ritchie Canada
2/12/2015 4:16:27 PM #

I agree with other comments about the utility of this.  It seems to be trying to put take responsibility off the enumerable and put it on the element, or trying to push List<T> functionality into IEnumerable<T>.  The concept of "First", "Index", or "Last" on an IEnumerable is specious.  The generic IEnumerable shouldn't really have these concepts, there are so many instances of an IEnumerable simply being an interface enumerate data--that data may no longer have a "beginning" and may not have an "end".  Imagine an IEnumerable<Key> that modeled the keys being pressed by a user on the keyboard--that stream of keys really doesn't have an end.  And the "first" would simply be the first key you noticed, Reset would either throw an exception or do nothing as the "next" key would then become the "first"...

It is creative though.

Reply

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading

About the author

I am a software architect at Roxit and also a C# Developer. My main interests in the area of ​​C# are LINQ and generics

Visit my personal homepage (Dutch) for more info about me.

Month List

Page List