-1

Given a sorted list, and a variable n, I want to break up the list into n parts. With n = 3, I expect three lists, with the last one taking on the overflow.

I expect: 0,1,2,3,4,5, 6,7,8,9,10,11, 12,13,14,15,16,17

If the number of items in the list is not divisible by n, then just put the overflow (mod n) in the last list.

This doesn't work:

static class Program
{
    static void Main(string[] args)
    {
        var input = new List<double>();
        for (int k = 0; k < 18; ++k)
        {
            input.Add(k);
        }
        var result = input.Split(3);
        
        foreach (var resul in result)
        {
            foreach (var res in resul)
            {
                Console.WriteLine(res);
            }
        }
    }
}

static class LinqExtensions
{
    public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> list, int parts)
    {
        int i = 0;
        var splits = from item in list
                     group item by i++ % parts into part
                     select part.AsEnumerable();
        return splits;
    }
}
Ivan
  • 7,448
  • 14
  • 69
  • 134
  • Your requirements don't make sense to me. Let's say that I have 20 values and I'm trying to split it into 13 parts. So, the number of items in the list is not divisible by `n` and then the last list has `mod n` values in it. That means I'm outputting `{1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13, 14, 15, 16, 17, 18, 19, 20}`. Is that correct? – Enigmativity Nov 28 '22 at 03:49
  • A more common thing would to be to take pairs until needing to take singles. Like this: `{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}, {11, 12}, {13, 14}, {15}, {16}, {17}, {18}, {19}, {20}`. – Enigmativity Nov 28 '22 at 03:59
  • The list is always guaranteed to be >= 3 * n. However, if the list can't be split into n parts, then just return the list. – Ivan Nov 28 '22 at 04:12
  • So return `n` equal parts if `length % n == 0` and otherwise just return the entire list? – Enigmativity Nov 28 '22 at 04:21
  • return n equal parts if length % n == 0, otherwise add the overflow to the last list – Ivan Nov 28 '22 at 04:34
  • so if n = 3 and there are 19 items, each list would be 6 long, except the last one would be 7 long – Ivan Nov 28 '22 at 04:36
  • So, `{1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13, 14, 15, 16, 17, 18, 19, 20}` for a list of `20` items with `13` parts? – Enigmativity Nov 28 '22 at 04:37
  • list will always be > 2 * parts. If not, just return the whole list – Ivan Nov 28 '22 at 05:14
  • OK, so, for example 35 with parts 13 should be `{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, 28, 29, 30, 31, 32, 33, 34, 35}`? – Enigmativity Nov 28 '22 at 05:32
  • 1
    You should describe what "doesn't work" means. Is there an exception? A compile error? Unexpected output? – Rufus L Nov 28 '22 at 05:57

3 Answers3

2

I think you would benefit from Linq's .Chunk() method.

If you first calculate how many parts will contain the equal item count, you can chunk list and yield return each chunk, before yield returning the remaining part of list (if list is not divisible by n).

As pointed out by Enigmativity, list should be materialized as an ICollection<T> to avoid possible multiple enumeration. The materialization can be obtained by trying to cast list to an ICollection<T>, and falling back to calling list.ToList() if that is unsuccessful.

A possible implementation of your extension method is hence:

public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> list, int parts)
{
    var collection = list is ICollection<T> c
        ? c
        : list.ToList();
    
    var itemCount = collection.Count;
    
    // return all items if source list is too short to split up
    if (itemCount < parts)
    {
        yield return collection;
        yield break;
    }
    
    var itemsInEachChunk = itemCount / parts;
    
    var chunks = itemCount % parts == 0
        ? parts
        : parts - 1;
    
    var itemsToChunk = chunks * itemsInEachChunk;
    
    foreach (var chunk in collection.Take(itemsToChunk).Chunk(itemsInEachChunk))
    {
        yield return chunk;
    }
    
    if (itemsToChunk < itemCount)
    {
        yield return collection.Skip(itemsToChunk);
    }
}

Example fiddle here.

Astrid E.
  • 2,280
  • 2
  • 6
  • 17
  • Nice solution. My only concern is a "hot" enumerable can easily produce different numbers of values each time it's run. You really need to materialized the list only once. – Enigmativity Nov 28 '22 at 09:26
  • 1
    Actually, it's probably best to try to cast the `IEnumerable` to `ICollection` and then the current solution is perfect. If the cast doesn't work then a `.ToList()` is probably needed as an interim step. Perhaps this: `ICollection collection = list is ICollection c ? c : list.ToList();` and then work with collection for the rest? – Enigmativity Nov 28 '22 at 09:28
  • 1
    @Enigmativity Great feedback and suggestion; thank you! Will update my answer shortly. – Astrid E. Nov 28 '22 at 10:07
1

I see two issues with your code. First, the way you're outputting the results, it's impossible to tell the groupings of the values since you're just outputing each one on its own line.

This could be resolved buy using Console.Write for each value in a group, and then adding a Console.WriteLine() when the group is done. This way the values from each group are displayed on a separate line. We also might want to pad the values so they line up nicely by getting the length of the largest value and passing that to the PadRight method:

static void Main(string[] args)
{
    var numItems = 18;
    var splitBy = 3;

    var input = Enumerable.Range(0, numItems).ToList();
    var results = input.Split(splitBy);

    // Get the length of the largest value to use for padding smaller values, 
    // so all the columns will line up when we display the results
    var padValue = input.Max().ToString().Length + 1;

    foreach (var group in results)
    {
        foreach (var item in group)
        {
            Console.Write($"{item}".PadRight(padValue));
        }

        Console.WriteLine();
    }

    Console.Write("\n\nDone. Press any key to exit...");
    Console.ReadKey();
} 

Now your results look pretty good, except we can see that the numbers are not grouped as we expect:

