Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c#

linq

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.

like image 244
Pierre-Alain Vigeant Avatar asked Mar 02 '11 19:03

Pierre-Alain Vigeant


4 Answers

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.

like image 69
Mark Byers Avatar answered Oct 04 '22 00:10

Mark Byers


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)
like image 32
Dan Tao Avatar answered Oct 04 '22 00:10

Dan Tao


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).

like image 44
Donut Avatar answered Oct 03 '22 23:10

Donut


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.

like image 37
Jon Skeet Avatar answered Oct 04 '22 00:10

Jon Skeet