Colonel Panic
Colonel Panic

Reputation: 137782

IEnumerable foreach, do something different for the last element

I have an IEnumerable<T>. I want to do one thing for each item of the collection, except the last item, to which I want to do something else. How can I code this neatly? In Pseudocode

foreach (var item in collection)
{
    if ( final )
    {
        g(item)
    }
    else
    {
        f(item)
    }
}

So if my IEnumerable were Enumerable.Range(1,4) I'd do f(1) f(2) f(3) g(4). NB. If my IEnumerable happens to be length 1, I want g(1).

My IEnumerable happens to be kind of crappy, making Count() as expensive as looping over the whole thing.

Upvotes: 12

Views: 6765

Answers (6)

Daniel M&#246;ller
Daniel M&#246;ller

Reputation: 86650

This might be a little too much, but I think it's elegant.

Creating an extension method for IEnumerable<T> that takes an Action<T> for each type of item, we can make a loop like this:

int sum = 0;

collection.IEnumForEachItem(

    firstItem =>
    {
        //do things for first item
        sum = firstItem.SomeProperty;
    },

    midItem =>
    {
        doThingsForIntermediate(midItem);
        sum += midItem.SomeProperty * 2;
    },

    lastItem =>
    {
        if (lastItem.SomeProperty > 10);
            sum += 10;
        else
            sum += lastItem.SomeProperty;
    }
);

This is the extension method (it must be in a static class):

public static void IEnumForEachItem<T>(this IEnumerable<T> items, 
    Action<T> actionForFirstItem, Action<T> actionForMidItems, Action<T> actionForLastItem)
{
    using (IEnumerator<T> enumerator = items.GetEnumerator())
    {
        if (enumerator.MoveNext())
            actionForFirstItem(enumerator.Current);

        if (!enumerator.MoveNext())
            throw new InvalidOperationException("IEnumerable must have at least 2 items");


        T currentItem = enumerator.Current;
        while (enumerator.MoveNext())
        {
            actionForMidItems(currentItem);
            currentItem = enumerator.Current;
        }
        actionForLastItem(currentItem);
    }
}

From this it's easy to make methods that take only "first and others" and also "others and last".

Upvotes: 0

Myster
Myster

Reputation: 18114

Similar to Marc's answer, but you could write an extension method to wrap it up.

public static class LastEnumerator
{
    public static IEnumerable<MetaEnumerableItem<T>> GetLastEnumerable<T>(this IEnumerable<T> blah)
    {
        bool isFirst = true;
        using (var enumerator = blah.GetEnumerator())
        {
            if (enumerator.MoveNext())
            {
                bool isLast;
                do
                {
                    var current = enumerator.Current;
                    isLast = !enumerator.MoveNext();
                    yield return new MetaEnumerableItem<T>
                        {
                            Value = current,
                            IsLast = isLast,
                            IsFirst = isFirst
                        };
                    isFirst = false;
                } while (!isLast);
            }
        }

    }
}

public class MetaEnumerableItem<T>
{
    public T Value { get; set; }
    public bool IsLast { get; set; }
    public bool IsFirst { get; set; }
}

Then call it like so:

foreach (var row in records.GetLastEnumerable())
{
    output(row.Value);
    if(row.IsLast)
    {
        outputLastStuff(row.Value);
    }
}

Upvotes: 3

Jon
Jon

Reputation: 437854

If you want to do this as efficiently as possible there is no other choice than effectively looking at not only the current but also the "next" or "previous" item, so you can defer the decision of what to do after you have that information. For example, assuming T is the type of items in the collection:

if (collection.Any()) {
    var seenFirst = false;
    T prev = default(T);
    foreach (var current in collection) {
        if (seenFirst) Foo(prev);
        seenFirst = true;
        prev = current;
    }
    Bar(prev);
}

See it in action.

Upvotes: 1

JonC
JonC

Reputation: 978

I wouldn't really recommend it, but I guess you could do something like this...

object m_item = notPartOfListFlag = new object();
foreach(var item in enumerator){
   if(m_item != notPartOfListFlag)
   {
      //do stuff to m_item;
   }
   m_item = item;
}
//do stuff to last item aka m_item;

But I would try to use some kind of collection that exposes the position of the items in the list, then use

if(collection.IndexOf(item) == collection.Count-1) do stuff

Upvotes: 0

Andy
Andy

Reputation: 6466

not 100% sure I like this but you could always delay the use of item until you've moved one step in to the IEnumerable array, that way when you get to the end you've not used the last entry.

it avoids having to force a count on the enumerator.

object item = null;
foreach (var a in items)
{
  // if item is set then we can use it.
  if (item != null)
  {
      // not final item
      f(item);
  }
  item = a;
}

// final item.
g(item);

Upvotes: 0

Marc Gravell
Marc Gravell

Reputation: 1064184

Since you mention IEnumerable[<T>] (not IList[<T>] etc), we can't rely on counts etc: so I would be tempted to unroll the foreach:

using(var iter = source.GetEnumerator()) {
    if(iter.MoveNext()) {
        T last = iter.Current;
        while(iter.MoveNext()) {
            // here, "last" is a non-final value; do something with "last"
            last = iter.Current;
        }
        // here, "last" is the FINAL one; do something else with "last"
    }
}

Note the above is technically only valid for IEnuemerable<T>; for non-generic, you'd need:

var iter = source.GetEnumerator();
using(iter as IDisposable) {
    if(iter.MoveNext()) {
        SomeType last = (SomeType) iter.Current;
        while(iter.MoveNext()) {
            // here, "last" is a non-final value; do something with "last"
            last = (SomeType) iter.Current;
        }
        // here, "last" is the FINAL one; do something else with "last"
    }
}

Upvotes: 23

Related Questions