0  3  6  9  12 15
1  4  7  10 13 16
2  5  8  11 14 17

The reason for this is that we're grouping by the remainder of each item divided by the number of parts. So, the first group contains all numbers whose remainder after being divided by 3 is 0, the second is all items whose remainder is 1, etc.

To resolve this, we should instead divide the index of the item by the number of items in a row (the number of columns).

In other words, 18 items divided by 3 rows will result in 6 items per row. With integer division, all the indexes from 0 to 5 will have a remainder of 0 when divided by 6, all the indexes from 6 to 11 will have a remainder of 1 when divided by 6, and all the indexes from 12 to 17 will have a remainder of 2 when divided by 6.

However, we also have to be able to handle the overflow numbers. One way to do this is to check if the index is greater than or equal to rows * columns (i.e. it would end up on a new row instead of on the last row). If this is true, then we set it to the last row.

I'm not great at linq so there may be a better way to write this, but we can modify our extension method like so:

public static IEnumerable<IEnumerable<T>> Split<T>(
    this IEnumerable<T> list, int parts)
{
    int numItems = list.Count();
    int columns = numItems / parts;
    int overflow = numItems % parts;

    int index = 0;

    return from item in list
           group item by
               index++ >= (parts * columns) ? parts - 1 : (index - 1) / columns
           into part
           select part.AsEnumerable();
}

And now our results look better:

// For 18 items split into 3
0  1  2  3  4  5
6  7  8  9  10 11
12 13 14 15 16 17

// For 25 items split into 7
0  1  2
3  4  5
6  7  8
9  10 11
12 13 14
15 16 17
18 19 20 21 22 23 24
Rufus L
  • 36,127
  • 5
  • 30
  • 43
  • Nice solution, although if `this IEnumerable list` is a "hot" enumerable that dynamically produces different numbers of values each time it is run then it won't work. `Enumerable.Range(0, rnd.Next(20)).Split(7)` for example. – Enigmativity Nov 28 '22 at 07:31
  • @Enigmativity Thanks for the feedback, that's a good point. I'll have to look at it later though...time for bed. – Rufus L Nov 28 '22 at 07:34
-1

This should work.

So each list should (ideally) have x/n elements, where x=> No. of elements in the list & n=> No. of lists it has to be split into

If x isn't divisible by n, then each list should have x/n (rounded down to the nearest integer). Let that no. be 'y'. While the last list should have x - y*(n - 1). Let that no. be 'z'.

What the first for-loop does is it repeats the process of creating a list with the appropriate no. of elements n times.

The if-else block is there to see if the list getting created is the last one or not. If it's the last one, it has z items. If not, it has y items.

The nested for-loops add the list items into "sub-lists" (List) which will then be added to the main list (List<List>) that is to be returned.

This solution is (noticeably) different from your signature and the other solutions offered. I used this approach because the code is (arguably) easier to understand albeit longer. When I used to look for solutions, I used to apply solutions where I could understand exactly what was going on. I wasn't able to fully understand the other solutions to this question (yet to get a proper hang of programming) so I presented the one I wrote below in case you were to end up in the same predicament.

Let me know if I should make any changes.

static class Program
    {
    static void Main(string[] args)
        {
        var input = new List<String>();
        for (int k = 0; k < 18; ++k)
            {
            input.Add(k.ToString());
            }
        var result = SplitList(input, 5);//I've used 5 but it can be any number
        
        foreach (var resul in result)
            {
            foreach (var res in result)
                {
                Console.WriteLine(res);
                }
            }
        }

    public static List<List<string>> SplitList (List<string> origList, int n)
        {//"n" is the number of parts you want to split your list into
        int splitLength = origList.Count / n;    //splitLength is no. of items in each list bar the last one. (In case of overflow)
        List<List<string>> listCollection = new List<List<string>>();

        for ( int i = 0; i < n; i++ )
            {
            List<string> tempStrList = new List<string>();

            if ( i < n - 1 )
                {
                for ( int j = i * splitLength; j < (i + 1) * splitLength; j++ )
                    {
                    tempStrList.Add(origList[j]);
                    }
                }
            else
                {
                for ( int j = i * splitLength; j < origList.Count; j++ )
                    {
                    tempStrList.Add(origList[j]);
                    }
                }

            listCollection.Add(tempStrList);
            }

        return listCollection;
        }
    }
New Guy
  • 53
  • 6
  • The OP gave a specific signature for the method in question. – Enigmativity Nov 28 '22 at 21:38
  • I thought OP was open to other approaches as well. This solution would also work though, I tried it on my machine. – New Guy Nov 29 '22 at 03:36
  • Can you let me know what made you thought the OP was open to other approaches? – Enigmativity Nov 29 '22 at 05:55
  • OP only said that what he has done doesn't work. He never said that he wanted a solution that was similar to what he has done. Even the title doesn't say anything about a certain approach. For these reasons I thought it was fair to assume he was open to other approaches. – New Guy Nov 29 '22 at 07:17
  • That would be a bad assumption. If you're presented a signature then you should run with that - otherwise you should explain why your approach is different and, presumably, better. – Enigmativity Nov 29 '22 at 07:35
  • No worries. I see that you're new here and I see that your have three answers to questions, all of which have a single down-vote. That's OK. It's a bit of a learning curve as to how to write good answers. I hope you stick in there. It's a rewarding community. – Enigmativity Nov 29 '22 at 11:08
  • Thanks a lot! Very nice of you for saying that. – New Guy Nov 30 '22 at 11:13
  • Tell us a bit more about why your approach works and why it will be a good fit for the OP. – Tim Jarosz Nov 30 '22 at 18:27
  • Ok, I'll add it to the answer. Let me know if I should change something else. – New Guy Dec 01 '22 at 18:13