Skip to content

Instantly share code, notes, and snippets.

@leemeichin
Created October 9, 2012 16:11
Show Gist options
  • Save leemeichin/3859792 to your computer and use it in GitHub Desktop.
Save leemeichin/3859792 to your computer and use it in GitHub Desktop.
Apply Standard Competition Rankings to your leaderboard

Standard Competition Rankings

This class makes it easier to generate a leaderboard that complies with the Standard Competition Ranking system. That is, it takes joint/tied positions into account, and adjusts the positions accordingly.

Take the following list of users and points for example:

User    | Points
1         35
2         35
3         50
4         10
5         35

You might be contented with a leaderboard like this:

User    | Points    | Position
3         50          1
2         35          2
1         35          3
5         35          4
4         10          5

Users 2, 1 and 5 all have the same points, so what makes user 2 come second, and user 5 come fourth? That can't be right!

What you really need is this:

User    | Points    | Position
3         50          1
2         35          2
1         35          2
5         35          2
4         10          5

Which you can do with the code in this gist!

ranking_data = {...} # hash representation of above user/points table

leaderboard = StandardCompetitionRankings.new(ranking_data, :rank_by => :points, :sort_direction => :desc)
better_leaderboard = leaderboard.calculate

If you don't want any sorting applied because you want to roll your own or whatever, set the :sort_direction parameter to nil or false.

After all that, it will append a field called position to your dataset, which contains your now standardised rankings, and return the whole thing. :)

class StandardCompetitionRankings
def initialize(data, options = {})
@data = data
@options = options.reverse_merge(:rank_by => :points, :sort_direction => :desc)
end
def sort_data!
return if @options[:sort_direction].nil? or @options[:sort_direction] == false
case @options[:sort_direction]
when :desc then @data.sort! {|a, b| b[@options[:rank_by]] <=> a[@options[:rank_by]] }
when :asc then @data.sort! {|a, b| a[@options[:rank_by]] <=> b[@options[:rank_by]] }
else raise ArgumentError, "Sort direction can only be :asc or :desc"
end
end
def calculate
return @rankings if @rankings.present?
@rankings = []
sort_data!
@data.each_with_index do |data, i|
if i == 0
data[:position] = 1
elsif data[@options[:rank_by]] == @data[i-1][@options[:rank_by]]
data[:position] = @rankings[i-1][:position]
else
data[:position] = i + 1
end
@rankings[i] = data
end
@rankings
end
end
require 'spec_helper'
describe StandardCompetitionRankings do
let!(:sample_data) do
%w(1000 2000 3000 3000 4000 5000 6000 7000 7000 9000).map {|points| {:points => points} }
end
describe '#calculate' do
it "should produce rankings with correctly calculated tied positions, sorted ascending" do
scr = Pokerfed::StandardCompetitionRankings.new(sample_data, :rank_by => :points, :sort_direction => :asc)
rankings = scr.calculate.map {|r| r[:position] }
rankings.should == [1, 2, 3, 3, 5, 6, 7, 8, 8, 10]
end
it "should produce rankings with correctly calculated tied positions, sorted descending" do
scr = Pokerfed::StandardCompetitionRankings.new(sample_data, :rank_by => :points, :sort_direction => :desc)
rankings = scr.calculate.map {|r| r[:position] }
rankings.should == [1, 2, 2, 4, 5, 6, 7, 7, 9, 10]
end
end
end
@bbuchalter
Copy link

@leemachin, thanks for sharing. While this is great for unstructured data, it's worth mentioning that if you can use SQL, it's a much simpler implementation and faster. Here's a sample which also supports pagination.

Keep in mind I've modified this from my working version to make it more generic, so I can't guarantee there won't be any bugs.

# app/models/user.rb
class User
  def rank
    @rank ||= User.where("ranked_attribute > ?", ranked_attribute).count + 1
  end
end

# app/controllers/leaderboard.rb
class LeaderboardController
  def index
    @limit = 15
    users = User.where("ranked_attribute IS NOT NULL")
    users = users.where("ranked_attribute > 0")
    users = users.order("ranked_attribute DESC")
    users = users.limit(@limit)
    users = users.where("ranked_attribute <= ?", params[:last_ranked_value]) if params[:last_ranked_value].present?
    users = users.where("id NOT IN (?)", params[:uids].to_a) if params[:uids].present? # don't display any already displayed

    @ranked_users = users.sort_by(&:rank_30_days)
  end
end
# app/views/leaderboard/index.html.haml
%ul#rankings.no-bullet
        = render partial: "rankings", ranked_users: @ranked_users
      %a.btn{:href => "#", :id => "show_more_leaderboard", "data-target" => "rankings"} Show More


# app/views/leaderboard/_rankings.html.haml
- @ranked_users.each do |user| 
  %li.row{"data-user-id" => user.id, "data-rank" => user.rank, "data-value" => user.ranked_attribute}
    = user.name
    = user.rank
# app/assets/javascript/leaderboard.js.coffee
$ ->
  $('a#show_more_leaderboard').on('click', getMoreLeaders);

getMoreLeaders = (eventObject) ->
  eventObject.preventDefault()
  clicked = $(eventObject.delegateTarget)
  $.ajax("/leaderboard", data: {
    uids: collectUserIds(clicked.data("target")),
    last_ranked_value: lastRankedValue(clicked.data("target")),
    last_rank: lastRank(clicked.data("target")),
    target: "#"+clicked.data("target"),
  }, dataType: 'script')

collectUserIds = (target) ->
  user_ids = []
  $("#"+target).find("li.row").each (index,element) ->
    user_ids.push($(element).data("user-id"))
  user_ids

lastRank = (target) ->
  $("#"+target).find("li.row").last().data("rank")

lastRankedValue = (target) ->
  $("#"+target).find("li.row").last().data("value")
# app/views/leaderboard/index.js.erb
<% if @ranked_users.length > 0 %>
  $("<%= @target%>").append("<%= escape_javascript(render partial: 'rankings', ranked_users: @ranked_users) %>");
<% end %>

<% if @ranked_users.length < @limit  %>
  $("<%= @target%>").append("<li class='row'>That's everbody!</li>");
  $("#show_more_leaderboard").hide();
<% end %> 

@dopa
Copy link

dopa commented Aug 28, 2013

THANK BOTH OF YOU!!! This really helped me with a project I'm working on!!!

@FlopTheNuts
Copy link

I'm trying to figure out the best way to extend this class a little to assign appropriate "points" based on standings.

For example, let's say I'm ranking 5 things and the positions come out as 1, 1, 3, 3, and 5. And points are assigned by position as 10, 8, 6, 4, 2.

When there's a tie for 1st, the points for 1st and 2nd should be summed and distributed among the ties. In this case, the resulting points would be:

9, 9, 5, 5, 2.

Any thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment