6

I would like to split a list in parts, without knowing how much items I will have in that list. The question is different from those of you who wants to split a list into chunk of fixed size.

int[] a = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

I would like the values to be splitted vertically.

Splitted in 2 :

-------------------
| item 1 | item 6 |
| item 2 | item 7 |
| item 3 | item 8 |
| item 4 | item 9 |
| item 5 |        |

Splitted in 3:

| item 1 | item 4 | item 7 |
| item 2 | item 5 | item 8 |
| item 3 | item 6 | item 9 |

Splitted in 4:

| item 1 | item 4 | item 6 | item 8 |
| item 2 | item 5 | item 7 | item 9 |
| item 3 |        |        |        |

I've found a few c# extensions that can do that but it doesn't distribute the value the way I want. Here's what I've found:

// this technic is an horizontal distribution
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;
}

the result is this but my problem is that the value are distributed horizontally:

| item 1 | item 2 |
| item 3 | item 4 |
| item 5 | item 6 |
| item 7 | item 8 |
| item 9 |        |

or

| item 1 | item 2 | item 3 |
| item 4 | item 5 | item 6 |
| item 7 | item 8 | item 9 |

any idea how I can distribute my values vertically and have the possibility to choose the number of parts that i want?

In real life

For those of you who want to know in which situation I would like to split a list vertically, here's a screenshot of a section of my website:

enter image description here

Mario S
  • 11,715
  • 24
  • 39
  • 47
