Search code examples
c#linqsortingranking

Reusable Anonymous Ranking Function For Composite Class


I have a class that is a composite, comprised of classes of stats from various sources. The composite class gets a "total score" based on sorting and ranking properties in the stats classes.

For example, a stats class may be as follows:

public class StatsTypeA
{
    public int PropAX { get; set; }
    public int PropAXPoints { get; set; }

    public int PropAY { get; set; }
    public int PropAYPoints { get; set; }
}

And the composite class is like this example:

class CompositeType
{
    public StatsTypeA StatsA { get; set; }
    public StatsTypeB StatsB { get; set; }
    public int Id { get; set; }
    public int TotalPoints
    {
        get
        {
            return StatsA.PropAXPoints + StatsA.PropAYPoints +                 StatsB.PropBXPoints + StatsB.PropBYPoints;
        }
    }
}

In the main class, there will be a list of composite classes that have values for each of the properties in the classes:

List<CompositeType> participants = new List<CompositeType>();

I want to sort and rank them, and can do so with brute force, but can't figure out a way to not repeat a bunch of code. For instance, a single property may be ranked and scored as:

var statsX = composites.GroupBy(s => s.StatsA.PropAX,
                            s => s.Id,
                            (key, group) => (new
                            {
                              Stats = key,
                              Participants = group.ToList()
                            }))
                   .OrderBy(s => s.Stats);
int points = 0;
foreach (var statGroup in statsX)
{
    var maxPoints = points + statGroup.Participants.Count * 2 - 2;
    var thisPoints = (points + maxPoints) / 2;
    foreach (var id in statGroup.Participants)
    {
        composites.Single(data => data.Id == id).StatsA.PropAXPoints = points;
    }
    points = maxPoints + 2;
}

Is there a way to make a reusable anonymous function using the anonymous type from the LINQ statement above? In other words, how can I have a single function/extension method to rank each individual statistical property?


Solution

  • Here's the general purpose method I came up with:

    Action<IEnumerable<CompositeType>, Func<CompositeType, int>, Action<CompositeType, int>> compute =
        (cs, f, a) =>
        {
            var stats =
                from c in cs
                group c by f(c) into gs
                orderby gs.Key
                select new
                {
                    Stats = gs.Key,
                    Participants = gs.ToList()
                };
    
            stats.Aggregate(0, (points, statGroup) =>
            {
                var maxPoints = points + statGroup.Participants.Count * 2 - 2;
                var avgPoints = (points + maxPoints)/2;
                statGroup.Participants.ForEach(c => a(c, avgPoints));
                return maxPoints + 2;
            });
        };
    

    Now I can call it like this:

    compute(composites, c => c.StatsA.PropAX, (c, p) => c.StatsA.PropAXPoints = p);
    compute(composites, c => c.StatsA.PropAY, (c, p) => c.StatsA.PropAYPoints = p);
    compute(composites, c => c.StatsB.PropBX, (c, p) => c.StatsB.PropBXPoints = p);
    compute(composites, c => c.StatsB.PropBY, (c, p) => c.StatsB.PropBYPoints = p);
    

    Is that what you were after?