10

I need to loop through an entire list of users, but need to grab 20 at a time.

foreach (var student in Class.Students.Take(20))
{
   Console.WriteLine("You belong to Group " + groupNumber);
   groupNumber++;
}

This way the first 20 will belong to Group 1, the second 20 to Group 2, and so on.

Is Take the correct syntax for this? I believe Take will take 20 then be done. Thanks!

Hairgami_Master
  • 5,429
  • 10
  • 45
  • 66

3 Answers3

17

You can do something like this:

int i = 0;
foreach (var grouping in Class.Students.GroupBy(s => ++i / 20))
    Console.WriteLine("You belong to Group " + grouping.Key.ToString());
MikeP
  • 7,829
  • 33
  • 34
  • Thanks MikeP! That's very helpful. If I needed to assign a particular value to each student in a group, how would I do that? I feel I'm going to have to do a nested for each at some point. – Hairgami_Master Jul 27 '11 at 19:39
  • The grouping object is also enumerable for the elements grouped into it. So just a foreach(var student in grouping) should do the trick. – MikeP Jul 27 '11 at 20:11
  • 2
    Doesn't this code have an off-by-one error because of the pre-increment? In essence, you're iterating starting from `i = 1` instead of 0. For example, if you had a group size of 2, then the first element of the enumerable would be in the first grouping all by itself, since `(0 + 1) / 2 == 0` but `(1 + 1) / 2 == 1`. – voithos Sep 12 '13 at 17:30
11

For a similar problem I once made an extension method:

public static IEnumerable<IEnumerable<T>> ToChunks<T>(this IEnumerable<T> enumerable, int chunkSize)
{
    int itemsReturned = 0;
    var list = enumerable.ToList(); // Prevent multiple execution of IEnumerable.
    int count = list.Count;
    while (itemsReturned < count)
    {
        int currentChunkSize = Math.Min(chunkSize, count - itemsReturned);
        yield return list.GetRange(itemsReturned, currentChunkSize);
        itemsReturned += currentChunkSize;
    }
}

Note that the last chunk may be smaller than the specified chunk size.

EDIT The first version used Skip()/Take(), but as Chris pointed out, GetRange is considerably faster.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
  • 2
    GetRange is 4000+ times faster than Skip/Take – Chris Gessler Jul 23 '12 at 10:14
  • 2
    Hey, thanks @ChrisGessler. It's embarrassing to find oneself overlooking the existence of a "good old" method that does a better job (while I always say to myself that linq is not the answer to everything). In a benchmark with large numbers and non-primitive objects I found a 10+ improvement of GetRange over `Skip()/Take()`. Good call. – Gert Arnold Jul 23 '12 at 14:14
  • @GertArnold It's a really old answer but can you also provide an async version of this as response to my question: https://stackoverflow.com/questions/66321310/convert-non-async-linq-extension-method-to-async – tunafish24 Feb 22 '21 at 20:16
0

You could project the group number onto each item in the original list using Select and an anonymous type like this:

var assigned = Class.Students.Select(
  (item, index) => new 
  { 
    Student = item, 
    GroupNumber = index / 20 + 1 
  });

Then you can use it like this:

foreach (var item in assigned)
{
  Console.WriteLine("Student = " + item.Student.Name);
  Console.WriteLine("You belong to Group " + item.GroupNumber.ToString());
}

Or if you wanted to pick off a particular group.

foreach (var student in assigned.Where(x => x.GroupNumber == 5))
{
  Console.WriteLine("Name = " + student.Name);
}

Or if you wanted to actually group them to perform aggregates

foreach (var group in assigned.GroupBy(x => x.GroupNumber))
{
  Console.WriteLine("Average age = " + group.Select(x => x.Student.Age).Average().ToString());
}

Now if you just want to lump the items in batches and you do not care about having the batch number information you can create this simple extension method.

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> target, int size)
{
  var assigned = target.Select(
      (item, index) => new
      {
          Value = item,
          BatchNumber = index / size
      });
  foreach (var group in assigned.GroupBy(x => x.BatchNumber))
  {
      yield return group.Select(x => x.Value);
  }
}

And you could use your new extension method like this.

foreach (var batch in Class.Students.Batch(20))
{
  foreach (var student in batch)
  {
    Console.WriteLine("Student = " + student.Name);
  }
}
Brian Gideon
  • 47,849
  • 13
  • 107
  • 150
  • You made the same mistake as many other posters who have since deleted their responses. You don't take 20 items in a row; each of the first twenty students will all be in a different group! – MikeP Jul 27 '11 at 22:51
  • Additionally, you're not actually grouping anything either. If you want to iterate the students in a group, your method leaves you high and dry. – MikeP Jul 27 '11 at 22:51
  • @MikeP: Yep, nice catch. Fixed. I also renamed `grouped` to `assigned` to better reflect what actually happens. If the OP really wanted to iterate students from a particular group you could do `assigned.Where(x => x.GroupNumber == value)` or `assigned.GroupBy(x => x.GroupNumber)`, though that doesn't look necessary based on what I see in the question. I guess one fun aspect of LINQ is that there are a lot of ways of doing the same thing. Anyway, yeah, I totally choked on the `%` thing instead of `/`. – Brian Gideon Jul 28 '11 at 02:45