C# Ranking of objects, multiple criteria

John Gietzen picture John Gietzen · May 31, 2009 · Viewed 11.6k times · Source

I am building a plugin for a LAN party website that I wrote that would allow the use of a Round Robin tournament.

All is going well, but I have some questions about the most efficient way to rank over two criteria.

Basically, I would like the following ranking layout:

         Rank  Wins  TotalScore
PersonE  1     5     50
PersonD  2     3.5   37
PersonA  2     3.5   37
PersonC  4     2.5   26
PersonB  5     2.5   24
PersonF  6     0     12

In SQL server, I would use:

SELECT
    [Person],
    RANK() OVER (ORDER BY Wins DESC, TotalScore DESC) [Rank],
    [Wins],
    [TotalScore]

Now, I only have List, Dictionary, and etc. to work with

Specifically:

Dictionary<TournamentTeam, double> wins = new Dictionary<TournamentTeam, double>();
Dictionary<TournamentTeam, double> score = new Dictionary<TournamentTeam, double>();

Is there a way to do this style of ranking with LINQ?

If not, is there an extensible way that would allow me later to take in to account Win-Loss-Draw instead of just wins if I choose to?

Edit:

My adaptation of TheSoftwareJedi's answer:

private class RRWinRecord : IComparable
{
    public int Wins { get; set; }
    public int Losses { get; set; }
    public int Draws { get; set; }
    public double OverallScore { get; set; }
    public double WinRecord
    {
        get
        {
            return this.Wins * 1.0 + this.Draws * 0.5 + this.Losses * 0.0;
        }
    }

    public int CompareTo(object obj) { ... }

    public override bool Equals(object obj) { ... }
    public override int GetHashCode() { ... }
    public static bool operator ==(RRWinRecord lhs, RRWinRecord rhs) { ... }
    public static bool operator !=(RRWinRecord lhs, RRWinRecord rhs) { ... }
    public static bool operator >(RRWinRecord lhs, RRWinRecord rhs) { ... }
    public static bool operator <(RRWinRecord lhs, RRWinRecord rhs) { ... }
    public static bool operator >=(RRWinRecord lhs, RRWinRecord rhs) { ... }
    public static bool operator <=(RRWinRecord lhs, RRWinRecord rhs) { ... }
}

...

    int r = 1, lastRank = 1;
    RRWinRecord lastRecord = null;

    var ranks = from team in records.Keys
                let teamRecord = records[team]
                orderby teamRecord descending
                select new RRRank() { Team = team, Rank = r++, Record = teamRecord };

    foreach (var rank in ranks)
    {
        if (rank.Record != null && lastRecord == rank.Record)
        {
            rank.Rank = lastRank;
        }

        lastRecord = rank.Record;
        lastRank = rank.Rank;

        string scoreDescription = String.Format("{0}-{1}-{2}", rank.Record.Wins, rank.Record.Losses, rank.Record.Draws);
        yield return new TournamentRanking(rank.Team, rank.Rank, scoreDescription);
    }

    yield break;

Answer

Amy B picture Amy B · Jun 1, 2009

Ranking isn't too hard. Just mishmash OrderBy and Select implementation patterns together and you can have an easy to use Ranking extension method. Like this:

    public static IEnumerable<U> Rank<T, TKey, U>
    (
      this IEnumerable<T> source,
      Func<T, TKey> keySelector,
      Func<T, int, U> selector
    )
    {
        if (!source.Any())
        {
            yield break;
        }

        int itemCount = 0;
        T[] ordered = source.OrderBy(keySelector).ToArray();
        TKey previous = keySelector(ordered[0]);
        int rank = 1;
        foreach (T t in ordered)
        {
            itemCount += 1;
            TKey current = keySelector(t);
            if (!current.Equals(previous))
            {
                rank = itemCount;
            }
            yield return selector(t, rank);
            previous = current;
        }
    }

Here's some test code

string[] myNames = new string[]
{ "Bob", "Mark", "John", "Jim", "Lisa", "Dave" };
//
var query = myNames.Rank(s => s.Length, (s, r) => new { s, r });
//
foreach (var x in query)
{
  Console.WriteLine("{0} {1}", x.r, x.s);
}

Which yields these results:

1 Bob
1 Jim
3 Mark
3 John
3 Lisa
3 Dave