Can I split an IEnumerable into two by a boolean criteria without two queries?

C#.NetLinq

C# Problem Overview


Can I split an IEnumerable<T> into two IEnumerable<T> using LINQ and only a single query/LINQ statement?

I want to avoid iterating through the IEnumerable<T> twice. For example, is it possible to combine the last two statements below so allValues is only traversed once?

IEnumerable<MyObj> allValues = ...
List<MyObj> trues = allValues.Where( val => val.SomeProp ).ToList();
List<MyObj> falses = allValues.Where( val => !val.SomeProp ).ToList();

C# Solutions


Solution 1 - C#

You can use this:

var groups = allValues.GroupBy(val => val.SomeProp);

To force immediate evaluation like in your example:

var groups = allValues.GroupBy(val => val.SomeProp)
                      .ToDictionary(g => g.Key, g => g.ToList());
List<MyObj> trues = groups[true];
List<MyObj> falses = groups[false];

Solution 2 - C#

Some people like Dictionaries, but I prefer Lookups due to the behavior when a key is missing.

IEnumerable<MyObj> allValues = ...
ILookup<bool, MyObj> theLookup = allValues.ToLookup(val => val.SomeProp);

// does not throw when there are not any true elements.
List<MyObj> trues = theLookup[true].ToList();
// does not throw when there are not any false elements.
List<MyObj> falses = theLookup[false].ToList();

Unfortunately, this approach enumerates twice - once to create the lookup, then once to create the lists.

If you don't really need lists, you can get this down to a single iteration:

IEnumerable<MyObj> trues = theLookup[true];
IEnumerable<MyObj> falses = theLookup[false];

Solution 3 - C#

Copy pasta extension method for your convenience.

public static void Fork<T>(
    this IEnumerable<T> source,
    Func<T, bool> pred,
    out IEnumerable<T> matches,
    out IEnumerable<T> nonMatches)
{
    var groupedByMatching = source.ToLookup(pred);
    matches = groupedByMatching[true];
    nonMatches = groupedByMatching[false];
}

Or using tuples in C# 7.0

public static (IEnumerable<T> matches, IEnumerable<T> nonMatches) Fork<T>(
    this IEnumerable<T> source,
    Func<T, bool> pred)
{
    var groupedByMatching = source.ToLookup(pred);
    return (groupedByMatching[true], groupedByMatching[false]);
}

// Ex.
var numbers = new [] { 1, 2, 3, 4, 5, 6, 7, 8 };
var (numbersLessThanEqualFour, numbersMoreThanFour) = numbers.Fork(x => x <= 4);

Solution 4 - C#

Modern C# example using just Linq, no custom extension methods:

(IEnumerable<MyObj> trues, IEnumerable<MyObj> falses) 
	= ints.Aggregate<MyObj,(IEnumerable<MyObj> trues, IEnumerable<MyObj> falses)>(
		(new List<MyObj>(),new List<MyObj>()), 
		(a, i) => i.SomeProp ? (a.trues.Append(i), a.falses) : (a.trues, a.falses.Append(i))
	);

Does this answer the question, yes; is this better or more readable than a foreach, no.

Solution 5 - C#

Had some fun coming up with this extension method based on the ToLookup suggestion in other answers:

public static (IEnumerable<T> XS, IEnumerable<T> YS) Bifurcate<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
    var lookup = source.ToLookup(predicate);
    return (lookup[true], lookup[false]);
}

The callsite will look like this:

var numbers = new []{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var (evens, odds) = numbers.Bifurcate(n => n % 2 == 0);

I think the usability of this is nice which is why I'm posting this answer.

We can go even further:

public static (IEnumerable<T> XS, IEnumerable<T> YS, IEnumerable<T> ZS) Trifurcate<T>(this IEnumerable<T> source, Func<T, bool> predicate1, Func<T, bool> predicate2)
{
    var lookup = source.ToLookup(x =>
    {
        if (predicate1(x))
            return 1;

        if (predicate2(x))
            return 2;

        return 3;
    });

    return (lookup[1], lookup[2], lookup[3]);
}

The order of predicates matters with this one. If you pass n => n > 5 and n => n > 100 in that order for example, the second collection will always be empty.

One might even have an itch to come up with a version of this that would work with a variable number of predicates(I know I did) but as far as I know that's not possible with tuple return values in C#.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSurajView Question on Stackoverflow
Solution 1 - C#Mark ByersView Answer on Stackoverflow
Solution 2 - C#Amy BView Answer on Stackoverflow
Solution 3 - C#Michael FryView Answer on Stackoverflow
Solution 4 - C#Zach JohnsonView Answer on Stackoverflow
Solution 5 - C#TKharaishviliView Answer on Stackoverflow