Pierre-Alain Vigeant
Pierre-Alain Vigeant

Reputation: 23093

Divide a large IEnumerable into smaller IEnumerable of a fix amount of item

In order to support an API that only accepts a specific amount of items (5 items), I want to transform a LINQ result into smaller groups of items that always contain that set amount of items.

Supposing the list {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}

I want to get three smaller lists of a maximum of 5 items each

{1, 2, 3, 4, 5}

{6, 7, 8, 9, 10}

{11, 12, 13, 14, 15}

{16, 17, 18}

How can I do that with LINQ? I'm assuming that it either involves Group or Aggregate, but I'm having trouble figuring how to write that.

Upvotes: 7

Views: 8864

Answers (5)

Jon Skeet
Jon Skeet

Reputation: 1501163

We have a Batch method in MoreLINQ. You need to be careful how you use it, as the batch that is passed to the selector each time is a reference to the same array - but it does work.

You can use GroupBy, but that can't be lazy - it has to accumulate all the results before it can return anything. That may be okay for you, but it's worth being aware of.

Upvotes: 1

Dan Tao
Dan Tao

Reputation: 128347

I'd just do something like this:

public static IEnumerable<IEnumerable<T>> TakeChunks<T>(this IEnumerable<T> source, int size)
{
    // Typically you'd put argument validation in the method call and then
    // implement it using a private method... I'll leave that to your
    // imagination.

    var list = new List<T>(size);

    foreach (T item in source)
    {
        list.Add(item);
        if (list.Count == size)
        {
            List<T> chunk = list;
            list = new List<T>(size);
            yield return chunk;
        }
    }

    if (list.Count > 0)
    {
        yield return list;
    }
}

Usage:

var list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

foreach (var chunk in list.TakeChunks(3))
{
    Console.WriteLine(string.Join(", ", chunk));
}

Output:

1, 2, 3
4, 5, 6
7, 8, 9
10

Rationale:

Compared to other methods such multiple calls to Skip and Take or a big fancy LINQ query, the above is:

  • More efficient
  • More obvious in function (in my opinion)
  • More readable in implementation (again, in my opinion)

Upvotes: 14

Dennis Traub
Dennis Traub

Reputation: 51644

var list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
var result = new List<List<int>>();
while (list.Count != 0) {
    result.Add(list.TakeWhile(x => x++ <= 5).ToList());
    list.RemoveRange(0, list.Count < 5 ? list.Count : 5);
}

Upvotes: 0

Donut
Donut

Reputation: 112835

One easy possibility is to use the Enumerable.Skip and Enumerable.Take methods, for example:

List<int> nums = new List<int>(){1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18};

var list1 = nums.Take(5);
var list2 = nums.Skip(5).Take(5);
var list3 = nums.Skip(10).Take(5);
var list4 = nums.Skip(15).Take(5);

As Jon mentioned in the comments though, a simple approach like this one will re-evaluate nums (in this example) each time, which will impact performance (depending on the size of the collection).

Upvotes: 4

Mark Byers
Mark Byers

Reputation: 838416

Try something like this:

var result = items.Select((value, index) => new { Index = index, Value = value})
                  .GroupBy(x => x.Index / 5)
                  .Select(g => g.Select(x => x.Value).ToList())
                  .ToList();

It works by partitioning the items into groups based on their index in the original list.

Upvotes: 17

Related Questions