13

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 them incorrectly. Basically, if I was to look at the items in the collection, the order should be the same when compared to the broken up collections joined. Here is the code I am using that doesn't work:

public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> list, int parts)
        {
            int i = 0;
            var splits = from name in list
                         group name by i++ % parts into part
                         select part.AsEnumerable();
            return splits;
        }
  • int parts = number of sub enumerables
Brian David Berman
  • 7,514
  • 26
  • 77
  • 144
  • possible duplicate of [LINQ Partition List into Lists of 8 members.](http://stackoverflow.com/questions/3773403/linq-partition-list-into-lists-of-8-members) – Kirk Woll Oct 08 '10 at 17:14
  • 1
    @Kirk Woll: It is not the same, in the question you gave extension method takes max number of elements in one sub-enumerable while here, as I understand, we have desired number of sub-enumerables. – Andrew Bezzub Oct 08 '10 at 17:24
  • @Andrew, you're right, I see you're point – Kirk Woll Oct 08 '10 at 17:28
  • Can you post an example as to what you are getting and why it's wrong. Did you want to partition them like *dealing* cards from a deck or like *cutting* the deck into parts? – Ian Mercer Oct 08 '10 at 18:01
  • BTW you can use .Select ((x, i) => ...) instead of declaring i and incrementing it. See http://msdn.microsoft.com/en-us/library/bb534869.aspx – Ian Mercer Oct 08 '10 at 18:04

9 Answers9

12

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;
}
Christopher Klein
  • 2,773
  • 4
  • 39
  • 61
  • Is there not a bug with this algorithm.. Has anyone tried to verify it? Seems to me that the line int maxSize = (int)Math.Ceiling(list.Count / (double)totalPartitions) could lead to invalid maxsize counts.. For instance; a list of 21 elements to be split into say desired 10 groups; will actually only create 7 groups of 3 elements with 3 empty groups.. reason being that the size calculation will round up from 2.1 to 3 – Dave Lawrence Nov 18 '13 at 13:39
  • Yes, I imagine that it is. If I have a list of the alphabet and I put to split that to 100 partitions I will get back an object that has the first 0-25 filled with 1 item each and 26-99 will be empty. LINQ as of 3.5 will probably correct the issue, I just haven't looked at this in a few years. It did what I needed it to do, and obviously what the OP needed. – Christopher Klein Nov 18 '13 at 19:11
4

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));
}
ebb
  • 9,297
  • 18
  • 72
  • 123
  • 1
    This is going to create partitions that aren't as equally sized as they could be. For 7 values, the partitions will have counts of 3, 3, and 1. I need my partition counts to be 3, 2, and 2. – Dudeman3000 May 30 '20 at 23:44
2

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.

John M. Wright
  • 4,477
  • 1
  • 43
  • 61
code4life
  • 15,655
  • 7
  • 50
  • 82
  • 1
    In these examples, it seems to want "parts" to be the amount of items in each collection. In MY example, I want "parts" to be the number of sub collections to create. – Brian David Berman Oct 08 '10 at 17:55
1

To split a generic list in to equal chunks use below generic method

 private IEnumerable<IEnumerable<T>> SplitMaintainingOrder<T>(IEnumerable<T> list, int columnCount)
                {
                    var elementsCount = list.Count();
                    int rowCount = elementsCount / columnCount;
                    int noOfCells = elementsCount % columnCount;

                    int finalRowCount = rowCount;
                    if (noOfCells > 0)
                    {
                        finalRowCount++;
                    }

                    var toreturn = new List<IEnumerable<T>>();
                    var pushto = 0;
                    for (int j = 0; j < columnCount; j++)
                    {
                        var start = j;
                        int i = 0;
                        var end = i;
                        for (i = 0; i < finalRowCount; i++)
                        {
                            if ((i < rowCount) || ((i == rowCount) && (j < noOfCells)))
                            {
                                start = j;
                                end = i;
                            }
                        }
                        toreturn.Add(list.Skip(pushto).Take(end + 1));
                        pushto += end + 1;
                    }

                    return toreturn;

                }

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

var splitedItems = SplitMaintainingOrder<int>(recordNumbers , 4);

Output will be:

List 1 : 1,2,3
List 2 : 4,5,6
List 3 : 7,8,9
List 4 : 10,11

~Happy coding..

  • I feel that this answer is more adequate since it evens out the sub lists, so if you intend to use this for a load balance for worker threads, for example, you will not have idle workers depending on your numbers. – marceloatg Aug 31 '22 at 23:51
1
    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;
kevev22
  • 3,737
  • 21
  • 32
0

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
Dave Lawrence
  • 3,843
  • 2
  • 21
  • 35
0

As I understand you want to break enumerable on several parts with equal size and without breaking the order of your elements. It looks like the only choice is to get the length of your input enumerable first, so you would need at least two iterations through the enumerable.

Andrew Bezzub
  • 15,744
  • 7
  • 51
  • 73
  • @Kirk Woll: it depends on what is needed. If "parts" is the number of elements in one enumerable than it is easy to solve. But code above makes me think that "parts" is number of desired sub-enumerables, not elements in the enumerables. – Andrew Bezzub Oct 08 '10 at 17:28
  • "parts" is the number of sub-enumerables – Brian David Berman Oct 08 '10 at 17:44
0
    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));
    }
Grozz
  • 8,317
  • 4
  • 38
  • 53
0

The code provided in the first answer splits a group of 21 elements to 10 with the following number of elements in the partitions: 3, 3, 3, 3, 3, 3, 3, 0, 0, 0

The wanted element number is: 3, 2, 2, 2, 2, 2, 2, 2, 2, 2

I made a small change in the provided code to give the wanted partition sizes

    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 k = 0;

        for (int i = 0; i < partitions.Length; i++)
        {
            int maxSize = (int)Math.Ceiling( (list.Count - i) / (double)totalPartitions);
            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;
    }
skobin
  • 1