Class: TieRankingLeaderboard

Inherits:
Leaderboard show all
Defined in:
lib/tie_ranking_leaderboard.rb

Constant Summary collapse

DEFAULT_OPTIONS =

Default options when creating a leaderboard. Page size is 25 and reverse is set to false, meaning various methods will return results in highest-to-lowest order.

{
  :page_size => DEFAULT_PAGE_SIZE,
  :reverse => false,
  :member_key => :member,
  :rank_key => :rank,
  :score_key => :score,
  :member_data_key => :member_data,
  :member_data_namespace => 'member_data',
  :ties_namespace => 'ties'
}

Constants inherited from Leaderboard

Leaderboard::DEFAULT_LEADERBOARD_REQUEST_OPTIONS, Leaderboard::DEFAULT_PAGE_SIZE, Leaderboard::DEFAULT_REDIS_HOST, Leaderboard::DEFAULT_REDIS_OPTIONS, Leaderboard::DEFAULT_REDIS_PORT, Leaderboard::VERSION

Instance Attribute Summary

Attributes inherited from Leaderboard

#leaderboard_name, #page_size, #reverse

Instance Method Summary collapse

Methods inherited from Leaderboard

#all_leaders, #all_leaders_from, #around_me, #around_me_in, #change_score_for, #check_member?, #check_member_in?, #delete_leaderboard, #disconnect, #expire_leaderboard, #expire_leaderboard_at, #intersect_leaderboards, #leaders, #leaders_in, #member_at, #member_at_in, #member_data_for, #member_data_for_in, #members_data_for, #members_data_for_in, #members_from_rank_range, #members_from_rank_range_in, #members_from_score_range, #members_from_score_range_in, #merge_leaderboards, #page_for, #page_for_in, #percentile_for, #percentile_for_in, #rank_for, #rank_member, #rank_member_if, #rank_member_if_in, #rank_members, #ranked_in_list, #remove_member, #remove_member_data, #remove_member_data_in, #remove_members_in_score_range, #remove_members_outside_rank, #remove_members_outside_rank_in, #score_and_rank_for, #score_for, #score_for_in, #score_for_percentile, #score_for_percentile_in, #top, #top_in, #total_members, #total_members_in, #total_members_in_score_range, #total_members_in_score_range_in, #total_pages, #total_pages_in, #total_scores, #total_scores_in, #update_member_data, #update_member_data_in

Constructor Details

#initialize(leaderboard_name, options = DEFAULT_OPTIONS, redis_options = DEFAULT_REDIS_OPTIONS) ⇒ TieRankingLeaderboard

Create a new instance of a leaderboard.

Examples

leaderboard = Leaderboard.new('highscores')
leaderboard = Leaderboard.new('highscores', {:page_size => 10})

Parameters:

  • leaderboard (String)

    Name of the leaderboard.

  • options (Hash) (defaults to: DEFAULT_OPTIONS)

    Options for the leaderboard such as :page_size.

  • redis_options (Hash) (defaults to: DEFAULT_REDIS_OPTIONS)

    Options for configuring Redis.



28
29
30
31
32
33
34
35
# File 'lib/tie_ranking_leaderboard.rb', line 28

def initialize(leaderboard_name, options = DEFAULT_OPTIONS, redis_options = DEFAULT_REDIS_OPTIONS)
  super

  leaderboard_options = DEFAULT_OPTIONS.dup
  leaderboard_options.merge!(options)

  @ties_namespace = leaderboard_options[:ties_namespace]
end

Instance Method Details

#change_score_for_member_in(leaderboard_name, member, delta, member_data = nil) ⇒ Object

Change the score for a member in the named leaderboard by a delta which can be positive or negative.

Parameters:

  • leaderboard_name (String)

    Name of the leaderboard.

  • member (String)

    Member name.

  • delta (float)

    Score change.

  • member_data (String) (defaults to: nil)

    Optional member data.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/tie_ranking_leaderboard.rb', line 54

def change_score_for_member_in(leaderboard_name, member, delta, member_data = nil)
  previous_score = score_for(member)
  new_score = (previous_score || 0) + delta

  total_members_at_previous_score = @redis_connection.zrevrangebyscore(leaderboard_name, previous_score, previous_score)

  @redis_connection.multi do |transaction|
    transaction.zadd(leaderboard_name, new_score, member)
    transaction.zadd(ties_leaderboard_key(leaderboard_name), new_score, new_score.to_f.to_s)
    transaction.hset(member_data_key(leaderboard_name), member, member_data) if member_data
  end

  if total_members_at_previous_score.length == 1
    @redis_connection.zrem(ties_leaderboard_key(leaderboard_name), previous_score.to_f.to_s)
  end
end

#delete_leaderboard_named(leaderboard_name) ⇒ Object

Delete the named leaderboard.

Parameters:

  • leaderboard_name (String)

    Name of the leaderboard.



40
41
42
43
44
45
46
# File 'lib/tie_ranking_leaderboard.rb', line 40

