2

I want to group objects by a boolean value, and I need to always get two groups (one for true, one for false), no matter if there are any elements in them.

The usual approach using GroupBy does not work, as it will only generate nonempty groups. Take e.g. this code:

var list = new List<(string, bool)>();
list.Add(("hello", true));
list.Add(("world", false));
var grouping = list.GroupBy(i => i.Item2);
var allTrue = grouping.Last();
var allFalse = grouping.First();

This only works if there is at least one element per boolean value. If we remove one of the Add lines, or even both, allTrue and allFalse will not contain the correct groups. If we remove both, we even get a runtime exception trying to call Last() ("sequence contains no elements").

Note: I want to do this lazily. (Not: Create two empty collections, iterate over the input, fill the collections.)

Kjara
  • 2,504
  • 15
  • 42
  • Can you use `where`? `var allTrue = list.where(i => i.Item2)` – Hans Kilian Sep 15 '20 at 10:51
  • Somewhat related: [Can I split an IEnumerable into two by a boolean criteria without two queries?](https://stackoverflow.com/questions/4549339/can-i-split-an-ienumerable-into-two-by-a-boolean-criteria-without-two-queries) – Theodor Zoulias Mar 12 '22 at 00:22

2 Answers2

3

The .NET platform does not contain a built-in way to produce empty IGroupings. There is no publicly accessible class that implements this interface, so we will have to create one manually:

class EmptyGrouping<TKey, TElement> : IGrouping<TKey, TElement>
{
    public TKey Key { get; }

    public EmptyGrouping(TKey key) => Key = key;

    public IEnumerator<TElement> GetEnumerator()
        => Enumerable.Empty<TElement>().GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator()
        => GetEnumerator();
}

In order to check if all required groupings are available, we will need a way to compare them based on their Key. Below is a simple IEqualityComparer implementation for IGroupings:

public class GroupingComparerByKey<TKey, TElement>
    : IEqualityComparer<IGrouping<TKey, TElement>>
{
    public bool Equals(IGrouping<TKey, TElement> x, IGrouping<TKey, TElement> y)
        => EqualityComparer<TKey>.Default.Equals(x.Key, y.Key);

    public int GetHashCode(IGrouping<TKey, TElement> obj)
        => obj.Key.GetHashCode();
}

With this infrastructure in place, we can now create a lazy LINQ operator that appends missing groupings to enumerables. Lets call it EnsureContains:

public static IEnumerable<IGrouping<TKey, TElement>> EnsureContains<TKey, TElement>(
    this IEnumerable<IGrouping<TKey, TElement>> source, params TKey[] keys)
{
    return source
        .Union(keys.Select(key => new EmptyGrouping<TKey, TElement>(key)),
            new GroupingComparerByKey<TKey, TElement>());
}

Usage example:

var groups = list
    .GroupBy(i => i.Item2)
    .EnsureContains(true, false);

Note: The enumerable produced by the GroupBy operator is lazy, so it is evaluated every time is used. Evaluating this operator is relatively expensive, so it is a good idea to avoid evaluating it more than once.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

You could ensure you get empty collections when there are no matching objects like this:

var list = new List<(string, bool)>();
list.Add(("hello", true));
list.Add(("world", false));
var allTrue = list.Where(x => x.Item2);
var allFalse = list.Where(x => !x.Item2);
Chris Pickford
  • 8,642
  • 5
  • 42
  • 73
  • Can you give me a reference where it is said that `GroupBy` is eager? Couldn't find it in microsoft docs. If that is true, then my question is lapsed. – Kjara Sep 15 '20 at 10:56
  • `List` is an implementation of `IEnumerable`, which is an in-memory collection. Are you confusing `IEnumerable` with `IQueryable`? The latter provides lazy operations until materialized using `ToList()` or similar. – Chris Pickford Sep 15 '20 at 10:58
  • I don't understand what `List` has to do with my question. The goal is to get some lazily evaluable `IEnumerable`s `allTrue` and `allFalse`, and I was assuming `GroupBy` works lazily. Some other SO answers suggest that too (see here: https://stackoverflow.com/a/10215531/5333340) – Kjara Sep 15 '20 at 11:11
  • You've explicitly used a List in your question. Sorry for the confusion, yes, GroupBy uses deferred execution, so the results are only parsed when GetEnumerator or foreach is called against the result. In this case it isn't clear what you're doing with the results. The comment about IQueryable was in case you are using LINQ-to-SQL but didn't mention it. – Chris Pickford Sep 15 '20 at 11:22
  • 1
    You should remove this answer, the correct one is below. – MikeJ Sep 15 '20 at 13:46