multi-leaderboard 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.8.7
@@ -0,0 +1,17 @@
1
+ # leaderboard 1.0.2 (2011-02-25)
2
+
3
+ * Adding `XXX_to`, `XXX_for`, `XXX_in` and `XXX_from` methods that will allow you to set the leaderboard name to interact with outside of creating a new object
4
+ * Added `merge_leaderboards(destination, keys, options = {:aggregate => :min})` method to merge leaderboards given by keys with this leaderboard into destination
5
+ * Added `intersect_leaderboards(destination, keys, options = {:aggregate => :min})` method to intersect leaderboards given by keys with this leaderboard into destination
6
+
7
+ # leaderboard 1.0.1 (2011-02-16)
8
+
9
+ * `redis_options` can be passed in the initializer to pass options for the connection to Redis
10
+ * `page_size` is now settable outside of the initializer
11
+ * `check_member?(member)`: Check to see whether member is in the leaderboard
12
+ * `score_and_rank_for(member, use_zero_index_for_rank = false)`: Retrieve the score and rank for a member in a single call
13
+ * `remove_members_in_score_range(min_score, max_score)`: Remove members from the leaderboard within a score range
14
+
15
+ # leaderboard 1.0.0
16
+
17
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Add dependencies to develop your gem here.
4
+ # Include everything needed to run rake, tests, features, etc.
5
+ group :development do
6
+ gem "bundler", "~> 1.0.0"
7
+ gem "jeweler", "~> 1.5.2"
8
+ gem "rcov", ">= 0"
9
+ end
10
+
11
+ gem 'redis', "~> 2.1.1"
12
+
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 David Czarnecki
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,170 @@
1
+ = leaderboard
2
+
3
+ Leaderboards backed by Redis in Ruby, http://redis.io.
4
+
5
+ Builds off ideas proposed in http://blog.agoragames.com/2011/01/01/creating-high-score-tables-leaderboards-using-redis/.
6
+
7
+ == Installation
8
+
9
+ Install the gem:
10
+
11
+ gem install leaderboard
12
+
13
+ Make sure your redis server is running! Redis configuration is outside the scope of this README, but
14
+ check out the Redis documentation, http://redis.io/documentation.
15
+
16
+ == Compatibility
17
+
18
+ The gem has been built and tested under Ruby 1.8.7 and Ruby 1.9.2
19
+
20
+ == Usage
21
+
22
+ Create a new leaderboard or attach to an existing leaderboard named 'highscores':
23
+
24
+ ruby-1.8.7-p302 > highscore_lb = Leaderboard.new('highscores')
25
+ => #<Leaderboard:0x1018e4250 @page_size=25, @port=6379, @host="localhost", @redis_connection=#<Redis client v2.1.1 connected to redis://localhost:6379/0 (Redis v2.1.10)>, @leaderboard_name="highscores">
26
+
27
+ If you need to pass in options for Redis, you can do this with the redis_options parameter:
28
+
29
+ redis_options = {:host => 'localhost', :port => 6379, :password => 'password', :db => 'some_redis_db'}
30
+ highscore_lb = Leaderboard.new('highscores', redis_options[:host], redis_options[:port], Leaderboard::DEFAULT_PAGE_SIZE, redis_options))
31
+
32
+ You can set the page size to something other than the default page size (25):
33
+
34
+ ruby-1.8.7-p302 > highscore_lb.page_size = 5
35
+ => 5
36
+ ruby-1.8.7-p302 > highscore_lb
37
+ => #<Leaderboard:0x1018e2130 @leaderboard_name="highscores", @page_size=5, @port=6379, @redis_connection=#<Redis client v2.1.1 connected to redis://localhost:6379/0 (Redis v2.1.10)>, @host="localhost", @redis_options={:host=>"localhost", :port=>6379}>
38
+
39
+ Add members to your leaderboard:
40
+
41
+ ruby-1.8.7-p302 > 1.upto(10) do |index|
42
+ ruby-1.8.7-p302 > highscore_lb.add_member("member_#{index}", index)
43
+ ruby-1.8.7-p302 ?> end
44
+ => 1
45
+
46
+ Get some information about your leaderboard:
47
+
48
+ ruby-1.8.7-p302 > highscore_lb.total_members
49
+ => 10
50
+ ruby-1.8.7-p302 > highscore_lb.total_pages
51
+ => 1
52
+
53
+ Get some information about a specific member(s) in the leaderboard:
54
+
55
+ ruby-1.8.7-p302 > highscore_lb.score_for('member_4')
56
+ => 4.0
57
+ ruby-1.8.7-p302 > highscore_lb.rank_for('member_4')
58
+ => 7
59
+ ruby-1.8.7-p302 > highscore_lb.rank_for('member_10')
60
+ => 1
61
+
62
+ Get page 1 in the leaderboard:
63
+
64
+ ruby-1.8.7-p302 > highscore_lb.leaders(1)
65
+ => [{:member=>"member_10", :rank=>1, :score=>"10"}, {:member=>"member_9", :rank=>2, :score=>"9"}, {:member=>"member_8", :rank=>3, :score=>"8"}, {:member=>"member_7", :rank=>4, :score=>"7"}, {:member=>"member_6", :rank=>5, :score=>"6"}, {:member=>"member_5", :rank=>6, :score=>"5"}, {:member=>"member_4", :rank=>7, :score=>"4"}, {:member=>"member_3", :rank=>8, :score=>"3"}, {:member=>"member_2", :rank=>9, :score=>"2"}, {:member=>"member_1", :rank=>10, :score=>"1"}]
66
+
67
+ Add more members to your leaderboard:
68
+
69
+ ruby-1.8.7-p302 > 50.upto(95) do |index|
70
+ ruby-1.8.7-p302 > highscore_lb.add_member("member_#{index}", index)
71
+ ruby-1.8.7-p302 ?> end
72
+ => 50
73
+ ruby-1.8.7-p302 > highscore_lb.total_pages
74
+ => 3
75
+
76
+ Get an "Around Me" leaderboard for a member:
77
+
78
+ ruby-1.8.7-p302 > highscore_lb.around_me('member_53')
79
+ => [{:member=>"member_65", :rank=>31, :score=>"65"}, {:member=>"member_64", :rank=>32, :score=>"64"}, {:member=>"member_63", :rank=>33, :score=>"63"}, {:member=>"member_62", :rank=>34, :score=>"62"}, {:member=>"member_61", :rank=>35, :score=>"61"}, {:member=>"member_60", :rank=>36, :score=>"60"}, {:member=>"member_59", :rank=>37, :score=>"59"}, {:member=>"member_58", :rank=>38, :score=>"58"}, {:member=>"member_57", :rank=>39, :score=>"57"}, {:member=>"member_56", :rank=>40, :score=>"56"}, {:member=>"member_55", :rank=>41, :score=>"55"}, {:member=>"member_54", :rank=>42, :score=>"54"}, {:member=>"member_53", :rank=>43, :score=>"53"}, {:member=>"member_52", :rank=>44, :score=>"52"}, {:member=>"member_51", :rank=>45, :score=>"51"}, {:member=>"member_50", :rank=>46, :score=>"50"}, {:member=>"member_10", :rank=>47, :score=>"10"}, {:member=>"member_9", :rank=>48, :score=>"9"}, {:member=>"member_8", :rank=>49, :score=>"8"}, {:member=>"member_7", :rank=>50, :score=>"7"}, {:member=>"member_6", :rank=>51, :score=>"6"}, {:member=>"member_5", :rank=>52, :score=>"5"}, {:member=>"member_4", :rank=>53, :score=>"4"}, {:member=>"member_3", :rank=>54, :score=>"3"}, {:member=>"member_2", :rank=>55, :score=>"2"}]
80
+
81
+ Get rank and score for an arbitrary list of members (e.g. friends):
82
+
83
+ ruby-1.8.7-p302 > highscore_lb.ranked_in_list(['member_1', 'member_62', 'member_67'], true)
84
+ => [{:rank=>55, :member=>"member_1", :score=>1.0}, {:rank=>33, :member=>"member_62", :score=>62.0}, {:rank=>28, :member=>"member_67", :score=>67.0}]
85
+
86
+ === Other useful methods
87
+
88
+ remove_member(member): Remove a member from the leaderboard
89
+ total_members_in_score_range(min_score, max_score): Count the number of members within a score range in the leaderboard
90
+ change_score_for(member, delta): Change the score for a member by some amount delta (delta could be positive or negative)
91
+ check_member?(member): Check to see whether member is in the leaderboard
92
+ score_and_rank_for(member, use_zero_index_for_rank = false): Retrieve the score and rank for a member in a single call
93
+ remove_members_in_score_range(min_score, max_score): Remove members from the leaderboard within a score range
94
+ merge_leaderboards(destination, keys, options = {:aggregate => :min}): Merge leaderboards given by keys with this leaderboard into destination
95
+ intersect_leaderboards(destination, keys, options = {:aggregate => :min}): Intersect leaderboards given by keys with this leaderboard into destination
96
+
97
+ == Performance Metrics
98
+
99
+ 10 million sequential scores insert:
100
+
101
+ ruby-1.8.7-p302 > insert_time = Benchmark.measure do
102
+ ruby-1.8.7-p302 > 1.upto(10000000) do |index|
103
+ ruby-1.8.7-p302 > highscore_lb.add_member("member_#{index}", index)
104
+ ruby-1.8.7-p302 ?> end
105
+ ruby-1.8.7-p302 ?> end
106
+ => #<Benchmark::Tms:0x101605660 @label="", @stime=173.61, @total=577.52, @real=911.718175172806, @utime=403.91, @cstime=0.0, @cutime=0.0>
107
+
108
+ Average time to request an arbitrary page from the leaderboard:
109
+
110
+ ruby-1.8.7-p302 > requests_to_make = 50000
111
+ => 50000
112
+ ruby-1.8.7-p302 > lb_request_time = 0
113
+ => 0
114
+ ruby-1.8.7-p302 > 1.upto(requests_to_make) do
115
+ ruby-1.8.7-p302 > lb_request_time += Benchmark.measure do
116
+ ruby-1.8.7-p302 > highscore_lb.leaders(rand(highscore_lb.total_pages))
117
+ ruby-1.8.7-p302 ?> end.total
118
+ ruby-1.8.7-p302 ?> end
119
+ => 1
120
+ ruby-1.8.7-p302 >
121
+ ruby-1.8.7-p302 > p lb_request_time / requests_to_make
122
+ 0.001808
123
+ => nil
124
+
125
+ 10 million random scores insert:
126
+
127
+ ruby-1.8.7-p302 > insert_time = Benchmark.measure do
128
+ ruby-1.8.7-p302 > 1.upto(10000000) do |index|
129
+ ruby-1.8.7-p302 > highscore_lb.add_member("member_#{index}", rand(50000000))
130
+ ruby-1.8.7-p302 ?> end
131
+ ruby-1.8.7-p302 ?> end
132
+ => #<Benchmark::Tms:0x10164ebf8 @label="", @stime=172.94, @total=577.91, @real=1356.57155895233, @utime=404.97, @cstime=0.0, @cutime=0.0>
133
+
134
+ Average time to request an arbitrary page from the leaderboard:
135
+
136
+ ruby-1.8.7-p302 > requests_to_make = 50000
137
+ => 50000
138
+ ruby-1.8.7-p302 > lb_request_time = 0
139
+ => 0
140
+ ruby-1.8.7-p302 > 1.upto(requests_to_make) do
141
+ ruby-1.8.7-p302 > lb_request_time += Benchmark.measure do
142
+ ruby-1.8.7-p302 > highscore_lb.leaders(rand(highscore_lb.total_pages))
143
+ ruby-1.8.7-p302 ?> end.total
144
+ ruby-1.8.7-p302 ?> end
145
+ => 1
146
+ ruby-1.8.7-p302 >
147
+ ruby-1.8.7-p302 > p lb_request_time / requests_to_make
148
+ 0.00179680000000001
149
+ => nil
150
+
151
+ == Future Ideas
152
+
153
+ * Bulk insert
154
+ * Atomicity for various operations?
155
+ * Is nil? OK to return if Redis returns no data or should it be []?
156
+
157
+ == Contributing to leaderboard
158
+
159
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
160
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
161
+ * Fork the project
162
+ * Start a feature/bugfix branch
163
+ * Commit and push until you are happy with your contribution
164
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
165
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
166
+
167
+ == Copyright
168
+
169
+ Copyright (c) 2011 David Czarnecki. See LICENSE.txt for further details.
170
+
data/Rakefile ADDED
@@ -0,0 +1,89 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "multi-leaderboard"
16
+ gem.homepage = "http://github.com/willcosgrove/leaderboard"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{Leaderboards backed by Redis in Ruby}
19
+ gem.description = %Q{Leaderboards backed by Redis in Ruby}
20
+ gem.email = "will@willcosgrove.com"
21
+ gem.authors = ["David Czarnecki", "Will Cosgrove"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
25
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rake/testtask'
30
+ Rake::TestTask.new(:test) do |test|
31
+ test.libs << 'lib' << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+
36
+ require 'rcov/rcovtask'
37
+ Rcov::RcovTask.new do |test|
38
+ test.libs << 'test'
39
+ test.pattern = 'test/**/test_*.rb'
40
+ test.verbose = true
41
+ end
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "multi-leaderboard #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
52
+
53
+ REDIS_DIR = File.expand_path(File.join("..", "test"), __FILE__)
54
+ REDIS_CNF = File.join(REDIS_DIR, "test.conf")
55
+ REDIS_PID = File.join(REDIS_DIR, "db", "redis.pid")
56
+ REDIS_LOCATION = ENV['REDIS_LOCATION']
57
+
58
+ task :default => :run
59
+
60
+ desc "Run tests and manage server start/stop"
61
+ task :run => [:start, :test, :stop]
62
+
63
+ desc "Run rcov and manage server start/stop"
64
+ task :rcoverage => [:start, :rcov, :stop]
65
+
66
+ desc "Start the Redis server"
67
+ task :start do
68
+ redis_running = \
69
+ begin
70
+ File.exists?(REDIS_PID) && Process.kill(0, File.read(REDIS_PID).to_i)
71
+ rescue Errno::ESRCH
72
+ FileUtils.rm REDIS_PID
73
+ false
74
+ end
75
+
76
+ if REDIS_LOCATION
77
+ system "#{REDIS_LOCATION}/redis-server #{REDIS_CNF}" unless redis_running
78
+ else
79
+ system "redis-server #{REDIS_CNF}" unless redis_running
80
+ end
81
+ end
82
+
83
+ desc "Stop the Redis server"
84
+ task :stop do
85
+ if File.exists?(REDIS_PID)
86
+ Process.kill "INT", File.read(REDIS_PID).to_i
87
+ FileUtils.rm REDIS_PID
88
+ end
89
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.4
@@ -0,0 +1,67 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{multi-leaderboard}
8
+ s.version = "1.0.4"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["David Czarnecki", "Will Cosgrove"]
12
+ s.date = %q{2011-05-02}
13
+ s.description = %q{Leaderboards backed by Redis in Ruby}
14
+ s.email = %q{will@willcosgrove.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rvmrc",
22
+ "CHANGELOG.markdown",
23
+ "Gemfile",
24
+ "LICENSE.txt",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "leaderboard.gemspec",
29
+ "lib/leaderboard.rb",
30
+ "test/db/.gitkeep",
31
+ "test/helper.rb",
32
+ "test/test.conf",
33
+ "test/test_leaderboard.rb"
34
+ ]
35
+ s.homepage = %q{http://github.com/willcosgrove/leaderboard}
36
+ s.licenses = ["MIT"]
37
+ s.require_paths = ["lib"]
38
+ s.rubygems_version = %q{1.3.7}
39
+ s.summary = %q{Leaderboards backed by Redis in Ruby}
40
+ s.test_files = [
41
+ "test/helper.rb",
42
+ "test/test_leaderboard.rb"
43
+ ]
44
+
45
+ if s.respond_to? :specification_version then
46
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
47
+ s.specification_version = 3
48
+
49
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
50
+ s.add_runtime_dependency(%q<redis>, ["~> 2.1.1"])
51
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
52
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
53
+ s.add_development_dependency(%q<rcov>, [">= 0"])
54
+ else
55
+ s.add_dependency(%q<redis>, ["~> 2.1.1"])
56
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
57
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
58
+ s.add_dependency(%q<rcov>, [">= 0"])
59
+ end
60
+ else
61
+ s.add_dependency(%q<redis>, ["~> 2.1.1"])
62
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
63
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
64
+ s.add_dependency(%q<rcov>, [">= 0"])
65
+ end
66
+ end
67
+
@@ -0,0 +1,235 @@
1
+ require 'redis'
2
+
3
+ class Leaderboard
4
+ VERSION = '1.0.4'.freeze
5
+
6
+ DEFAULT_PAGE_SIZE = 25
7
+ DEFAULT_REDIS_HOST = 'localhost'
8
+ DEFAULT_REDIS_PORT = 6379
9
+
10
+ attr_reader :host
11
+ attr_reader :port
12
+ attr_reader :leaderboard_name
13
+ attr_accessor :page_size
14
+
15
+ def initialize(leaderboard_name, host = DEFAULT_REDIS_HOST, port = DEFAULT_REDIS_PORT, page_size = DEFAULT_PAGE_SIZE, redis_options = {})
16
+ @leaderboard_name = leaderboard_name
17
+ @host = host
18
+ @port = port
19
+
20
+ if page_size < 1
21
+ page_size = DEFAULT_PAGE_SIZE
22
+ end
23
+
24
+ @page_size = page_size
25
+
26
+ redis_options = redis_options.dup
27
+ redis_options[:host] ||= @host
28
+ redis_options[:port] ||= @port
29
+
30
+ @redis_options = redis_options
31
+
32
+ @@redis_connection ||= Redis.new(@redis_options)
33
+ end
34
+
35
+ def add_member(member, score)
36
+ add_member_to(@leaderboard_name, member, score)
37
+ end
38
+
39
+ def add_member_to(leaderboard_name, member, score)
40
+ @@redis_connection.zadd(leaderboard_name, score, member)
41
+ end
42
+
43
+ def remove_member(member)
44
+ remove_member_from(@leaderboard_name, member)
45
+ end
46
+
47
+ def remove_member_from(leaderboard_name, member)
48
+ @@redis_connection.zrem(leaderboard_name, member)
49
+ end
50
+
51
+ def total_members
52
+ total_members_in(@leaderboard_name)
53
+ end
54
+
55
+ def total_members_in(leaderboard_name)
56
+ @@redis_connection.zcard(leaderboard_name)
57
+ end
58
+
59
+ def total_pages
60
+ total_pages_in(@leaderboard_name)
61
+ end
62
+
63
+ def total_pages_in(leaderboard_name)
64
+ (total_members_in(leaderboard_name) / @page_size.to_f).ceil
65
+ end
66
+
67
+ def total_members_in_score_range(min_score, max_score)
68
+ total_members_in_score_range_in(@leaderboard_name, min_score, max_score)
69
+ end
70
+
71
+ def total_members_in_score_range_in(leaderboard_name, min_score, max_score)
72
+ @@redis_connection.zcount(leaderboard_name, min_score, max_score)
73
+ end
74
+
75
+ def change_score_for(member, delta)
76
+ change_score_for_member_in(@leaderboard_name, member, delta)
77
+ end
78
+
79
+ def change_score_for_member_in(leaderboard_name, member, delta)
80
+ @@redis_connection.zincrby(leaderboard_name, delta, member)
81
+ end
82
+
83
+ def rank_for(member, use_zero_index_for_rank = false)
84
+ rank_for_in(@leaderboard_name, member, use_zero_index_for_rank)
85
+ end
86
+
87
+ def rank_for_in(leaderboard_name, member, use_zero_index_for_rank = false)
88
+ if use_zero_index_for_rank
89
+ return @@redis_connection.zrevrank(leaderboard_name, member)
90
+ else
91
+ return @@redis_connection.zrevrank(leaderboard_name, member) + 1 rescue nil
92
+ end
93
+ end
94
+
95
+ def score_for(member)
96
+ score_for_in(@leaderboard_name, member)
97
+ end
98
+
99
+ def score_for_in(leaderboard_name, member)
100
+ @@redis_connection.zscore(leaderboard_name, member).to_f
101
+ end
102
+
103
+ def check_member?(member)
104
+ check_member_in?(@leaderboard_name, member)
105
+ end
106
+
107
+ def check_member_in?(leaderboard_name, member)
108
+ @@redis_connection.zscore(leaderboard_name, member).nil?
109
+ end
110
+
111
+ def score_and_rank_for(member, use_zero_index_for_rank = false)
112
+ score_and_rank_for_in(@leaderboard_name, member, use_zero_index_for_rank)
113
+ end
114
+
115
+ def score_and_rank_for_in(leaderboard_name, member, use_zero_index_for_rank = false)
116
+ {:member => member, :score => score_for_in(leaderboard_name, member), :rank => rank_for_in(leaderboard_name, member, use_zero_index_for_rank)}
117
+ end
118
+
119
+ def remove_members_in_score_range(min_score, max_score)
120
+ remove_members_in_score_range_in(@leaderboard_name, min_score, max_score)
121
+ end
122
+
123
+ def remove_members_in_score_range_in(leaderboard_name, min_score, max_score)
124
+ @@redis_connection.zremrangebyscore(leaderboard_name, min_score, max_score)
125
+ end
126
+
127
+ def leaders(current_page, with_scores = true, with_rank = true, use_zero_index_for_rank = false)
128
+ leaders_in(@leaderboard_name, current_page, with_scores, with_rank, use_zero_index_for_rank)
129
+ end
130
+
131
+ def leaders_in(leaderboard_name, current_page, with_scores = true, with_rank = true, use_zero_index_for_rank = false)
132
+ if current_page < 1
133
+ current_page = 1
134
+ end
135
+
136
+ if current_page > total_pages
137
+ current_page = total_pages
138
+ end
139
+
140
+ index_for_redis = current_page - 1
141
+
142
+ starting_offset = (index_for_redis * @page_size)
143
+ if starting_offset < 0
144
+ starting_offset = 0
145
+ end
146
+
147
+ ending_offset = (starting_offset + @page_size) - 1
148
+
149
+ raw_leader_data = @@redis_connection.zrevrange(leaderboard_name, starting_offset, ending_offset, :with_scores => with_scores)
150
+ if raw_leader_data
151
+ massage_leader_data(leaderboard_name, raw_leader_data, with_rank, use_zero_index_for_rank)
152
+ else
153
+ return nil
154
+ end
155
+ end
156
+
157
+ def around_me(member, with_scores = true, with_rank = true, use_zero_index_for_rank = false)
158
+ around_me_in(@leaderboard_name, member, with_scores, with_rank, use_zero_index_for_rank)
159
+ end
160
+
161
+ def around_me_in(leaderboard_name, member, with_scores = true, with_rank = true, use_zero_index_for_rank = false)
162
+ reverse_rank_for_member = @@redis_connection.zrevrank(leaderboard_name, member)
163
+
164
+ starting_offset = reverse_rank_for_member - (@page_size / 2)
165
+ if starting_offset < 0
166
+ starting_offset = 0
167
+ end
168
+
169
+ ending_offset = (starting_offset + @page_size) - 1
170
+
171
+ raw_leader_data = @@redis_connection.zrevrange(leaderboard_name, starting_offset, ending_offset, :with_scores => with_scores)
172
+ if raw_leader_data
173
+ massage_leader_data(leaderboard_name, raw_leader_data, with_rank, use_zero_index_for_rank)
174
+ else
175
+ return nil
176
+ end
177
+ end
178
+
179
+ def ranked_in_list(members, with_scores = true, use_zero_index_for_rank = false)
180
+ ranked_in_list_in(@leaderboard_name, members, with_scores, use_zero_index_for_rank)
181
+ end
182
+
183
+ def ranked_in_list_in(leaderboard_name, members, with_scores = true, use_zero_index_for_rank = false)
184
+ ranks_for_members = []
185
+
186
+ members.each do |member|
187
+ data = {}
188
+ data[:member] = member
189
+ data[:rank] = rank_for_in(leaderboard_name, member, use_zero_index_for_rank)
190
+ data[:score] = score_for_in(leaderboard_name, member) if with_scores
191
+
192
+ ranks_for_members << data
193
+ end
194
+
195
+ ranks_for_members
196
+ end
197
+
198
+ # Merge leaderboards given by keys with this leaderboard into destination
199
+ def merge_leaderboards(destination, keys, options = {:aggregate => :sum})
200
+ @@redis_connection.zunionstore(destination, keys.insert(0, @leaderboard_name), options)
201
+ end
202
+
203
+ # Intersect leaderboards given by keys with this leaderboard into destination
204
+ def intersect_leaderboards(destination, keys, options = {:aggregate => :sum})
205
+ @@redis_connection.zinterstore(destination, keys.insert(0, @leaderboard_name), options)
206
+ end
207
+
208
+ # Disconnect from the redis server
209
+ def disconnect
210
+ @@redis_connection.client.disconnect
211
+ end
212
+
213
+ private
214
+
215
+ def massage_leader_data(leaderboard_name, leaders, with_rank, use_zero_index_for_rank)
216
+ member_attribute = true
217
+ leader_data = []
218
+
219
+ data = {}
220
+ leaders.each do |leader_data_item|
221
+ if member_attribute
222
+ data[:member] = leader_data_item
223
+ else
224
+ data[:score] = leader_data_item.to_f
225
+ data[:rank] = rank_for_in(leaderboard_name, data[:member], use_zero_index_for_rank) if with_rank
226
+ leader_data << data
227
+ data = {}
228
+ end
229
+
230
+ member_attribute = !member_attribute
231
+ end
232
+
233
+ leader_data
234
+ end
235
+ end
data/test/db/.gitkeep ADDED
File without changes
data/test/helper.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+
12
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ require 'leaderboard'
15
+
16
+ class Test::Unit::TestCase
17
+ end
data/test/test.conf ADDED
@@ -0,0 +1,8 @@
1
+ dir ./test/db
2
+ pidfile ./redis.pid
3
+ port 6379
4
+ timeout 300
5
+ loglevel debug
6
+ logfile stdout
7
+ databases 16
8
+ daemonize yes
@@ -0,0 +1,259 @@
1
+ require 'helper'
2
+
3
+ class TestLeaderboard < Test::Unit::TestCase
4
+ def setup
5
+ @leaderboard = Leaderboard.new('name')
6
+ @redis_connection = Redis.new
7
+ end
8
+
9
+ def teardown
10
+ @redis_connection.flushdb
11
+ end
12
+
13
+ def test_version
14
+ assert_equal '1.0.2', Leaderboard::VERSION
15
+ end
16
+
17
+ def test_initialize_with_defaults
18
+ assert_equal 'name', @leaderboard.leaderboard_name
19
+ assert_equal 'localhost', @leaderboard.host
20
+ assert_equal 6379, @leaderboard.port
21
+ assert_equal Leaderboard::DEFAULT_PAGE_SIZE, @leaderboard.page_size
22
+ end
23
+
24
+ def test_page_size_is_default_page_size_if_set_to_invalid_value
25
+ @leaderboard = Leaderboard.new('name', 'localhost', 6379, 0)
26
+
27
+ assert_equal Leaderboard::DEFAULT_PAGE_SIZE, @leaderboard.page_size
28
+ end
29
+
30
+ def test_add_member_and_total_members
31
+ @leaderboard.add_member('member', 1)
32
+
33
+ assert_equal 1, @leaderboard.total_members
34
+ end
35
+
36
+ def test_total_members_in_score_range
37
+ add_members_to_leaderboard(5)
38
+
39
+ assert_equal 3, @leaderboard.total_members_in_score_range(2, 4)
40
+ end
41
+
42
+ def test_rank_for
43
+ add_members_to_leaderboard(5)
44
+
45
+ assert_equal 2, @leaderboard.rank_for('member_4')
46
+ assert_equal 1, @leaderboard.rank_for('member_4', true)
47
+ end
48
+
49
+ def test_score_for
50
+ add_members_to_leaderboard(5)
51
+
52
+ assert_equal 4, @leaderboard.score_for('member_4')
53
+ end
54
+
55
+ def test_total_pages
56
+ add_members_to_leaderboard(10)
57
+
58
+ assert_equal 1, @leaderboard.total_pages
59
+
60
+ @redis_connection.flushdb
61
+
62
+ add_members_to_leaderboard(Leaderboard::DEFAULT_PAGE_SIZE + 1)
63
+
64
+ assert_equal 2, @leaderboard.total_pages
65
+ end
66
+
67
+ def test_leaders
68
+ add_members_to_leaderboard(25)
69
+
70
+ assert_equal 25, @leaderboard.total_members
71
+
72
+ leaders = @leaderboard.leaders(1)
73
+
74
+ assert_equal 25, leaders.size
75
+ assert_equal 'member_25', leaders[0][:member]
76
+ assert_equal 'member_2', leaders[-2][:member]
77
+ assert_equal 'member_1', leaders[-1][:member]
78
+ assert_equal 1, leaders[-1][:score].to_i
79
+ end
80
+
81
+ def test_leaders_with_multiple_pages
82
+ add_members_to_leaderboard(Leaderboard::DEFAULT_PAGE_SIZE * 3 + 1)
83
+
84
+ assert_equal Leaderboard::DEFAULT_PAGE_SIZE * 3 + 1, @leaderboard.total_members
85
+
86
+ leaders = @leaderboard.leaders(1)
87
+ assert_equal @leaderboard.page_size, leaders.size
88
+
89
+ leaders = @leaderboard.leaders(2)
90
+ assert_equal @leaderboard.page_size, leaders.size
91
+
92
+ leaders = @leaderboard.leaders(3)
93
+ assert_equal @leaderboard.page_size, leaders.size
94
+
95
+ leaders = @leaderboard.leaders(4)
96
+ assert_equal 1, leaders.size
97
+
98
+ leaders = @leaderboard.leaders(-5)
99
+ assert_equal @leaderboard.page_size, leaders.size
100
+
101
+ leaders = @leaderboard.leaders(10)
102
+ assert_equal 1, leaders.size
103
+ end
104
+
105
+ def test_around_me
106
+ add_members_to_leaderboard(Leaderboard::DEFAULT_PAGE_SIZE * 3 + 1)
107
+
108
+ assert_equal Leaderboard::DEFAULT_PAGE_SIZE * 3 + 1, @leaderboard.total_members
109
+
110
+ leaders_around_me = @leaderboard.around_me('member_30')
111
+ assert_equal @leaderboard.page_size / 2, leaders_around_me.size / 2
112
+
113
+ leaders_around_me = @leaderboard.around_me('member_1')
114
+ assert_equal @leaderboard.page_size / 2 + 1, leaders_around_me.size
115
+
116
+ leaders_around_me = @leaderboard.around_me('member_76')
117
+ assert_equal @leaderboard.page_size / 2, leaders_around_me.size / 2
118
+ end
119
+
120
+ def test_ranked_in_list
121
+ add_members_to_leaderboard(Leaderboard::DEFAULT_PAGE_SIZE)
122
+
123
+ assert_equal Leaderboard::DEFAULT_PAGE_SIZE, @leaderboard.total_members
124
+
125
+ members = ['member_1', 'member_5', 'member_10']
126
+ ranked_members = @leaderboard.ranked_in_list(members, true)
127
+
128
+ assert_equal 3, ranked_members.size
129
+
130
+ assert_equal 25, ranked_members[0][:rank]
131
+ assert_equal 1, ranked_members[0][:score]
132
+
133
+ assert_equal 21, ranked_members[1][:rank]
134
+ assert_equal 5, ranked_members[1][:score]
135
+
136
+ assert_equal 16, ranked_members[2][:rank]
137
+ assert_equal 10, ranked_members[2][:score]
138
+ end
139
+
140
+ def test_remove_member
141
+ add_members_to_leaderboard(Leaderboard::DEFAULT_PAGE_SIZE)
142
+
143
+ assert_equal Leaderboard::DEFAULT_PAGE_SIZE, @leaderboard.total_members
144
+
145
+ @leaderboard.remove_member('member_1')
146
+
147
+ assert_equal Leaderboard::DEFAULT_PAGE_SIZE - 1, @leaderboard.total_members
148
+ assert_nil @leaderboard.rank_for('member_1')
149
+ end
150
+
151
+ def test_change_score_for
152
+ @leaderboard.add_member('member_1', 5)
153
+ assert_equal 5, @leaderboard.score_for('member_1')
154
+
155
+ @leaderboard.change_score_for('member_1', 5)
156
+ assert_equal 10, @leaderboard.score_for('member_1')
157
+
158
+ @leaderboard.change_score_for('member_1', -5)
159
+ assert_equal 5, @leaderboard.score_for('member_1')
160
+ end
161
+
162
+ def test_check_member
163
+ @leaderboard.add_member('member_1', 10)
164
+
165
+ assert_equal true, @leaderboard.check_member?('member_1')
166
+ assert_equal false, @leaderboard.check_member?('member_2')
167
+ end
168
+
169
+ def test_can_change_page_size_and_have_it_reflected_in_size_of_result_set
170
+ add_members_to_leaderboard(Leaderboard::DEFAULT_PAGE_SIZE)
171
+
172
+ @leaderboard.page_size = 5
173
+ assert_equal 5, @leaderboard.total_pages
174
+ assert_equal 5, @leaderboard.leaders(1).size
175
+ end
176
+
177
+ def test_score_and_rank_for
178
+ add_members_to_leaderboard
179
+
180
+ data = @leaderboard.score_and_rank_for('member_1')
181
+ assert_equal 'member_1', data[:member]
182
+ assert_equal 1, data[:score]
183
+ assert_equal 5, data[:rank]
184
+ end
185
+
186
+ def test_remove_members_in_score_range
187
+ add_members_to_leaderboard
188
+
189
+ assert_equal 5, @leaderboard.total_members
190
+
191
+ @leaderboard.add_member('cheater_1', 100)
192
+ @leaderboard.add_member('cheater_2', 101)
193
+ @leaderboard.add_member('cheater_3', 102)
194
+
195
+ assert_equal 8, @leaderboard.total_members
196
+
197
+ @leaderboard.remove_members_in_score_range(100, 102)
198
+
199
+ assert_equal 5, @leaderboard.total_members
200
+
201
+ leaders = @leaderboard.leaders(1)
202
+ leaders.each do |leader|
203
+ assert leader[:score] < 100
204
+ end
205
+ end
206
+
207
+ def test_merge_leaderboards
208
+ foo = Leaderboard.new('foo')
209
+ bar = Leaderboard.new('bar')
210
+
211
+ foo.add_member('foo_1', 1)
212
+ foo.add_member('foo_2', 2)
213
+ bar.add_member('bar_1', 3)
214
+ bar.add_member('bar_2', 4)
215
+ bar.add_member('bar_3', 5)
216
+
217
+ foobar_keys = foo.merge_leaderboards('foobar', ['bar'])
218
+ assert_equal 5, foobar_keys
219
+
220
+ foobar = Leaderboard.new('foobar')
221
+ assert_equal 5, foobar.total_members
222
+
223
+ first_leader_in_foobar = foobar.leaders(1).first
224
+ assert_equal 1, first_leader_in_foobar[:rank]
225
+ assert_equal 'bar_3', first_leader_in_foobar[:member]
226
+ assert_equal 5, first_leader_in_foobar[:score]
227
+ end
228
+
229
+ def test_intersect_leaderboards
230
+ foo = Leaderboard.new('foo')
231
+ bar = Leaderboard.new('bar')
232
+
233
+ foo.add_member('foo_1', 1)
234
+ foo.add_member('foo_2', 2)
235
+ foo.add_member('bar_3', 6)
236
+ bar.add_member('bar_1', 3)
237
+ bar.add_member('foo_1', 4)
238
+ bar.add_member('bar_3', 5)
239
+
240
+ foobar_keys = foo.intersect_leaderboards('foobar', ['bar'], {:aggregate => :max})
241
+ assert_equal 2, foobar_keys
242
+
243
+ foobar = Leaderboard.new('foobar')
244
+ assert_equal 2, foobar.total_members
245
+
246
+ first_leader_in_foobar = foobar.leaders(1).first
247
+ assert_equal 1, first_leader_in_foobar[:rank]
248
+ assert_equal 'bar_3', first_leader_in_foobar[:member]
249
+ assert_equal 6, first_leader_in_foobar[:score]
250
+ end
251
+
252
+ private
253
+
254
+ def add_members_to_leaderboard(members_to_add = 5)
255
+ 1.upto(members_to_add) do |index|
256
+ @leaderboard.add_member("member_#{index}", index)
257
+ end
258
+ end
259
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: multi-leaderboard
3
+ version: !ruby/object:Gem::Version
4
+ hash: 31
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 4
10
+ version: 1.0.4
11
+ platform: ruby
12
+ authors:
13
+ - David Czarnecki
14
+ - Will Cosgrove
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2011-05-02 00:00:00 -05:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: redis
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ hash: 9
31
+ segments:
32
+ - 2
33
+ - 1
34
+ - 1
35
+ version: 2.1.1
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ - !ruby/object:Gem::Dependency
39
+ name: bundler
40
+ prerelease: false
41
+ requirement: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ hash: 23
47
+ segments:
48
+ - 1
49
+ - 0
50
+ - 0
51
+ version: 1.0.0
52
+ type: :development
53
+ version_requirements: *id002
54
+ - !ruby/object:Gem::Dependency
55
+ name: jeweler
56
+ prerelease: false
57
+ requirement: &id003 !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ hash: 7
63
+ segments:
64
+ - 1
65
+ - 5
66
+ - 2
67
+ version: 1.5.2
68
+ type: :development
69
+ version_requirements: *id003
70
+ - !ruby/object:Gem::Dependency
71
+ name: rcov
72
+ prerelease: false
73
+ requirement: &id004 !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ hash: 3
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ type: :development
83
+ version_requirements: *id004
84
+ description: Leaderboards backed by Redis in Ruby
85
+ email: will@willcosgrove.com
86
+ executables: []
87
+
88
+ extensions: []
89
+
90
+ extra_rdoc_files:
91
+ - LICENSE.txt
92
+ - README.rdoc
93
+ files:
94
+ - .document
95
+ - .rvmrc
96
+ - CHANGELOG.markdown
97
+ - Gemfile
98
+ - LICENSE.txt
99
+ - README.rdoc
100
+ - Rakefile
101
+ - VERSION
102
+ - leaderboard.gemspec
103
+ - lib/leaderboard.rb
104
+ - test/db/.gitkeep
105
+ - test/helper.rb
106
+ - test/test.conf
107
+ - test/test_leaderboard.rb
108
+ has_rdoc: true
109
+ homepage: http://github.com/willcosgrove/leaderboard
110
+ licenses:
111
+ - MIT
112
+ post_install_message:
113
+ rdoc_options: []
114
+
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ hash: 3
123
+ segments:
124
+ - 0
125
+ version: "0"
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ hash: 3
132
+ segments:
133
+ - 0
134
+ version: "0"
135
+ requirements: []
136
+
137
+ rubyforge_project:
138
+ rubygems_version: 1.5.2
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: Leaderboards backed by Redis in Ruby
142
+ test_files:
143
+ - test/helper.rb
144
+ - test/test_leaderboard.rb