leaderboard 3.6.0 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 457a795959a19eb87e92f2e15df972997cf862c0
4
- data.tar.gz: 46efa5becb8ddcada520c3ca5b37235c9d751507
3
+ metadata.gz: 73f4fb333c04f1aef154ecf0469cdea2f3602c24
4
+ data.tar.gz: 6ada4c6ec175b491b2dec536b4961e3bd472eb54
5
5
  SHA512:
6
- metadata.gz: d1a7accf722c5fb0e1d1e24a897e173a88296694fdf94dabfc9289569a3543482c355c411365fab3ded182d91ba4ceb614708d21073205c54f31a0afbfb4d8f9
7
- data.tar.gz: 0582a7737d0b07933f3665644c7436e90e2ed59684698bd0f4971dc061cd9b2aaa614024ca70636712364029fe29ee124fb64c2de3beb40e9424146e18e780c3
6
+ metadata.gz: 104e46c727ed24d53883a4f45260af84fc3518d863ef662ccafab806152aea83aea40a5a740c80452535c2dcf25853cd54ce52aa7b398f9b019638654f44e99c
7
+ data.tar.gz: 96f0f5756c4a17bd0dd9e573a3e8b682f961168f7a47e2fb0f0100cd6ec427457afdacb194367bf01a6f38eb5032bdcd3458f344a8683450dc8697a5a31d62c6
data/.rspec CHANGED
@@ -1,3 +1,3 @@
1
1
  --color
2
- --format nested
2
+ --format documentation
3
3
  --order random
data/.ruby-version CHANGED
@@ -1,3 +1 @@
1
- ruby-1.8.7
2
- ruby-1.9.3
3
- ruby-2.0.0
1
+ ruby-2.1.2
data/.travis.yml CHANGED
@@ -1,7 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.0.0
3
+ - 2.1.2
4
4
  - 1.9.3
5
- - 1.8.7
6
5
  services:
7
6
  - redis-server
