tags:

views:

656

answers:

6

Hi,

I'd like to partition a list into a list of lists, by specifying the number of elements in each partition.

For instance, suppose I have the list {1, 2, ... 11}, and would like to partition it such that each set has 4 elements, with the last set filling as many elements as it can. The resulting partition would look like {{1..4}, {5..8}, {9..11}}

What would be an elegant way of writing this?

+5  A: 

Something like (untested air code):

IEnumerable<IList<T>> PartitionList<T>(IList<T> list, int maxCount)
{
    List<T> partialList = new List<T>(maxCount);
    foreach(T item in list)
    {
        if (partialList.Count == maxCount)
        {
           yield return partialList;
           partialList = new List<T>(maxCount);
        }
        partialList.Add(item);
    }
    if (partialList.Count > 0) yield return partialList;
}

This returns an enumeration of lists rather than a list of lists, but you can easily wrap the result in a list:

IList<IList<T>> listOfLists = new List<T>(PartitionList<T>(list, maxCount));
Joe
A: 
Lee
+12  A: 

Here is an extension method that will do what you want:

public static IEnumerable<List<T>> Partition<T>(this IList<T> source, Int32 size)
{
 for (int i = 0; i < (source.Count / size) + (source.Count % size > 0 ? 1 : 0); i++)
  yield return new List<T>(source.Skip(size * i).Take(size));
}

Edit: Here is a much cleaner version of the function:

public static IEnumerable<List<T>> Partition<T>(this IList<T> source, Int32 size)
{
 for (int i = 0; i < Math.Ceiling(source.Count / (Double)size); i++)
  yield return new List<T>(source.Skip(size * i).Take(size));
}
Andrew Hare
Just what I was about to suggest. +1 for reading my mind.
LBushkin
I think the source.Count % size isn't correct - I think you only need one extra set to fill the remaining elements? For example, for the numbers 1..15 with a set size of 4, this would produce 15/4 + 15 mod 4 = 3 + 3 groups = 6 groups, instead of just 4.
David Hodgson
Nice catch - I have fixed it.
Andrew Hare
for (int i = 0; i < source.Count; i += size) { /* ... */ }
Roger Lipscombe
An unfortunate effect of this method is that the given array is not accessibly by index. There's a method here that returns a List instead http://www.vcskicks.com/partition-list.php
George
+11  A: 

Using LINQ you could cut your groups up in a single line of code like this...

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

var groups = x.Select((i, index) => new
{
    i,
    index
}).GroupBy(group => group.index / 4, element => element.i);

You could then iterate over the groups like the following...

foreach (var group in groups)
{
    Console.WriteLine("Group: {0}", group.Key);

    foreach (var item in group)
    {
        Console.WriteLine("\tValue: {0}", item);
    }
}

and you'll get an output that looks like this...

Group: 0
        Value: 1
        Value: 2
        Value: 3
        Value: 4
Group: 1
        Value: 5
        Value: 6
        Value: 7
        Value: 8
Group: 2
        Value: 9
        Value: 10
        Value: 11
Scott Ivey
Dosn't exact meet the requirements of the question, but +1 for thinking about it a little differently.
RichardOD
RichardOD - you're right - I updated the example so that the output is a group of ints rather than a group of anon types.
Scott Ivey
I think you just blew my mind. I'm really curious to know where you learned syntax like that (I really like it). All the LINQ docs I've seen are good -- but they don't cover grouping very well.
Dan Esparza
Lots of tinkering + reading SO questions. LINQ is definitely one of my favorite new features in 3.5 - and I've learned quite a bit about it just by hanging out here. This overload for GroupBy was something that I hadn't used before - so that was new to me as well :)
Scott Ivey
+1  A: 
var yourList = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
var groupSize = 4;

// here's the actual query that does the grouping...
var query = yourList
    .Select((x, i) => new { x, i })
    .GroupBy(i => i.i / groupSize, x => x.x);

// and here's a quick test to ensure that it worked properly...
foreach (var group in query)
{
    foreach (var item in group)
    {
     Console.Write(item + ",");
    }
    Console.WriteLine();
}

If you need an actual List<List<T>> rather than an IEnumerable<IEnumerable<T>> then change the query as follows:

var query = yourList
    .Select((x, i) => new { x, i })
    .GroupBy(i => i.i / groupSize, x => x.x)
    .Select(g => g.ToList())
    .ToList();
LukeH
A: 

Or in .Net 2.0 you would do this:

 static void Main(string[] args)
 {
  int[] values = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
  List<int[]> items = new List<int[]>(SplitArray(values, 4));
 }

 static IEnumerable<T[]> SplitArray<T>(T[] items, int size)
 {
  for (int index = 0; index < items.Length; index += size)
  {
   int remains = Math.Min(size, items.Length-index);
   T[] segment = new T[remains];
   Array.Copy(items, index, segment, 0, remains);
   yield return segment;
  }
 }
csharptest.net