Suppose a series of objects (presented here as tuples):
"a" | 1 "a" | 2 "b" | 3 "b" | 4 "a" | 5
There is no built in function (that I know of) to group by the first columns's sequence, that is, all the "a"'s in a row, then the "b"'s, then the one "a" alone. So that the groups become: {1,2},{3,4},{5} and not {1,2,5},{3,4}.
So I wrote this, which I'm submitting for review. I emulate all 8 variants of GroupBy
which I present here as the two main variants (with and without result selector):
public static IEnumerable<IGrouping<TKey, TElement>> GroupBySequence<TSource, TKey, TElement>
(this TSource[] source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
IEqualityComparer<TKey> comparer)
{
var newElement = source.Select(keySelector).ToArray().MakeSequentialKey(comparer).Zip(
source.Select(elementSelector),
(x, y) => new Tuple<int, TElement>(x, y));
var groupElement = newElement.GroupBy(t => t.Item1, t => t.Item2);
var newKey = source.Select(keySelector).ToArray().MakeSequentialKey(comparer).Zip(
source.Select(keySelector),
(x, y) => new Tuple<int, TKey>(x, y));
var groupKey = newKey.GroupBy(t => t.Item1, t => t.Item2);
return groupKey.Zip(groupElement,
(key,element) => new Grouping<TKey,TElement>(key.First(),element));
}
public static IEnumerable<TResult> GroupBySequence<TSource, TKey, TElement, TResult>
(this TSource[] source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
Func<TKey, IEnumerable<TElement>, TResult> resultSelector,
IEqualityComparer<TKey> comparer)
{
return source.GroupBySequence(keySelector,
elementSelector, comparer).Select(x => resultSelector(x.Key, x));
}
Helper methods:
//Performs an operation over each consecutive item. Here used for determining equality.
public static IEnumerable<TResult> WithNext<T, TResult>
(this T[] source, Func<T, T, TResult> operation)
{
return source.Zip(source.Skip(1), operation);
}
//Makes the unique key
public static IEnumerable<int> MakeSequentialKey<T>
(this T[] source, IEqualityComparer<T> comparer)
{
if (source.Length == 0)
return Enumerable.Empty<int>();
return (new[] { 0 })
.Concat(source.ToArray().WithNext<T, int>((x, y) => comparer.Equals(x, y) ? 0 : 1))
.ToArray()
.RunningSum();
}
//Sum of all previous elements up to each item of an array
public static IEnumerable<int> RunningSum(this int[] source)
{
int cumul = 0;
foreach (int i in source)
yield return cumul += i;
}
And the Grouping class, which is pretty much a straightforward implementation of IGrouping:
public class Grouping<TKey, TElement> : IGrouping<TKey, TElement>
{
TKey key;
IEnumerable<TElement> elements;
public Grouping(TKey key, IEnumerable<TElement> elements)
{
this.key = key;
this.elements = elements;
}
public TKey Key { get { return key; } }
public IEnumerator<TElement> GetEnumerator()
{
return elements.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return elements.GetEnumerator();
}
}
Anticipated questions:
- What is your general approach here?
Generate a really unique key from the given key, group the elements AND the key with that, and either reform new groups with
Grouping
or apply result to it, so that the original key type is still used. - Why extent
T[]
and notIEnumerable<T>
? Because usage like this means the elements are ordered. It would not make sense to useGroupBySequence
over a Dictionary or a HashSet which both implement IEnumerable, because these two collections have AFAIK no notion of order. If there s a better or clearer way to indicate this, I don't know it.
I'm looking for criticism, suggestions on clarity and best practices. Thank you for your time.
{1,2},{3,4},{5}
is your desired outcome, I don't understand all the complexity you added. Can't you just write a simple loop through the items which yields a result every time a group is passed? \$\endgroup\$GroupBy
. \$\endgroup\$