I am trying to split a collection into multiple collections while maintaining a sort I have on the collection. I have tried using the following extension method, but it breaks
This will do exactly as requested. It will also cater for uneven groupings i.e. 27 elements in to 10 groups will yield 7 groups of three and 3 groups of two
public static IEnumerable<IEnumerable<T>> SplitMaintainingOrder<T>(this IEnumerable<T> list, int parts)
{
if (list.Count() == 0) return Enumerable.Empty<IEnumerable<T>>();
var toreturn = new List<IEnumerable<T>>();
var splitFactor = Decimal.Divide((decimal)list.Count(), parts);
int currentIndex = 0;
for (var i = 0; i < parts; i++)
{
var toTake = Convert.ToInt32(
i == 0 ? Math.Ceiling(splitFactor) : (
(Decimal.Compare(Decimal.Divide(Convert.ToDecimal(currentIndex), Convert.ToDecimal(i)), splitFactor) > 0) ?
Math.Floor(splitFactor) : Math.Ceiling(splitFactor)));
toreturn.Add(list.Skip(currentIndex).Take(toTake));
currentIndex += toTake;
}
return toreturn;
}
For demo purposes
[TestMethod]
public void splitlist()
{
var list = new decimal[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27 };
var splitlists = list.SplitMaintainingOrder(10);
int i = 1;
foreach (var group in splitlists)
{
Console.WriteLine("Group {0} elements {1}", i++, String.Join(",", group));
}
}
the above demo yields
Test Name: splitlist
Test Outcome: Passed
Result StandardOutput:
Group 1 elements 1,2,3
Group 2 elements 4,5
Group 3 elements 6,7,8
Group 4 elements 9,10,11
Group 5 elements 12,13
Group 6 elements 14,15,16
Group 7 elements 17,18,19
Group 8 elements 20,21
Group 9 elements 22,23,24
Group 10 elements 25,26,27
I had to make use of this to compare a list of objects to one another in groups of 4... it will keep the objects in the order that the original possessed. Could be expanded to do something other than 'List'
/// <summary>
/// Partition a list of elements into a smaller group of elements
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list"></param>
/// <param name="totalPartitions"></param>
/// <returns></returns>
public static List<T>[] Partition<T>(List<T> list, int totalPartitions)
{
if (list == null)
throw new ArgumentNullException("list");
if (totalPartitions < 1)
throw new ArgumentOutOfRangeException("totalPartitions");
List<T>[] partitions = new List<T>[totalPartitions];
int maxSize = (int)Math.Ceiling(list.Count / (double)totalPartitions);
int k = 0;
for (int i = 0; i < partitions.Length; i++)
{
partitions[i] = new List<T>();
for (int j = k; j < k + maxSize; j++)
{
if (j >= list.Count)
break;
partitions[i].Add(list[j]);
}
k += maxSize;
}
return partitions;
}
A slightly more clean LINQ approach, for this rather old question:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int n)
{
var count = source.Count();
return source.Select((x, i) => new { value = x, index = i })
.GroupBy(x => x.index / (int)Math.Ceiling(count / (double)n))
.Select(x => x.Select(z => z.value));
}
Jon Skeet's MoreLINQ library might do the trick for you:
https://code.google.com/p/morelinq/source/browse/MoreLinq/Batch.cs
var items = list.Batch(parts); // gives you IEnumerable<IEnumerable<T>>
var items = list.Batch(parts, seq => seq.ToList()); // gives you IEnumerable<List<T>>
// etc...
Another example:
public class Program
{
static void Main(string[] args)
{
var list = new List<int>();
for (int i = 1; i < 10000; i++)
{
list.Add(i);
}
var batched = list.Batch(681);
// will print 15. The 15th element has 465 items...
Console.WriteLine(batched.Count().ToString());
Console.WriteLine(batched.ElementAt(14).Count().ToString());
Console.WriteLine();
Console.WriteLine("Press enter to exit.");
Console.ReadLine();
}
}
When I scanned the contents of the batches, the ordering was preserved.
public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> list, int parts)
{
int nGroups = (int)Math.Ceiling(list.Count() / (double)parts);
var groups = Enumerable.Range(0, nGroups);
return groups.Select(g => list.Skip(g * parts).Take(parts));
}
double partLength = list.Count() / (double)parts;
int i = 0;
var splits = from name in list
group name by Math.Floor((double)(i++ / partLength)) into part
select part;