Search code examples
c#linqgeneric-collections

Take all items except the last ones that satisfy condition?


My specific requirement is that I have an IEnumerable<IEnumerable<string>>, and I want to "take" all items in the outer enumeration except any "empty" trailing items, where "empty" means all strings are null/empty or the inner enumeration is empty. Note that I want to keep any empty items that appear before the last non-empty one. For example:

Item 1: a, b, c
Item 2: (nothing)
Item 3: a, f, g, h
Item 4: (nothing)
Item 5: (nothing)

I would like to keep items 1–3 but trim items 4 and 5.

In a more general sense, I have an enumeration of items, where I want to trim any trailing items that satisfy a condition, that appear behind the last item that does not satisfy the condition.

For the sake of choosing an appropriate solution, I might add that the outer enumeration will usually contain a few hundred up to a few hundred thousand items, while the inner enumerations contain only a few items each. There will probably be only a couple of empty items that I need to trim.

My current solution puts all outer items in a list (after transforming them with .Select(...)), and then in a loop keep removing the last item if it's empty, until a non-empty item is found.


Solution

  • There is no standard efficient LINQ solution. I would go with a custom extension "LINQ like" method like this:

    public static class EnumerableExtensions
    {
        public static IEnumerable<T> SkipLastWhile<T>(this IEnumerable<T> source, Func<T, bool> predicate)
        {
            var skipBuffer = new List<T>();
            foreach (var item in source)
            {
                if (predicate(item))
                    skipBuffer.Add(item);
                else
                {
                    if (skipBuffer.Count > 0)
                    {
                        foreach (var skipped in skipBuffer)
                            yield return skipped;
                        skipBuffer.Clear();
                    }
                    yield return item;
                }
            }
        }
    }
    

    It requires additional space for buffering the longest item sequence satisfying the skip predicate while the LINQ Reverse method has to buffer the whole input sequence.

    The usage will be:

    var result = input.SkipLastWhile(e => !e.Any());