简体   繁体   中英

Split C# collection into equal parts, maintaining sort

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

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.

    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;

This will do exactly as requested. It will also cater for uneven groupings ie 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

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

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.

    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));
    }

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;
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM