Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# How to split a List in two using LINQ [duplicate]

I am trying to split a List into two Lists using LINQ without iterating the 'master' list twice. One List should contain the elements for which the LINQ condition is true, and the other should contain all the other elements. Is this at all possible?

Right now I just use two LINQ queries, thus iterating the (huge) master List twice.

Here's the (pseudo) code I am using right now:

List<EventModel> events = GetAllEvents();

List<EventModel> openEvents = events.Where(e => e.Closer_User_ID == null);
List<EventModel> closedEvents = events.Where(e => e.Closer_User_ID != null);

Is it possible to yield the same results without iterating the original List twice?

like image 482
Johan Wintgens Avatar asked Jan 10 '19 13:01

Johan Wintgens


1 Answers

You can do this in one statement by converting it into a Lookup table:

var splitTables = events.Tolookup(event => event.Closer_User_ID == null);

This will return a sequence of two elements, where every element is an IGrouping<bool, EventModel>. The Key says whether the sequence is the sequence with null Closer_User_Id, or not.

However this looks rather mystical. My advice would be to extend LINQ with a new function.

This function takes a sequence of any kind, and a predicate that divides the sequence into two groups: the group that matches the predicate and the group that doesn't match the predicate.

This way you can use the function to divide all kinds of IEnumerable sequences into two sequences.

See Extension methods demystified

public static IEnumerable<IGrouping<bool, TSource>> Split<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource,bool> predicate)
{
    return source.ToLookup(predicate);
}

Usage:

IEnumerable<Person> persons = ...
// divide the persons into adults and non-adults:
var result = persons.Split(person => person.IsAdult);

Result has two elements: the one with Key true has all Adults.

Although usage has now become easier to read, you still have the problem that the complete sequence is processed, while in fact you might only want to use a few of the resulting items

Let's return an IEnumerable<KeyValuePair<bool, TSource>>, where the Boolean value indicates whether the item matches or doesn't match:

public static IEnumerable<KeyValuePair<bool, TSource>> Audit<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource,bool> predicate)
{
    foreach (var sourceItem in source)
    {
        yield return new KeyValuePair<bool, TSource>(predicate(sourceItem, sourceItem));
    }
}

Now you get a sequence, where every element says whether it matches or not. If you only need a few of them, the rest of the sequence is not processed:

IEnumerable<EventModel> eventModels = ...
EventModel firstOpenEvent = eventModels.Audit(event => event.Closer_User_ID == null)
    .Where(splitEvent => splitEvent.Key)
    .FirstOrDefault();

The where says that you only want those Audited items that passed auditing (key is true).

Because you only need the first element, the rest of the sequence is not audited anymore

like image 78
Harald Coppoolse Avatar answered Sep 22 '22 08:09

Harald Coppoolse