Alexandre Jobin
  • 2,811
  • 4
  • 33
  • 43
  • So you want the first half in one list, the second half in another. You know how many elements are in the list... perhaps you could try creating a list and inserting the first half the elements into that new list... hmmm... – ean5533 Nov 21 '12 at 21:49
  • 7
    `var list1 = a.Take(a.Length / 2).ToList(); var list2 = a.Skip(list1.Count).ToList();` – Ilia G Nov 21 '12 at 21:51
  • Are you looking to split into 2 equal (similar) length lists, fixed page lengths, or split on content value? Also are you looking for the ability to handle infinite sets or fixed collections? (Obviously it is not possible to split an infinite set in half) – Matthew Whited Nov 21 '12 at 21:55
  • @MatthewWhited i would like to split in 2 equal similar length list. I have updated my question to show more precisely what i want. – Alexandre Jobin Nov 22 '12 at 16:38
  • @IliaG the code is good but it will only work if i want it splitted in 2. What if i want it in 3 parts? – Alexandre Jobin Nov 22 '12 at 16:39
  • @AlexandreJobin that seems like a trivial change from the given code. You need to calc the length and offsets for your sublists and then grab valuee using Skip() and Take() – Ilia G Nov 22 '12 at 21:25
  • possible duplicate of [What's a clean way to break up a DataTable into chunks of a fixed size with Linq?](http://stackoverflow.com/questions/769373/whats-a-clean-way-to-break-up-a-datatable-into-chunks-of-a-fixed-size-with-linq) – Oliver Nov 23 '12 at 09:21
  • 1
    @Oliver It would seem so at first glance, but the result the OP wants, differ from that question and for example [this question](http://stackoverflow.com/questions/419019/split-list-into-sublists-with-linq). – Mario S Nov 23 '12 at 09:27
  • Exact duplicate of [Split C# collection into equal parts, maintaining sort](http://stackoverflow.com/questions/3892734/split-c-sharp-collection-into-equal-parts-maintaining-sort) – nawfal Feb 18 '13 at 12:40

3 Answers3

15

With .Take() and .Skip() you can:

int[] a = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

int splitIndex = 4; // or (a.Length / 2) to split in the middle.

var list1 = a.Take(splitIndex).ToArray(); // Returns a specified number of contiguous elements from the start of a sequence.
var list2 = a.Skip(splitIndex).ToArray(); // Bypasses a specified number of elements in a sequence and then returns the remaining elements.

You can use .ToList() instead of .ToArray() if you want a List<int>.


EDIT:

After you changed (clarified maybe) your question a bit, I guess this is what you needed:

public static class Extensions
{
    public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int parts)
    {
        var list = new List<T>(source);
        int defaultSize = (int)((double)list.Count / (double)parts);
        int offset = list.Count % parts;
        int position = 0;

        for (int i = 0; i < parts; i++)
        {
            int size = defaultSize;
            if (i < offset)
                size++; // Just add one to the size (it's enough).

            yield return list.GetRange(position, size);

            // Set the new position after creating a part list, so that it always start with position zero on the first yield return above.
            position += size;
        }
    }
}

Using it:

int[] a = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var lists = a.Split(2);

This would generate:

split in 2 : a.Split(2);

| item 1 | item 6 |
| item 2 | item 7 |
| item 3 | item 8 |
| item 4 | item 9 |
| item 5 |        |

split in 3 : a.Split(3);

| item 1 | item 4 | item 7 |
| item 2 | item 5 | item 8 |
| item 3 | item 6 | item 9 |

split in 4 : a.Split(4);

| item 1 | item 4 | item 6 | item 8 |
| item 2 | item 5 | item 7 | item 9 |
| item 3 |        |        |        |

Also, if you would have:

int[] b = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 10 items

and split in 4 : b.Split(4);

| item 1 | item 4 | item 7 | item 9 |
| item 2 | item 5 | item 8 | item 10|
| item 3 | item 6 |        |        |
Mario S
  • 11,715
  • 24
  • 39
  • 47
  • 1
    The code is good but i wanted a function that would split de list in the number of parts that i want. My question wasnt very clear about that and i have updated the description to show more precisely what i wanted to do. Sorry about that! – Alexandre Jobin Nov 22 '12 at 16:41
  • @AlexandreJobin I have updated the answer. Is this what you meant? – Mario S Nov 22 '12 at 23:30
  • @AlexandreJobin I must say that I don't quite know where and why one would split a list in this way, but it is a bit intriguing =) – Mario S Nov 22 '12 at 23:43
  • 1
    @jessehouwing Yepp, it was late night for me when I wrote it. I have updated the method so that it won't iterate the list so many times. Though, I believe it's inevitable to iterate through the list, at least once, to get the desired result. – Mario S Nov 23 '12 at 09:17
  • 1
    @Mario i have updated my description to show a real scenario where i need to split my list in parts verticaly. Thank you for your code, it work great! – Alexandre Jobin Nov 23 '12 at 16:04
1

This seems to do the trick quite nicely. It can probably be done even more efficient, but this was puzzling enough... It's much easier to do:

1|4|7|10
2|5|8
3|6|9

Than:

1|4|7|9
2|5|8|10
3|6|

I ignored the LINQ request first, since I had trouble wrapping my head around it. The solution using normal array manipulation could result in something like this:

    public static IEnumerable<IEnumerable<TListItem>> Split<TListItem>(this IEnumerable<TListItem> items, int parts)
        where TListItem : struct
    {
        var itemsArray = items.ToArray();
        int itemCount = itemsArray.Length;
        int itemsOnlastRow = itemCount - ((itemCount / parts) * parts);
        int numberOfRows = (int)(itemCount / (decimal)parts) + 1;

        for (int row = 0; row < numberOfRows; row++)
        {
            yield return SplitToRow(itemsArray, parts, itemsOnlastRow, numberOfRows, row);
        }
    }

    private static IEnumerable<TListItem> SplitToRow<TListItem>(TListItem[] items, int itemsOnFirstRows, int itemsOnlastRow,
                                                                int numberOfRows, int row)
    {
        for (int column = 0; column < itemsOnFirstRows; column++)
        {
            // Are we on the last row?
            if (row == numberOfRows - 1)
            {
                // Are we within the number of items on that row?
                if (column < itemsOnlastRow)
                {
                    yield return items[(column + 1) * numberOfRows -1];
                }
            }
            else
            {
                int firstblock = itemsOnlastRow * numberOfRows;
                int index;

                // are we in the first block?
                if (column < itemsOnlastRow)
                {
                    index = column*numberOfRows + ((row + 1)%numberOfRows) - 1;
                }
                else
                {
                    index = firstblock + (column - itemsOnlastRow)*(numberOfRows - 1) + ((row + 1)%numberOfRows) - 1;
                }

                yield return
                    items[index];
            }
        }
    }

The LINQ pseudo code would be:

//WARNING: DOES NOT WORK
public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> list, int parts)
{
    int itemOnIndex = 0;
    var splits = from item in list
                 group item by MethodToDefineRow(itemOnIndex++) into row
                 select row.AsEnumerable();
    return splits;
}

But without knowing the number of items, there is no way to calculate the place where to put it.

So by doing a little pre-calculation, you can use LINQ to achieve the same thing as above this requires going through the IEnumerable twice, there doesn't seem to be a way around that. The trick is to calculate the row each value will be assigned to.

    //WARNING: Iterates the IEnumerable twice
    public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> list, int parts)
    {
        int itemOnIndex = 0;
        int itemCount = list.Count();
        int itemsOnlastRow = itemCount - ((itemCount / parts) * parts);
        int numberOfRows = (int)(itemCount / (decimal)parts) + 1;
        int firstblock = (numberOfRows*itemsOnlastRow);

        var splits = from item in list
                     group item by (itemOnIndex++ < firstblock) ? ((itemOnIndex -1) % numberOfRows) : ((itemOnIndex - 1 - firstblock) % (numberOfRows - 1)) into row
                     orderby row.Key
                     select row.AsEnumerable();
        return splits;
    }
jessehouwing
  • 106,458
  • 22
  • 256
  • 341
-2

Use .Take(#OfElements) to specify number of elements you want to take.

You can also use .First and .Last

jordan
  • 3,436
  • 11
  • 44
  • 75