def delete_leaderboard_named(leaderboard_name)
  @redis_connection.multi do |transaction|
    transaction.del(leaderboard_name)
    transaction.del(member_data_key(leaderboard_name))
    transaction.del(ties_leaderboard_key(leaderboard_name))
  end
end

#expire_leaderboard_at_for(leaderboard_name, timestamp) ⇒ Object

Expire the given leaderboard at a specific UNIX timestamp. Do not use this with leaderboards that utilize member data as there is no facility to cascade the expiration out to the keys for the member data.

Parameters:

  • leaderboard_name (String)

    Name of the leaderboard.

  • timestamp (int)

    UNIX timestamp at which the leaderboard will be expired.



203
204
205
206
207
208
209
# File 'lib/tie_ranking_leaderboard.rb', line 203

def expire_leaderboard_at_for(leaderboard_name, timestamp)
  @redis_connection.multi do |transaction|
    transaction.expireat(leaderboard_name, timestamp)
    transaction.expireat(ties_leaderboard_key(leaderboard_name), timestamp)
    transaction.expireat(member_data_key(leaderboard_name), timestamp)
  end
end

#expire_leaderboard_for(leaderboard_name, seconds) ⇒ Object

Expire the given leaderboard in a set number of seconds. Do not use this with leaderboards that utilize member data as there is no facility to cascade the expiration out to the keys for the member data.

Parameters:

  • leaderboard_name (String)

    Name of the leaderboard.

  • seconds (int)

    Number of seconds after which the leaderboard will be expired.



189
190
191
192
193
194
195
# File 'lib/tie_ranking_leaderboard.rb', line 189

def expire_leaderboard_for(leaderboard_name, seconds)
  @redis_connection.multi do |transaction|
    transaction.expire(leaderboard_name, seconds)
    transaction.expire(ties_leaderboard_key(leaderboard_name), seconds)
    transaction.expire(member_data_key(leaderboard_name), seconds)
  end
end

#rank_for_in(leaderboard_name, member) ⇒ Object

Retrieve the rank for a member in the named leaderboard.

Parameters:

  • leaderboard_name (String)

    Name of the leaderboard.

  • member (String)

    Member name.

Returns:

  • the rank for a member in the leaderboard.



138
139
140
141
142
143
144
145
# File 'lib/tie_ranking_leaderboard.rb', line 138

def rank_for_in(leaderboard_name, member)
  member_score = score_for_in(leaderboard_name, member)
  if @reverse
    return @redis_connection.zrank(ties_leaderboard_key(leaderboard_name), member_score.to_f.to_s) + 1 rescue nil
  else
    return @redis_connection.zrevrank(ties_leaderboard_key(leaderboard_name), member_score.to_f.to_s) + 1 rescue nil
  end
end

#rank_member_across(leaderboards, member, score, member_data = nil) ⇒ Object

Rank a member across multiple leaderboards.

Parameters:

  • leaderboards (Array)

    Leaderboard names.

  • member (String)

    Member name.

  • score (float)

    Member score.

  • member_data (String) (defaults to: nil)

    Optional member data.



97
98
99
100
101
# File 'lib/tie_ranking_leaderboard.rb', line 97

def rank_member_across(leaderboards, member, score, member_data = nil)
  leaderboards.each do |leaderboard_name|
    rank_member_in(leaderboard_name, member, score, member_data)
  end
end

#rank_member_in(leaderboard_name, member, score, member_data = nil) ⇒ Object

Rank a member in the named leaderboard.

Parameters:

  • leaderboard_name (String)

    Name of the leaderboard.

  • member (String)

    Member name.

  • score (float)

    Member score.

  • member_data (String) (defaults to: nil)

    Optional member data.



77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/tie_ranking_leaderboard.rb', line 77

def rank_member_in(leaderboard_name, member, score, member_data = nil)
  member_score = @redis_connection.zscore(leaderboard_name, member) || nil
  can_delete_score = member_score &&
    members_from_score_range_in(leaderboard_name, member_score, member_score).length == 1 &&
    member_score != score

  @redis_connection.multi do |transaction|
    transaction.zadd(leaderboard_name, score, member)
    transaction.zadd(ties_leaderboard_key(leaderboard_name), score, score.to_f.to_s)
    transaction.zrem(ties_leaderboard_key(leaderboard_name), member_score.to_f.to_s) if can_delete_score
    transaction.hset(member_data_key(leaderboard_name), member, member_data) if member_data
  end
end

#rank_members_in(leaderboard_name, *members_and_scores) ⇒ Object

Rank an array of members in the named leaderboard.

Parameters:

  • leaderboard_name (String)

    Name of the leaderboard.

  • members_and_scores (Splat or Array)

    Variable list of members and scores



107
108
109
110
111
112
113
114
115
# File 'lib/tie_ranking_leaderboard.rb', line 107

def rank_members_in(leaderboard_name, *members_and_scores)
  if members_and_scores.is_a?(Array)
    members_and_scores.flatten!
  end

  members_and_scores.each_slice(2) do |member_and_score|
    rank_member_in(leaderboard_name, member_and_score[0], member_and_score[1])
  end
