In answering a question on StackOverflow, it occurred to me that a more generic version of GroupBy
for C# that delegated membership in each group to a class could be useful.
This is my attempt, but I am unsatisfied with a few things. I don't particularly like the need to constrain the element type to IOrderable
- I feel like it should be possible to delegate ordering to the group type somehow (I really wanted a static method constraint in the IGGrouping
interface).
public interface IOrderable<T> {
double ordering();
}
public interface IGGrouping<T> : IEnumerable<T> where T : IOrderable<T> {
bool BelongsToGroup(T aT);
IGGrouping<T> Add(T aT);
}
public static class IEnumerableExt {
public static IEnumerable<IGGrouping<T>> GroupBy<TG,T>(this IEnumerable<T> src) where TG : IGGrouping<T>, new() where T : IOrderable<T> {
var groups = new List<IGGrouping<T>>();
void Add(T aT) {
var found = false;
foreach (var g in groups) {
found = g.BelongsToGroup(aT);
if (found) {
g.Add(aT);
break;
}
}
if (!found)
groups.Add(new TG().Add(aT));
}
foreach (var s in src.OrderBy(s => s.ordering()))
Add(s);
return groups;
}
}
Here's a sample class to group by distance from center of current groups:
public class gRectangleGroup : IGGrouping<Rectangle> {
List<Rectangle> members;
public Point center;
public gRectangleGroup() {
members = new List<Rectangle>();
}
public gRectangleGroup Add(Rectangle r) {
members.Add(r);
center = new Point(members.Average(m => m.Loc.X), members.Average(m => m.Loc.Y));
return this;
}
public bool BelongsToGroup(Rectangle r) => center.Distance(r.Loc) <= 5;
public Rectangle Middle() => members.OrderBy(m => m.Loc.Distance(center)).First();
public IEnumerator<Rectangle> GetEnumerator() => members.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
IGGrouping<Rectangle> IGGrouping<Rectangle>.Add(Rectangle aT) => Add(aT);
}
You use gRectangleGroup
like so:
List<Rectangle> list;
var ans2 = list.GroupBy<gRectangleGroup, Rectangle>().Cast<gRectangleGroup>().Select(rg => rg.Middle());
I don't like having to list the group type and the element type explicitly in the call to GroupBy
. I don't like the Cast
to gRectangleGroup
when the type is passed to the GroupBy
, but if GroupBy
returns TG
then I have to give up the fluent Add
call for new groups.
I also don't like how a class that implements IGGrouping
(like gRectangleGroup
) and has a fluent Add
that returns this
, must also have another Add
to return IGGrouping<T>
because of the interface.
Can anyone suggest how this might be improved, or what they would change to solve the same issue?
GroupBy
is actually doing? I cannot figure this out. How is it different from the original one? \$\endgroup\$GroupBy
groups according to a key calculated from a single item. ThisGroupBy
groups according to a rule specified in each group, which could involve calculating across all previous group members. The example is about clustering points into groups that are "nearby" the centroid of the group. \$\endgroup\$