Summary#
Group a sequence and keep the groups — each one is a sequence of its elements with a Key, exactly as in C#:
class Visit { public string Room = ""; public string Who = ""; public int Minutes; }
string Summary(List<Visit> visits) {
var parts = new List<string>();
foreach (var g in visits.GroupBy(v => v.Room)) {
parts.Add(g.Key + ":" + g.Count() + ":" + g.First().Who);
}
return string.Join(",", parts);
}Groups come out in the order their keys first appear, and each group's elements stay in source order.
Signature#
IEnumerable<IGrouping<K, T>> xs.GroupBy(x => key) // groups of the elements
IEnumerable<IGrouping<K, E>> xs.GroupBy(x => key, x => element) // groups of a projection of them
K g.Key // the key every element of the group shares
T g.First() / g.Last() / g.FirstOrDefault(x => p) // one element of the group
ILookup<K, T> xs.ToLookup(x => key[, x => element]) // the same groups, held by key
IEnumerable<T> lookup[key] // that key's elements — EMPTY when absent
int lookup.Count // how many keys
bool lookup.Contains(key) // does it hold that key?Description#
What a group is#
A group is a list of its elements that carries one more thing: the Key they share. So every list verb works on it
— Count(), Sum(x => …), OrderBy, First(), foreach — and g.Key answers the key. A variable can hold one:
IGrouping<string, Visit> g is a type you can write.
That makes the ordinary C# shapes work, with C#'s results:
List<string> FirstPerRoom(List<Visit> visits) =>
visits.GroupBy(v => v.Room).Select(g => g.First().Who).ToList();
int RoomsVisitedTwice(List<Visit> visits) =>
visits.GroupBy(v => v.Room).Where(g => g.Count() > 1).Count();
Dictionary<string, int> MinutesPerRoom(List<Visit> visits) =>
visits.GroupBy(v => v.Room).ToDictionary(g => g.Key, g => g.Sum(v => v.Minutes));Over rows from the data store#
A grouping that reduces each group to one row of key and aggregates — GroupBy(k).Select(g => new T { … g.Key, g.Count(), g.Sum(…) }) — is a real GROUP BY in the database: see GroupBy (and HAVING). Anything else you do with a
group needs the group's ROWS — its first element, the group itself, a foreach over it — so the rows the query
selects are read and then grouped, as C#'s LINQ would group them. A Where before the GroupBy still filters in the
database, so only those rows travel:
entity Climb {
[Required, MaxLength(40)] string Wall;
[Required, MaxLength(40)] string Name;
int Grade;
}
List<string> HardestPerWall() =>
Climb.Where(c => c.Grade > 0)
.GroupBy(c => c.Wall)
.Select(g => g.Key + "=" + g.OrderByDescending(c => c.Grade).First().Name)
.ToList();One element per group reads one or two rows a group#
When the projection uses the group only through g.Key and one pick — g.First(), g.Last(),
g.FirstOrDefault(), g.LastOrDefault(), optionally after g.OrderBy(…)/ThenBy(…) — the database returns only
the rows that decide each group: its first row in source order, which places the group, and the row the pick chooses.
HardestPerWall above reads two rows a wall however many climbs each wall holds. The answer is the one C# gives over
every row — ties go to the earlier row, as a stable sort leaves them — and an OrderBy before the GroupBy sets that
source order:
List<string> EasiestPerWall() =>
Climb.OrderByDescending(c => c.Grade)
.GroupBy(c => c.Wall)
.Select(g => g.Key + ":" + g.Last().Name)
.ToList();While the current unit of work holds an unsaved change to those rows, every row is read and grouped instead, so the answer includes what it has not saved yet.
ToLookup — the groups, held by key#
ToLookup makes the same groups and holds them by key. Unlike a Dictionary, indexing it with a key it does not
hold is not an error: it answers an empty sequence. Enumerating it yields its groups, in first-appearance order.
string Visitors(List<Visit> visits, string room) {
ILookup<string, string> byRoom = visits.ToLookup(v => v.Room, v => v.Who);
if (!byRoom.Contains(room)) return "nobody (" + byRoom.Count + " rooms visited)";
return string.Join(", ", byRoom[room]);
}A custom key comparer is not supported; normalise the key instead (v => v.Room.ToUpperInvariant()).
See also#
- GroupBy (and HAVING) — one row per group, computed by the database (and
Take(n)on it) - LINQ over a local list — every other LINQ verb over a list you hold
- Sequence fields on a class —
List<T>,Dictionary<K, V>and the other collections a group can become