Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Splitting an IEnumerable into two

Tags:

c#

linq

I'd like to split a list into two lists, one which can be handled directly, and the other being the remainder, which will be passed down a chain to other handlers.

Input:

  • One list of items
  • a filtering method to determine which list to include the item in.

Output:

  • a "true" list
  • a "false" list

Does this already exist? Perhaps a Linq method that I'm not thinking of at the moment? Otherwise, does anyone have a good C# example?

like image 383
chills42 Avatar asked Oct 11 '12 19:10

chills42


3 Answers

Here is one simple method. Note that ToLookup eagerly evaluates the input sequence.

List<int> list = new List<int> { 1, 2, 3, 4, 5, 6 };

var lookup = list.ToLookup(num => num % 2 == 0);

IEnumerable<int> trueList = lookup[true];
IEnumerable<int> falseList = lookup[false];

You can use GroupBy to get lazy evaluation of the input sequence, but it's not quite as pretty:

var groups = list.GroupBy(num => num % 2 == 0);

IEnumerable<int> trueList = groups.Where(group => group.Key).FirstOrDefault();
IEnumerable<int> falseList = groups.Where(group => !group.Key).FirstOrDefault();
like image 94
Servy Avatar answered Nov 03 '22 14:11

Servy


After some consideration and some rather rubbish ideas, I've come to the conclusion: don't try to bend LINQ into doing this for you.

Have a simple couple of loops that consume your input sequence, pass each element to the first "handler" that can cope with it, and either ensure your last handler catches everything or at worst return a List rather than an IEnumerable.

public static void Handle(
    IEnumerable<T> source,
    Action<T> catchAll,
    params Func<T, bool>[] handlers)
{
    foreach (T t in source)
    {
        int i = 0; bool handled = false;
        while (i < handlers.Length && !handled)
            handled = handlers[i++](t);
        if (!handled) catchAll(t);
    }
}

// e.g.
public bool handleP(int input, int p)
{
    if (input % p == 0)
    {
        Console.WriteLine("{0} is a multiple of {1}", input, p);
        return true;
    }
    return false;
}


Handle(
    source,
    i => { Console.WriteLine("{0} has no small prime factor"); },
    i => handleP(i, 2),
    i => handleP(i, 3),
    ...
    );

This has the advantage of handling each element in the input order rather than dividing them into groups and losing the ordering prior to whatever you do subsequently.

like image 2
Rawling Avatar answered Nov 03 '22 14:11

Rawling


I agree with Servy's answer, but after the comments going on I thought that this approach could be interesting:

static class EnumerableExtensions
{
    public static IEnumerable<TSource> Fork<TSource>(
        this IEnumerable<TSource> source,
        Func<TSource, bool> filter,
        Action<TSource> secondary)
    {
        if (source == null) throw new ArgumentNullException("source");
        //...

        return ForkImpl(source, filter, secondary);
    }

    private static IEnumerable<TSource> ForkImpl<TSource>(
        this IEnumerable<TSource> source,
        Func<TSource, bool> filter,
        Action<TSource> secondary)
    {
        foreach(var e in source)
            if (filter(e))
                yield return e;
            else
                secondary(e);
    }
}

this could be used like this:

var ints = new [] { 1,2,3,4,5,6,7,8,9 };

// one possible use of the secondary sequence: accumulation
var acc = new List<int>();

foreach (var i in ints.Fork(x => x % 2 == 0, t => acc.Add(t)))
{
    //...
}

// later on we can process the accumulated secondary sequence
process(acc);

Here we do accumulation on the secondary sequence (the "false" values), but live processing of this secondary sequence would be possible too, therefore with just one enumeration of the source.

like image 1
Wasp Avatar answered Nov 03 '22 15:11

Wasp