[ACCEPTED]-Grouping lists into groups of X items per group-partitioning
Here's one way to do this using LINQ...
public static IEnumerable<IGrouping<int, TSource>> GroupBy<TSource>
(this IEnumerable<TSource> source, int itemsPerGroup)
{
return source.Zip(Enumerable.Range(0, source.Count()),
(s, r) => new { Group = r / itemsPerGroup, Item = s })
.GroupBy(i => i.Group, g => g.Item)
.ToList();
}
0
I think you are looking for something like 2 this:
return source.Select((x, idx) => new { x, idx })
.GroupBy(x => x.idx / itemsPerGroup)
.Select(g => g.Select(a => a.x));
You need to change your return type 1 as IEnumerable<IEnumerable<TSource>>
The problem with using GroupBy()
is that unless it 20 somehow has knowledge under the hood that 19 the input is ordered by key value, it has 18 to read the entire sequence and allocate 17 everything to its bucket before it can emit 16 a single group. That's overkill in this 15 case, since the key is a function of its 14 ordinal position within the sequence.
I like 13 the source.Skip(m).Take(n)
approach, but that makes assumptions 12 that items in source
can be directly addressed. If 11 that's not true or Skip()
and Take()
have no knowledge 10 of the underlying implementation, then the 9 production of each group is going to be 8 an O(n/2) operation on the average, as it 7 repeatedly iterates over source
to produce the 6 group.
This makes the overall partitioning 5 operation, potentially quite expensive.
- IF producing a group is an O(n/2) operation on the average, and
- Given a group size of s, the production of approximately n/s groups is required,
Then 4 the total cost of the operation is something 3 like O(n2/2s), right?
So, I would do something 2 this, an O(n) operation (feel free to use 1 an IGrouping
implementation if you'd like):
public static IEnumerable<KeyValuePair<int,T[]>> Partition<T>( this IEnumerable<T> source , int partitionSize )
{
if ( source == null ) throw new ArgumentNullException("source") ;
if ( partitionSize < 1 ) throw new ArgumentOutOfRangeException("partitionSize") ;
int i = 0 ;
List<T> partition = new List<T>( partitionSize ) ;
foreach( T item in source )
{
partition.Add(item) ;
if ( partition.Count == partitionSize )
{
yield return new KeyValuePair<int,T[]>( ++i , partition.ToArray() ) ;
partition.Clear() ;
}
}
// return the last partition if necessary
if ( partition.Count > 0 )
{
yield return new Partition<int,T>( ++i , items.ToArray() ) ;
}
}
Essentially you have an IEnumerable, and 15 you want to group it into an IEnumerable 14 of IGroupables which each contain the key 13 as an index and the group as the values. Your 12 version does seem to accomplish on the first 11 pass, but I think that you can definitely 10 stream line a little bit.
Using skip and 9 take is the most desirable way to accomplish 8 in my opinion, but the custom key for grouping 7 is where there is an issue. There is a way 6 around this which is to create your own 5 class as a grouping template (seen in this 4 answer: https://stackoverflow.com/a/5073144/1026459).
The end result is this:
public static class GroupExtension
{
public static IEnumerable<IGrouping<int, T>> GroupAt<T>(this IEnumerable<T> source, int itemsPerGroup)
{
for(int i = 0; i < (int)Math.Ceiling( (double)source.Count() / itemsPerGroup ); i++)
{
var currentGroup = new Grouping<int,T>{ Key = i };
currentGroup.AddRange(source.Skip(itemsPerGroup*i).Take(itemsPerGroup));
yield return currentGroup;
}
}
private class Grouping<TKey, TElement> : List<TElement>, IGrouping<TKey, TElement>
{
public TKey Key { get; set; }
}
}
And here 3 is the demo in the fiddle which consumes 2 it on a simple string
public class Program
{
public void Main(){
foreach(var p in getLine().Select(s => s).GroupAt(3))
Console.WriteLine(p.Aggregate("",(s,val) => s += val));
}
public string getLine(){ return "Hello World, how are you doing, this just some text to show how the grouping works"; }
}
edit
Alternatively as just 1 an IEnumerable of IEnumerable
public static IEnumerable<IEnumerable<T>> GroupAt<T>(this IEnumerable<T> source, int itemsPerGroup)
{
for(int i = 0; i < (int)Math.Ceiling( (double)source.Count() / itemsPerGroup ); i++)
yield return source.Skip(itemsPerGroup*i).Take(itemsPerGroup);
}
This is based on Selman's Select
with index idea, but 5 using ToLookup
to combine both the GroupBy
and Select
together 4 as one:
public static IEnumerable<IEnumerable<TSource>> GroupBy<TSource>
(this IEnumerable<TSource> source, int itemsPerGroup)
{
return source.Select((x, idx) => new { x, idx })
.ToLookup(q => q.idx / itemsPerGroup, q => q.x);
}
The main difference though is that 3 ToLookup
actually evaluates results immediately 2 (as concisely explained here: https://stackoverflow.com/a/11969517/7270462), which may 1 or may not be desired.
More Related questions
We use cookies to improve the performance of the site. By staying on our site, you agree to the terms of use of cookies.