13

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?

Johan Wintgens
  • 529
  • 7
  • 15
  • 3
    In your case I would avoid using LINQ. Iterate manually instead to distribute your events to the two target lists – schlonzo Jan 10 '19 at 13:31
  • 4
    You can use `GroupBy` – CornelC Jan 10 '19 at 13:32
  • can you have two methods instead of one to get your events. so one called `GetOpenEvents()` and one `GetClosedEvents()`, it's cleaner and is more SRP. – MattjeS Jan 10 '19 at 13:34
  • H.Mikhaeljan, I am looking to get 2 separate lists depending on whether or not the condition is *true*, rather than getting one grouped list. – Johan Wintgens Jan 11 '19 at 09:43
  • MattjeS, I could indeed, but that is not the question. I do need the three lists in my code for various operations, and with GetAllEvents() having to query the database, the performance of also getting open and closed events from the database separately would imply a far greater performance hit than the code I have right now. Thank you for your input, though! – Johan Wintgens Jan 11 '19 at 09:46

5 Answers5

17

You can use ToLookup extension method as follows:

 List<Foo> items = new List<Foo> { new Foo { Name="A",Condition=true},new Foo { Name = "B", Condition = true },new Foo { Name = "C", Condition = false } };

  var lookupItems = items.ToLookup(item => item.Condition);
        var lstTrueItems = lookupItems[true];
        var lstFalseItems = lookupItems[false];
TanvirArjel
  • 30,049
  • 14
  • 78
  • 114
Amin Sahranavard
  • 268
  • 1
  • 10
  • This is the best LINQ approach but you should use OP's condition: `var isOpenEventsLookup = events.ToLookup(ev => ev.Closer_User_ID == null);` and use `isOpenEventsLookup[true].ToList()` to get the lists – Tim Schmelter Jan 10 '19 at 14:07
10

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

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • Wow, thank you for such an elaborate answer! Although it seems a bit extensive (pun intended) for what I am trying to achieve, it certainly is a very good answer to the question, providing very interesting information and an in-depth explanation of what your code is doing exactly. Thank you very much, kind sir! – Johan Wintgens Jan 11 '19 at 10:12
  • While the first approach was also given by Amin Sahranavard, I'll mark this answer as the solution, as it provides a lot more information and suggestions along the way. – Johan Wintgens Jan 16 '19 at 10:37
5

GroupBy and Single should accomplish what you're looking for:

var groups = events.GroupBy(e => e.Closer_User_ID == null).ToList(); // As others mentioned this needs to be materialized to prevent `events` from being iterated twice.
var openEvents = groups.SingleOrDefault(grp => grp.Key == true)?.ToList() ?? new List<EventModel>();
var closedEvents = groups.SingleOrDefault(grp => grp.Key == false)?.ToList() ?? new List<EventModel>();
Neil
  • 1,613
  • 1
  • 16
  • 18
4

One line solution by using ForEach method of List:

List<EventModel> events = GetAllEvents();

List<EventModel> openEvents = new List<EventModel>();
List<EventModel> closedEvents = new List<EventModel>();

events.ForEach(x => (x.Closer_User_ID == null ? openEvents : closedEvents).Add(x));
3

You can do without LINQ. Switch to conventional loop approach.

List<EventModel> openEvents = new List<EventModel>();
List<EventModel> closedEvents = new List<EventModel>();

foreach(var e in  events)
{
  if(e.Closer_User_ID == null)
  {
    openEvents.Add(e);
  }
  else
  {
    closedEvents.Add(e);
  }
}
Anu Viswan
  • 17,797
  • 2
  • 22
  • 51
  • 2
    I could indeed, but that's no answer to the question 'Can I do this using LINQ without iterating the main list twice?'. Thank you for the input, though! – Johan Wintgens Jan 11 '19 at 09:52