Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Split a collection into n parts with LINQ, in VB.Net

Question

In VB.Net, If I have a collection like this:

Dim collection As IEnumerable(Of Integer) = Enumerable.Range(0, 99)

How I could split it in groups/Ienumerables of an indeterminated amount of elements?


Conditions

Using a LINQ query (not MORELinq or any other 3rd party libs)

Not writting a Function, just using (or appending to the collection) a LINQ query avoiding the insertion of a custom and generic procedure to split in parts.

Not generating an Anonymous Type (because I will respect the VB.Net Option Statements).


Research

I've read at these questions but I was not able to properly translate the C# LINQ querys into VB.Net (even online translators fails and need much modifications):

Split a collection into `n` parts with LINQ?

How can I split an IEnumerable<String> into groups of IEnumerable<string>

Split List into Sublists with LINQ

Split a entity collection into n parts

These are two different approaches that I've tried to adapt from the solutions given in those S.O. questions above, but my translations doesn't works, the first can't compile because the condition of Group By and the second does not generates an splitted collection, it generates a collection per each element:

1.

    Dim parts As Integer = 4
    Dim i As Integer = 0

    Dim splits As IEnumerable(Of IEnumerable(Of Integer)) =
        From item As Integer In collection
        Group By (Math.Max(Interlocked.Increment(i), i - 1) Mod parts)
        Into Group
        Select Group.AsEnumerable

2.

    Dim parts As Integer = 4

    Dim result As IEnumerable(Of IEnumerable(Of Integer)) =
        collection.Select(Function(s, i) New With
            {
                Key .Value = s,
                Key .Index = i
            }
        ).GroupBy(Function(item)
                      Return (item.Index >= (item.Index / parts)) And (item.Index >= item.Value)
                  End Function,
               Function(item) item.Value).Cast(Of IEnumerable(Of Integer))()

Expected Results

So, If I have a source collection like this:

Dim collection As IEnumerable(Of Integer) = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

And supposing that I have that imaginary LINQ query that can split into a variable named splits by the value specified on variable parts:

Dim parts as Integer = ...
Dim splits As IEnumerable(Of IEnumerable(Of Integer)) =
   From value As Integer In collection (Linq Query that splits by given 'parts' value)

If I choosen that parts value is 4 then the result will be an IEnumerable(Of IEnumerable(Of Integer)) which will contains 3 collections, where:

splits(0) = {1, 2, 3, 4}
splits(1) = {5, 6, 7, 8}
splits(2) = {9, 10}

If I choosen that parts value is 5 then the result will be an IEnumerable(Of IEnumerable(Of Integer)) which will contains 2 collections, where:

splits(0) = {1, 2, 3, 4, 5}
splits(1) = {6, 7, 8, 9, 10}

If I choosen that parts value is 1 then the result will be an IEnumerable(Of IEnumerable(Of Integer)) which will contains the identical source collection because I choosen to split in just 1 part:

splits(0) = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

If I choosen that parts value is 10 then the result will be an IEnumerable(Of IEnumerable(Of Integer)) which will contains 10 collections, one collection per each value of the source example collection:

splits(0) = {1}
splits(1) = {2}
splits(2) = {3}
and so on...
like image 772
ElektroStudios Avatar asked Mar 04 '15 06:03

ElektroStudios


2 Answers

You can use Range, Skip and Take to achieve your goal.

Dim collection As IEnumerable(Of Integer) = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
Dim parts As Integer = 4
Dim count As Integer = CInt(Math.Ceiling(collection.Count() / parts)) '= 3
Dim result As IEnumerable(Of IEnumerable(Of Integer)) = Nothing

'Method 1:
result = From i In Enumerable.Range(0, count) Select collection.Skip(i * parts).Take(parts)

'Method 2:
result = Enumerable.Range(0, count).Select(Function(i) collection.Skip(i * parts).Take(parts))

For Each item In result
    Debug.WriteLine(String.Join(", ", item))
Next

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

Mandatory extension method:

Public Module Extensions

    <System.Runtime.CompilerServices.Extension()>
    Public Function Split(Of TSource)(collection As IEnumerable(Of TSource), ByVal parts As Integer) As IEnumerable(Of IEnumerable(Of TSource))
        If (collection Is Nothing) Then Throw New ArgumentNullException("collection")
        If (parts < 1) Then Throw New ArgumentOutOfRangeException("parts")
        Dim count As Integer = collection.Count()
        If (count = 0) Then Return {}
        If (parts >= count) Then Return {collection}
        Return Enumerable.Range(0, CInt(Math.Ceiling(count / parts))).Select(Function(i) collection.Skip(i * parts).Take(parts))
    End Function

End Module

result = collection.Split(4)
like image 62
Bjørn-Roger Kringsjå Avatar answered Sep 17 '22 22:09

Bjørn-Roger Kringsjå


The query you're looking for could look like this:

Dim partitionSize As Integer = 4
Dim collection As IEnumerable(Of Integer) = Enumerable.Range(0, 100)
Dim parts As Integer = collection.Count \ partitionSize
Dim splits = (From i In collection
              Group i By key = i Mod parts Into g = Group
              Select key, g)

This relies on the sample you've shown. However if the actual data is arranged more randomly, this way will use the value at the index grouped according to the index value:

Dim partitionSize As Integer = 4
Dim collection As IEnumerable(Of Integer) = Enumerable.Range(100, 100)
Dim parts As Integer = collection.Count \ partitionSize
Dim splits = (From i In Enumerable.Range(0, 100)
              Group collection(i) By key = i Mod parts Into g = Group
              Select key, g)

Your post says that you don't want to use anonymous types. However Group By uses anonymous types. You can make the anonymous type temporary by casting the collection to a Dictionary:

Dim splits = (From i In Enumerable.Range(0, 100)
              Group collection(i) By key = i Mod parts Into g = Group
              Select key, g).ToDictionary(Function(x) x.key, Function(x) x.g)
like image 30
tinstaafl Avatar answered Sep 18 '22 22:09

tinstaafl