Limit number of items returned by Linq GroupBy

Viewed 104

I have a List of stuff that I need to group, but I need to limit the number of items in each grouping to a certain number. (i.e. using the list below, I want to group the items by last name, but I only want a maximum of two items in a group, with any left over in another grouping)

I think I am either missing something basic, or I am barking up the wrong tree and need to refactor how I want to do things.

 public class Student
 {
   public string First { get; set; }
   public string Last { get; set; }
   public int ID { get; set; }
   public List<int> Scores;
 }

List<Student> students = new List<Student>
{
  new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= new List<int> {97, 72, 81, 60}},
  new Student {First="Claire", Last="Smith", ID=112, Scores= new List<int> {75, 84, 91, 39}},
  new Student {First="Sven", Last="Mortensen", ID=113, Scores= new List<int> {99, 89, 91, 95}},
  new Student {First="Cesar", Last="Garcia", ID=114, Scores= new List<int> {72, 81, 65, 84}},
  new Student {First="Debra", Last="Garcia", ID=115, Scores= new List<int> {97, 89, 85, 82}},
  new Student {First="Bob", Last="Garcia", ID=116, Scores= new List<int> {97, 89, 85, 82}}
};

I can group by last name easily enough:

var g = students.GroupBy(m => m.Last);

foreach (var gp in g)
{
  Console.WriteLine($"- Last Name: {gp.Key}");
  foreach (var o in gp) {
    Console.WriteLine($"     {o.First} {o.ID}");
  }
}

Which gets me

- Last Name: Omelchenko
     Svetlana 111
- Last Name: Smith
     Claire 112
- Last Name: Mortensen
     Sven 113
- Last Name: Garcia
     Cesar 114
     Debra 115
     Bob 116

But I want to be able to limit the maximum size of the groupings to 2, which means I want:

- Last Name: Omelchenko
     Svetlana 111
- Last Name: Smith
     Claire 112
- Last Name: Mortensen
     Sven 113
- Last Name: Garcia
     Cesar 114
     Debra 115
- Last Name: Garcia
     Bob 116

Note that this is a contrived example, my dataset size is considerably larger.

1 Answers

You could use following approach that materializes the students of each group into a List<Stundent>(which might be a problem if you have millions).

const int maxLength = 2;
var nameStudenLists = new Dictionary<string, List<List<Student>>>();
var g = students.GroupBy(m => m.Last);
foreach(var kv in g)
{
    bool contains = nameStudenLists.TryGetValue(kv.Key, out List<List<Student>> lists);
    List<List<Student>> studentLists = contains ? lists : new List<List<Student>>();
    List<Student> allStudents = kv.ToList();
    if(allStudents.Count <= maxLength)
    {
        studentLists.Add(allStudents);
        nameStudenLists.Add(kv.Key, studentLists);
        continue;
    }
    
    for(int i = 0; i <= maxLength; i+=maxLength)
    {
        studentLists.Add(allStudents.GetRange(i, Math.Min(maxLength, allStudents.Count - i)));
    }
    nameStudenLists.Add(kv.Key, studentLists);
}

The result is Dictionary<string, List<List<Student>>>() which seems to be the perfect collection for your requirement. The key is the name and the value are the sublists with max 2 students:

foreach (var gp in nameStudenLists)
{
    Console.WriteLine($"- Last Name: {gp.Key}");
    foreach (List<Student> students in gp.Value)
    {
        Console.WriteLine($"List-Size: {students.Count}");
        Console.WriteLine(string.Join(",", students.Select(s => $"{s.First} {s.Last}")));
    }
}
Related