Tutorial :C# Ranking of objects, multiple criteria



Question:

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;  


Solution:1

This should work for a non-dense rank:

static class Program  {        static IEnumerable<Result> GetResults(Dictionary<TournamentTeam, double> wins, Dictionary<TournamentTeam, double> scores)      {          int r = 1;          double lastWin = -1;          double lastScore = -1;          int lastRank = 1;            foreach (var rank in from name in wins.Keys                               let score = scores[name]                               let win = wins[name]                               orderby win descending, score descending                               select new Result { Name = name, Rank = r++, Score = score, Win = win })          {              if (lastWin == rank.Win && lastScore == rank.Score)              {                  rank.Rank = lastRank;              }              lastWin = rank.Win;              lastScore = rank.Score;              lastRank = rank.Rank;              yield return rank;          }      }  }    class Result  {      public TournamentTeam Name;      public int Rank;      public double Score;      public double Win;  }  


Solution:2

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  


Solution:3

Assuming you have a List<Result> structure where the Result object has the following parameters...

Pesron     - string  Rank       - int  Wins       - double  TotalScore - int  

You could write a custom comparer, and then pass that to List.Sort(Comparison<Result> comparison)

Alternative, you could just make your Result object implement IComparable<Result> and stick this in your class.

        #region IComparable Members            public int CompareTo(Result obj)          {              if (this.Rank.CompareTo(obj.Rank) != 0)                  return this.Rank.CompareTo(obj.Rank);                if (this.Wins.CompareTo(obj.Wins) != 0)                  return (this.Wins.CompareTo(obj.Wins);                return (this.TotalScore.CompareTo(obj.TotalScore) ;            }            





        
Previous
Next Post »