1

I have a query which I get as:

var query = Data.Items
            .Where(x => criteria.IsMatch(x))
            .ToList<Item>();

This works fine.

However now I want to break up this list into x number of lists, for example 3. Each list will therefore contain 1/3 the amount of elements from query.

Can it be done using LINQ?

Ivan-Mark Debono
  • 15,500
  • 29
  • 132
  • 263
  • 2
    take a look at (and possible duplicate of) http://stackoverflow.com/questions/419019/split-list-into-sublists-with-linq – gunr2171 Oct 11 '13 at 15:33
  • MoreLinq has a [`Batch`](http://code.google.com/p/morelinq/source/browse/MoreLinq/Batch.cs?r=f85495b139a19bce7df2be98ad88754ba8932a28) extension method designed to do exactly this. EDIT: Ooops, nevermind. Got it backwards. It defines the _size_ of each list, not the number of lists. Sorry. – Chris Sinclair Oct 11 '13 at 16:01
  • @ChrisSinclair Not quite. `Batch` separates the query into an unknown number of groups, all of size n. He wants to separate the query into m groups all of an unknown size. – Servy Oct 11 '13 at 16:03
  • How do you want to size your groupings if you supply a non-divisible number (like 10 into 3), you would want your groupings to be of lengths `{4, 3, 3}`, `{3, 3, 4}`, or something else? – Chris Sinclair Oct 11 '13 at 16:08

5 Answers5

2

I think something like this could work, splitting the list into IGroupings.

const int numberOfGroups = 3;

var groups = query
    .Select((item, i) => new { item, i })
    .GroupBy(e => e.i % numberOfGroups);
Daniel Imms
  • 47,944
  • 19
  • 150
  • 166
  • Ah yea, `%` should be used so the group by will be in range `0..(numberOfGroups-1)` – Daniel Imms Oct 11 '13 at 15:41
  • 1
    The problem of `%` approach is that produces sublists of non-consecutive elements: for instance if you have 12 elements you get `{0,3,6,9} - {1,4,7,10} - {2,5,8,11}` – digEmAll Oct 11 '13 at 16:04
2

You can use PLINQ partitioners to break the results into separate enumerables.

var partitioner = Partitioner.Create<Item>(query);
var partitions = partitioner.GetPartitions(3);

You'll need to reference the System.Collections.Concurrent namespace. partitions will be a list of IEnumerable<Item> where each enumerable returns a portion of the query.

William
  • 1,867
  • 11
  • 10
2

You can use Skip and Take in a simple for to accomplish what you want

   var groupSize = (int)Math.Ceiling(query.Count() / 3d);
   var result = new List<List<Item>>();
   for (var j = 0; j < 3; j++)
      result.Add(query.Skip(j * groupSize).Take(groupSize).ToList());
Esteban Elverdin
  • 3,552
  • 1
  • 17
  • 21
1

If the order of the elements doesn't matter using an IGrouping as suggested by Daniel Imms is probably the most elegant way (add .Select(gr => gr.Select(e => e.item)) to get an IEnumerable<IEnumerable<T>>).

If however you want to preserve the order you need to know the total number of elements. Otherwise you wouldn't know when to start the next group. You can do this with LINQ but it requires two enumerations: one for counting and another for returning the data (as suggested by Esteban Elverdin).

If enumerating the query is expensive you can avoid the second enumeration by turning the query into a list and then use the GetRange method:

public static IEnumerable<List<T>> SplitList<T>(List<T> list, int numberOfRanges)
{
    int sizeOfRanges = list.Count / numberOfRanges;
    int remainder = list.Count % numberOfRanges;

    int startIndex = 0;

    for (int i = 0; i < numberOfRanges; i++)
    {
        int size = sizeOfRanges + (remainder > 0 ? 1 : 0);
        yield return list.GetRange(startIndex, size);

        if (remainder > 0)
        {
            remainder--;
        }

        startIndex += size;
    }
}

static void Main()
{
    List<int> list = Enumerable.Range(0, 10).ToList();

    IEnumerable<List<int>> result = SplitList(list, 3);

    foreach (List<int> values in result)
    {
        string s = string.Join(", ", values);
        Console.WriteLine("{{ {0} }}", s);
    }
}

The output is:

{ 0, 1, 2, 3 }
{ 4, 5, 6 }
{ 7, 8, 9 }
pescolino
  • 3,086
  • 2
  • 14
  • 24
0

You can create an extension method:

public static IList<List<T>> GetChunks<T>(this IList<T> items, int numOfChunks)
{
    if (items.Count < numOfChunks)
        throw new ArgumentException("The number of elements is lower than the number of chunks");
    int div = items.Count / numOfChunks;
    int rem = items.Count % numOfChunks;

    var listOfLists = new List<T>[numOfChunks];

    for (int i = 0; i < numOfChunks; i++)
        listOfLists[i] = new List<T>();

    int currentGrp = 0;
    int currRemainder = rem;
    foreach (var el in items)
    {
        int currentElementsInGrp = listOfLists[currentGrp].Count;
        if (currentElementsInGrp == div && currRemainder > 0)
        {
            currRemainder--;
        }
        else if (currentElementsInGrp >= div)
        {
            currentGrp++;
        }
        listOfLists[currentGrp].Add(el);
    }
    return listOfLists;
}

then use it like this :

var chunks = query.GetChunks(3);

N.B.

in case of number of elements not divisible by the number of groups, the first groups will be bigger. e.g. [0,1,2,3,4] --> [0,1] - [2,3] - [4]

digEmAll
  • 56,430
  • 9
  • 115
  • 140