end

#ranked_in_list_in(leaderboard_name, members, options = {}) ⇒ Object

Retrieve a page of leaders from the named leaderboard for a given list of members.

Parameters:

  • leaderboard_name (String)

    Name of the leaderboard.

  • members (Array)

    Member names.

  • options (Hash) (defaults to: {})

    Options to be used when retrieving the page from the named leaderboard.

Returns:

  • a page of leaders from the named leaderboard for a given list of members.



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/tie_ranking_leaderboard.rb', line 218

def ranked_in_list_in(leaderboard_name, members, options = {})
  leaderboard_options = DEFAULT_LEADERBOARD_REQUEST_OPTIONS.dup
  leaderboard_options.merge!(options)

  ranks_for_members = []

  responses = @redis_connection.multi do |transaction|
    members.each do |member|
      if @reverse
        transaction.zrank(leaderboard_name, member)
      else
        transaction.zrevrank(leaderboard_name, member)
      end
      transaction.zscore(leaderboard_name, member)
    end
  end unless leaderboard_options[:members_only]

  members.each_with_index do |member, index|
    data = {}
    data[@member_key] = member
    unless leaderboard_options[:members_only]
      data[@score_key] = responses[index * 2 + 1].to_f if responses[index * 2 + 1]

      if @reverse
        data[@rank_key] = @redis_connection.zrank(ties_leaderboard_key(leaderboard_name), data[@score_key].to_s) + 1 rescue nil
      else
        data[@rank_key] = @redis_connection.zrevrank(ties_leaderboard_key(leaderboard_name), data[@score_key].to_s) + 1 rescue nil
      end

      if data[@rank_key] == nil
        next unless leaderboard_options[:include_missing]
      end
    end

    ranks_for_members << data
  end

  if leaderboard_options[:with_member_data]
    included_members = ranks_for_members.collect { |member| member[@member_key] }
    members_data_for_in(leaderboard_name, included_members).each_with_index do |member_data, index|
      ranks_for_members[index][@member_data_key] = member_data
    end
  end

  case leaderboard_options[:sort_by]
  when :rank
    ranks_for_members = ranks_for_members.sort_by { |member| member[@rank_key] }
  when :score
    ranks_for_members = ranks_for_members.sort_by { |member| member[@score_key] }
  end

  ranks_for_members
end

#remove_member_from(leaderboard_name, member) ⇒ Object

Remove a member from the named leaderboard.

Parameters:

  • leaderboard_name (String)

    Name of the leaderboard.

  • member (String)

    Member name.



121
122
123
124
125
126
127
128
129
130
# File 'lib/tie_ranking_leaderboard.rb', line 121

def remove_member_from(leaderboard_name, member)
  member_score = @redis_connection.zscore(leaderboard_name, member) || nil
  can_delete_score = member_score && members_from_score_range_in(leaderboard_name, member_score, member_score).length == 1

  @redis_connection.multi do |transaction|
    transaction.zrem(leaderboard_name, member)
    transaction.zrem(ties_leaderboard_key(leaderboard_name), member_score.to_f.to_s) if can_delete_score
    transaction.hdel(member_data_key(leaderboard_name), member)
  end
end

#remove_members_in_score_range_in(leaderboard_name, min_score, max_score) ⇒ Object

Remove members from the named leaderboard in a given score range.

Parameters:

  • leaderboard_name (String)

    Name of the leaderboard.

  • min_score (float)

    Minimum score.

  • max_score (float)

    Maximum score.



176
177
178
179
180
181
# File 'lib/tie_ranking_leaderboard.rb', line 176

def remove_members_in_score_range_in(leaderboard_name, min_score, max_score)
  @redis_connection.multi do |transaction|
    transaction.zremrangebyscore(leaderboard_name, min_score, max_score)
    transaction.zremrangebyscore(ties_leaderboard_key(leaderboard_name), min_score, max_score)
  end
end

#score_and_rank_for_in(leaderboard_name, member) ⇒ Object

Retrieve the score and rank for a member in the named leaderboard.

Parameters:

  • leaderboard_name (String)

    Name of the leaderboard.

  • member (String)

    Member name.

Returns:

  • the score and rank for a member in the named leaderboard as a Hash.



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/tie_ranking_leaderboard.rb', line 153

def score_and_rank_for_in(leaderboard_name, member)
  member_score = @redis_connection.zscore(leaderboard_name, member)

  responses = @redis_connection.multi do |transaction|
    transaction.zscore(leaderboard_name, member)
    if @reverse
      transaction.zrank(ties_leaderboard_key(leaderboard_name), member_score.to_f.to_s)
    else
      transaction.zrevrank(ties_leaderboard_key(leaderboard_name), member_score.to_f.to_s)
    end
  end

  responses[0] = responses[0].to_f if responses[0]
  responses[1] = responses[1] + 1 rescue nil

  {@member_key => member, @score_key => responses[0], @rank_key => responses[1]}
end