17

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?

5
  • 3
    In your case I would avoid using LINQ. Iterate manually instead to distribute your events to the two target lists Commented Jan 10, 2019 at 13:31
  • 4
    You can use GroupBy Commented Jan 10, 2019 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. Commented Jan 10, 2019 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. Commented Jan 11, 2019 at 9: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! Commented Jan 11, 2019 at 9:46

5 Answers 5

21

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];
Sign up to request clarification or add additional context in comments.

1 Comment

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
11

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

2 Comments

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!
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.
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>();

10 Comments

Why Single? This would throw an exception when there is more than one event per Key
@DavidG thanks, edited. Should be SingleOrDefault as well to account for no members of either group.
@DavidG Aaaah ;-)
please correct me if am wrong, wouldn't this iterate twice ?
Actually this still iterates events twice because groups is lazy initialized so you end up doing the grouping twice. Either add a ToList or even a ToDictionary after it to materialize the values.
|
5

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));

1 Comment

Interesting approach, I will certainly check this out. I am curious about how it performs. Thanks man!
4

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);
  }
}

1 Comment

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!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.