data/CHANGELOG.markdown CHANGED
@@ -1,5 +1,9 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 3.7.0 (2014-07-28)
4
+
5
+ * Add support for tie handling in leaderboards [#46](https://github.com/agoragames/leaderboard/pull/46)
6
+
3
7
  ## 3.6.0 (2014-02-15)
4
8
 
5
9
  * Allow for customization of member_data namespace [#45](https://github.com/agoragames/leaderboard/pull/45)
data/README.markdown CHANGED
@@ -21,7 +21,7 @@ check out the [Redis documentation](http://redis.io/documentation).
21
21
 
22
22
  ## Compatibility
23
23
 
24
- The gem has been built and tested under Ruby 1.8.7, Ruby 1.9.2 and Ruby 1.9.3.
24
+ The gem has been built and tested under Ruby 1.9.3 and Ruby 2.1.2.
25
25
 
26
26
  ## Usage
27
27
 
@@ -302,6 +302,38 @@ Use this method to do bulk insert of data, but be mindful of the amount of data
302
302
  highscore_lb.rank_member_across(['highscores', 'more_highscores'], 'david', 50000, { :member_name => "david" })
303
303
  ```
304
304
 
305
+ ### Alternate leaderboard types
306
+
307
+ The leaderboard library offers 3 styles of ranking. This is only an issue for members with the same score in a leaderboard.
308
+
309
+ Default: The `Leaderboard` class uses the default Redis sorted set ordering, whereby different members having the same score are ordered lexicographically. As per the Redis documentation on Redis sorted sets, "The lexicographic ordering used is binary, it compares strings as array of bytes."
310
+
311
+ Tie ranking: The `TieRankingLeaderboard` subclass of `Leaderboard` allows you to define a leaderboard where members with the same score are given the same rank. For example, members in a leaderboard with the associated scores would have the ranks of:
312
+
313
+ ```
314
+ | member | score | rank |
315
+ -----------------------------
316
+ | member_1 | 50 | 1 |
317
+ | member_2 | 50 | 1 |
318
+ | member_3 | 30 | 2 |
319
+ | member_4 | 30 | 2 |
320
+ | member_5 | 10 | 3 |
321
+ ```
322
+
323
+ The `TieRankingLeaderboard` accepts one additional option, `:ties_namespace` (default: ties), when initializing a new instance of this class. Please note that in its current implementation, the `TieRankingLeaderboard` class uses an additional sorted set to rank the scores, so please keep this in mind when you are doing any capacity planning for Redis with respect to memory usage.
324
+
325
+ Competition ranking: The `CompetitionRankingLeaderboard` subclass of `Leaderboard` allows you to define a leaderboard where members with the same score will have the same rank, and then a gap is left in the ranking numbers. For example, members in a leaderboard with the associated scores would have the ranks of:
326
+
327
+ ```
328
+ | member | score | rank |
329
+ -----------------------------
330
+ | member_1 | 50 | 1 |
331
+ | member_2 | 50 | 1 |
332
+ | member_3 | 30 | 3 |
333
+ | member_4 | 30 | 3 |
334
+ | member_5 | 10 | 5 |
335
+ ```
336
+
305
337
  ### Other useful methods
306
338
 
307
339
  ```
data/Rakefile CHANGED
@@ -9,8 +9,3 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
9
9
  end
10
10
 
11
11
  task :default => :spec
12
-
13
- desc "Run the specs against Ruby 1.8.7, 1.9.3, 2.0.0"
14
- task :test_rubies do
15
- system "rvm 1.8.7@leaderboard_gem,1.9.3@leaderboard_gem,2.0.0@leaderboard_gem do rake spec"
16
- end
data/leaderboard.gemspec CHANGED
@@ -22,8 +22,5 @@ Gem::Specification.new do |s|
22
22
 
23
23
  s.add_dependency('redis')
24
24
  s.add_development_dependency('rake')
25
- if '1.8.7'.eql?(RUBY_VERSION)
26
- s.add_development_dependency('SystemTimer')
27
- end
28
25
  s.add_development_dependency('rspec')
29
26
  end
@@ -0,0 +1,98 @@
1
+ require 'leaderboard'
2
+
3
+ class CompetitionRankingLeaderboard < Leaderboard
4
+ # Retrieve the rank for a member in the named leaderboard.
5
+ #
6
+ # @param leaderboard_name [String] Name of the leaderboard.
7
+ # @param member [String] Member name.
8
+ #
9
+ # @return the rank for a member in the leaderboard.
10
+ def rank_for_in(leaderboard_name, member)
11
+ member_score = score_for_in(leaderboard_name, member)
12
+ if @reverse
13
+ return @redis_connection.zcount(leaderboard_name, '-inf', "(#{member_score}") + 1 rescue nil
14
+ else
15
+ return @redis_connection.zcount(leaderboard_name, "(#{member_score}", '+inf') + 1 rescue nil
16
+ end
17
+ end
18
+
19
+ # Retrieve the score and rank for a member in the named leaderboard.
20
+ #
21
+ # @param leaderboard_name [String]Name of the leaderboard.
22
+ # @param member [String] Member name.
23
+ #
24
+ # @return the score and rank for a member in the named leaderboard as a Hash.
25
+ def score_and_rank_for_in(leaderboard_name, member)
26
+ responses = @redis_connection.multi do |transaction|
27
+ transaction.zscore(leaderboard_name, member)
28
+ if @reverse
29
+ transaction.zrank(leaderboard_name, member)
30
+ else
31
+ transaction.zrevrank(leaderboard_name, member)
32
+ end
33
+ end
34
+
35
+ responses[0] = responses[0].to_f if responses[0]
36
+ responses[1] =
37
+ if @reverse
38
+ @redis_connection.zcount(leaderboard_name, '-inf', "(#{responses[0]}") + 1 rescue nil
39
+ else
40
+ @redis_connection.zcount(leaderboard_name, "(#{responses[0]}", '+inf') + 1 rescue nil
41
+ end
42
+
43
+ {@member_key => member, @score_key => responses[0], @rank_key => responses[1]}
44
+ end
45
+
46
+ # Retrieve a page of leaders from the named leaderboard for a given list of members.
47
+ #
48
+ # @param leaderboard_name [String] Name of the leaderboard.
49
+ # @param members [Array] Member names.
50
+ # @param options [Hash] Options to be used when retrieving the page from the named leaderboard.
51
+ #
52
+ # @return a page of leaders from the named leaderboard for a given list of members.
53
+ def ranked_in_list_in(leaderboard_name, members, options = {})
54
+ leaderboard_options = DEFAULT_LEADERBOARD_REQUEST_OPTIONS.dup
55
+ leaderboard_options.merge!(options)
56
+
57
+ ranks_for_members = []
58
+
59
+ responses = @redis_connection.multi do |transaction|
60
+ members.each do |member|
61
+ if @reverse
62
+ transaction.zrank(leaderboard_name, member)
63
+ else
64
+ transaction.zrevrank(leaderboard_name, member)
65
+ end
66
+ transaction.zscore(leaderboard_name, member)
67
+ end
68
+ end unless leaderboard_options[:members_only]
69
+
70
+ members.each_with_index do |member, index|
71
+ data = {}
72
+ data[@member_key] = member
73
+ unless leaderboard_options[:members_only]
74
+ data[@score_key] = responses[index * 2 + 1].to_f if responses[index * 2 + 1]
75
+ if @reverse
76
+ data[@rank_key] = @redis_connection.zcount(leaderboard_name, '-inf', "(#{data[@score_key]}") + 1 rescue nil
77
+ else
78
+ data[@rank_key] = @redis_connection.zcount(leaderboard_name, "(#{data[@score_key]}", '+inf') + 1 rescue nil
79
+ end
80
+ end
81
+
82
+ if leaderboard_options[:with_member_data]
83
+ data[@member_data_key] = member_data_for_in(leaderboard_name, member)
84
+ end
85
+
86
+ ranks_for_members << data
87
+ end
88
+
89
+ case leaderboard_options[:sort_by]
90
+ when :rank
91
+ ranks_for_members = ranks_for_members.sort_by { |member| member[@rank_key] }
92
+ when :score
93
+ ranks_for_members = ranks_for_members.sort_by { |member| member[@score_key] }
94
+ end
95
+
96
+ ranks_for_members
97
+ end
98
+ end
data/lib/leaderboard.rb CHANGED
@@ -966,7 +966,7 @@ class Leaderboard
966
966
  @redis_connection.zinterstore(destination, keys.insert(0, @leaderboard_name), options)
967
967
  end
968
968
 
969
- private
969
+ protected
970
970
 
971
971
  # Key for retrieving optional member data.
972
972
  #
@@ -1,3 +1,3 @@
1
1
  class Leaderboard
2
- VERSION = '3.6.0'.freeze
2
+ VERSION = '3.7.0'.freeze
3
3
  end
@@ -0,0 +1,253 @@
1
+ require 'leaderboard'
2
+
3
+ class TieRankingLeaderboard < Leaderboard
4
+ # Default options when creating a leaderboard. Page size is 25 and reverse
5
+ # is set to false, meaning various methods will return results in
6
+ # highest-to-lowest order.
7
+ DEFAULT_OPTIONS = {
8
+ :page_size => DEFAULT_PAGE_SIZE,
9
+ :reverse => false,
10
+ :member_key => :member,
11
+ :rank_key => :rank,
12
+ :score_key => :score,
13
+ :member_data_key => :member_data,
14
+ :member_data_namespace => 'member_data',
15
+ :ties_namespace => 'ties'
16
+ }
17
+
18
+ # Create a new instance of a leaderboard.
19
+ #
20
+ # @param leaderboard [String] Name of the leaderboard.
21
+ # @param options [Hash] Options for the leaderboard such as +:page_size+.
22
+ # @param redis_options [Hash] Options for configuring Redis.
23
+ #
24
+ # Examples
25
+ #
26
+ # leaderboard = Leaderboard.new('highscores')
27
+ # leaderboard = Leaderboard.new('highscores', {:page_size => 10})
28
+ def initialize(leaderboard_name, options = DEFAULT_OPTIONS, redis_options = DEFAULT_REDIS_OPTIONS)
29
+ super
30
+
31
+ leaderboard_options = DEFAULT_OPTIONS.dup
32
+ leaderboard_options.merge!(options)
33
+
34
+ @ties_namespace = leaderboard_options[:ties_namespace]
35
+ end
36
+
37
+ # Delete the named leaderboard.
38
+ #
39
+ # @param leaderboard_name [String] Name of the leaderboard.
40
+ def delete_leaderboard_named(leaderboard_name)
41
+ @redis_connection.multi do |transaction|
42
+ transaction.del(leaderboard_name)
43
+ transaction.del(member_data_key(leaderboard_name))
44
+ transaction.del(ties_leaderboard_key(leaderboard_name))
45
+ end
46
+ end
47
+
48
+ # Rank a member in the named leaderboard.
49
+ #
50
+ # @param leaderboard_name [String] Name of the leaderboard.
51
+ # @param member [String] Member name.
52
+ # @param score [float] Member score.
53
+ # @param member_data [String] Optional member data.
54
+ def rank_member_in(leaderboard_name, member, score, member_data = nil)
55
+ @redis_connection.multi do |transaction|
56
+ transaction.zadd(leaderboard_name, score, member)
57
+ transaction.zadd(ties_leaderboard_key(leaderboard_name), score, score.to_f.to_s)
58
+ transaction.hset(member_data_key(leaderboard_name), member, member_data) if member_data
59
+ end
60
+ end
61
+
62
+ # Rank a member across multiple leaderboards.
63
+ #
64
+ # @param leaderboards [Array] Leaderboard names.
65
+ # @param member [String] Member name.
66
+ # @param score [float] Member score.
67
+ # @param member_data [String] Optional member data.
68
+ def rank_member_across(leaderboards, member, score, member_data = nil)
69
+ @redis_connection.multi do |transaction|
70
+ leaderboards.each do |leaderboard_name|
71
+ transaction.zadd(leaderboard_name, score, member)
72
+ transaction.zadd(ties_leaderboard_key(leaderboard_name), score, score.to_f.to_s)
73
+ transaction.hset(member_data_key(leaderboard_name), member, member_data) if member_data
74
+ end
75
+ end
76
+ end
77
+
78
+ # Rank an array of members in the named leaderboard.
79
+ #
80
+ # @param leaderboard_name [String] Name of the leaderboard.
81
+ # @param members_and_scores [Splat or Array] Variable list of members and scores
82
+ def rank_members_in(leaderboard_name, *members_and_scores)
83
+ if members_and_scores.is_a?(Array)
84
+ members_and_scores.flatten!
85
+ end
86
+
87
+ @redis_connection.multi do |transaction|
88
+ members_and_scores.each_slice(2) do |member_and_score|
89
+ transaction.zadd(leaderboard_name, member_and_score[1], member_and_score[0])
90
+ transaction.zadd(ties_leaderboard_key(leaderboard_name), member_and_score[0], member_and_score[0].to_f.to_s)
91
+ end
92
+ end
93
+ end
94
+
95
+ # Remove a member from the named leaderboard.
96
+ #
97
+ # @param leaderboard_name [String] Name of the leaderboard.
98
+ # @param member [String] Member name.
99
+ def remove_member_from(leaderboard_name, member)
100
+ member_score = @redis_connection.zscore(leaderboard_name, member) || nil
101
+ can_delete_score = member_score && members_from_score_range_in(leaderboard_name, member_score, member_score).length == 1
102
+
103
+ @redis_connection.multi do |transaction|
104
+ transaction.zrem(leaderboard_name, member)
105
+ transaction.zrem(ties_leaderboard_key(leaderboard_name), member_score.to_f.to_s) if can_delete_score
106
+ transaction.hdel(member_data_key(leaderboard_name), member)
107
+ end
108
+ end
109
+
110
+ # Retrieve the rank for a member in the named leaderboard.
111
+ #
112
+ # @param leaderboard_name [String] Name of the leaderboard.
113
+ # @param member [String] Member name.
114
+ #
115
+ # @return the rank for a member in the leaderboard.
116
+ def rank_for_in(leaderboard_name, member)
117
+ member_score = score_for_in(leaderboard_name, member)
118
+ if @reverse
119
+ return @redis_connection.zrank(ties_leaderboard_key(leaderboard_name), member_score.to_f.to_s) + 1 rescue nil
120
+ else
121
+ return @redis_connection.zrevrank(ties_leaderboard_key(leaderboard_name), member_score.to_f.to_s) + 1 rescue nil
122
+ end
123
+ end
124
+
125
+ # Retrieve the score and rank for a member in the named leaderboard.
126
+ #
127
+ # @param leaderboard_name [String]Name of the leaderboard.
128
+ # @param member [String] Member name.
129
+ #
130
+ # @return the score and rank for a member in the named leaderboard as a Hash.
131
+ def score_and_rank_for_in(leaderboard_name, member)
132
+ member_score = @redis_connection.zscore(leaderboard_name, member)
133
+
134
+ responses = @redis_connection.multi do |transaction|
135
+ transaction.zscore(leaderboard_name, member)
136
+ if @reverse
137
+ transaction.zrank(ties_leaderboard_key(leaderboard_name), member_score.to_f.to_s)
138
+ else
139
+ transaction.zrevrank(ties_leaderboard_key(leaderboard_name), member_score.to_f.to_s)
140
+ end
141
+ end
142
+
143
+ responses[0] = responses[0].to_f if responses[0]
144
+ responses[1] = responses[1] + 1 rescue nil
145
+
146
+ {@member_key => member, @score_key => responses[0], @rank_key => responses[1]}
147
+ end
148
+
149
+ # Remove members from the named leaderboard in a given score range.
150
+ #
151
+ # @param leaderboard_name [String] Name of the leaderboard.
152
+ # @param min_score [float] Minimum score.
153
+ # @param max_score [float] Maximum score.
154
+ def remove_members_in_score_range_in(leaderboard_name, min_score, max_score)
155
+ @redis_connection.multi do |transaction|
156
+ transaction.zremrangebyscore(leaderboard_name, min_score, max_score)
157
+ transaction.zremrangebyscore(ties_leaderboard_key(leaderboard_name), min_score, max_score)
158
+ end
159
+ end
160
+
161
+ # Expire the given leaderboard in a set number of seconds. Do not use this with
162
+ # leaderboards that utilize member data as there is no facility to cascade the
163
+ # expiration out to the keys for the member data.
164
+ #
165
+ # @param leaderboard_name [String] Name of the leaderboard.
166
+ # @param seconds [int] Number of seconds after which the leaderboard will be expired.
167
+ def expire_leaderboard_for(leaderboard_name, seconds)
168
+ @redis_connection.multi do |transaction|
169
+ transaction.expire(leaderboard_name, seconds)
170
+ transaction.expire(ties_leaderboard_key(leaderboard_name), seconds)
171
+ transaction.expire(member_data_key(leaderboard_name), seconds)
172
+ end
173
+ end
174
+
175
+ # Expire the given leaderboard at a specific UNIX timestamp. Do not use this with
176
+ # leaderboards that utilize member data as there is no facility to cascade the
177
+ # expiration out to the keys for the member data.
178
+ #
179
+ # @param leaderboard_name [String] Name of the leaderboard.
180
+ # @param timestamp [int] UNIX timestamp at which the leaderboard will be expired.
181
+ def expire_leaderboard_at_for(leaderboard_name, timestamp)
182
+ @redis_connection.multi do |transaction|
183
+ transaction.expireat(leaderboard_name, timestamp)
184
+ transaction.expireat(ties_leaderboard_key(leaderboard_name), timestamp)
185
+ transaction.expireat(member_data_key(leaderboard_name), timestamp)
186
+ end
187
+ end
188
+
189
+ # Retrieve a page of leaders from the named leaderboard for a given list of members.
190
+ #
191
+ # @param leaderboard_name [String] Name of the leaderboard.
192
+ # @param members [Array] Member names.
193
+ # @param options [Hash] Options to be used when retrieving the page from the named leaderboard.
194
+ #
195
+ # @return a page of leaders from the named leaderboard for a given list of members.
196
+ def ranked_in_list_in(leaderboard_name, members, options = {})
197
+ leaderboard_options = DEFAULT_LEADERBOARD_REQUEST_OPTIONS.dup
198
+ leaderboard_options.merge!(options)
199
+
200
+ ranks_for_members = []
201
+
202
+ responses = @redis_connection.multi do |transaction|
203
+ members.each do |member|
204
+ if @reverse
205
+ transaction.zrank(leaderboard_name, member)
206
+ else
207
+ transaction.zrevrank(leaderboard_name, member)
208
+ end
209
+ transaction.zscore(leaderboard_name, member)
210
+ end
211
+ end unless leaderboard_options[:members_only]
212
+
213
+ members.each_with_index do |member, index|
214
+ data = {}
215
+ data[@member_key] = member
216
+ unless leaderboard_options[:members_only]
217
+ data[@score_key] = responses[index * 2 + 1].to_f if responses[index * 2 + 1]
218
+
219
+ if @reverse
220
+ data[@rank_key] = @redis_connection.zrank(ties_leaderboard_key(leaderboard_name), data[@score_key].to_s) + 1 rescue nil
221
+ else
222
+ data[@rank_key] = @redis_connection.zrevrank(ties_leaderboard_key(leaderboard_name), data[@score_key].to_s) + 1 rescue nil
223
+ end
224
+ end
225
+
226
+ if leaderboard_options[:with_member_data]
227
+ data[@member_data_key] = member_data_for_in(leaderboard_name, member)
228
+ end
229
+
230
+ ranks_for_members << data
231
+ end
232
+
233
+ case leaderboard_options[:sort_by]
234
+ when :rank
235
+ ranks_for_members = ranks_for_members.sort_by { |member| member[@rank_key] }
236
+ when :score
237
+ ranks_for_members = ranks_for_members.sort_by { |member| member[@score_key] }
238
+ end
239
+
240
+ ranks_for_members
241
+ end
242
+
243
+ protected
244
+
245
+ # Key for ties leaderboard.
246
+ #
247
+ # @param leaderboard_name [String] Name of the leaderboard.
248
+ #
249
+ # @return a key in the form of +leaderboard_name:ties_namespace+
250
+ def ties_leaderboard_key(leaderboard_name)
251
+ "#{leaderboard_name}:#{@ties_namespace}"
252
+ end
253
+ end