lib/leaderboard.rb in leaderboard-2.0.1 vs lib/leaderboard.rb in leaderboard-2.0.2

- old
+ new

@@ -1,17 +1,19 @@ require 'redis' +require 'leaderboard/version' class Leaderboard - VERSION = '2.0.1'.freeze - DEFAULT_PAGE_SIZE = 25 + DEFAULT_OPTIONS = { :page_size => DEFAULT_PAGE_SIZE } DEFAULT_REDIS_HOST = 'localhost' + DEFAULT_REDIS_PORT = 6379 + DEFAULT_REDIS_OPTIONS = { :host => DEFAULT_REDIS_HOST, :port => DEFAULT_REDIS_PORT } @@ -20,13 +22,26 @@ :with_rank => true, :use_zero_index_for_rank => false, :page_size => nil } + # Name of the leaderboard. attr_reader :leaderboard_name + + # Page size to be used when paging through the leaderboard. attr_reader :page_size + # Create a new instance of a leaderboard. + # + # @param leaderboard [String] Name of the leaderboard. + # @param options [Hash] Options for the leaderboard such as +:page_size+. + # @param redis_options [Hash] Options for configuring Redis. + # + # Examples + # + # leaderboard = Leaderboard.new('highscores') + # leaderboard = Leaderboard.new('highscores', {:page_size => 10}) def initialize(leaderboard_name, options = DEFAULT_OPTIONS, redis_options = DEFAULT_REDIS_OPTIONS) @leaderboard_name = leaderboard_name @page_size = options[:page_size] if @page_size < 1 @@ -38,110 +53,223 @@ redis_options.delete(:redis_connection) end @redis_connection = Redis.new(redis_options) if @redis_connection.nil? end - + + # Set the page size to be used when paging through the leaderboard. This method + # also has the side effect of setting the page size to the +DEFAULT_PAGE_SIZE+ + # if the page size is less than 1. + # + # @param page_size [int] Page size. def page_size=(page_size) page_size = DEFAULT_PAGE_SIZE if page_size < 1 @page_size = page_size end + # Disconnect the Redis connection. def disconnect @redis_connection.client.disconnect end + # Delete the current leaderboard. def delete_leaderboard delete_leaderboard_named(@leaderboard_name) end + # Delete the named leaderboard. + # + # @param leaderboard_name [String] Name of the leaderboard. def delete_leaderboard_named(leaderboard_name) @redis_connection.del(leaderboard_name) end + # Rank a member in the leaderboard. + # + # @param member [String] Member name. + # @param score [float] Member score. def rank_member(member, score) rank_member_in(@leaderboard_name, member, score) end + # Rank a member in the named leaderboard. + # + # @param leaderboard_name [String] Name of the leaderboard. + # @param member [String] Member name. + # @param score [float] Member score. def rank_member_in(leaderboard_name, member, score) @redis_connection.zadd(leaderboard_name, score, member) end + # Remove a member from the leaderboard. + # + # @param member [String] Member name. def remove_member(member) remove_member_from(@leaderboard_name, member) end + # Remove a member from the named leaderboard. + # + # @param leaderboard_name [String] Name of the leaderboard. + # @param member [String] Member name. def remove_member_from(leaderboard_name, member) @redis_connection.zrem(leaderboard_name, member) end + # Retrieve the total number of members in the leaderboard. + # + # @return total number of members in the leaderboard. def total_members total_members_in(@leaderboard_name) end + # Retrieve the total number of members in the named leaderboard. + # + # @param leaderboard_name [String] Name of the leaderboard. + # + # @return the total number of members in the named leaderboard. def total_members_in(leaderboard_name) @redis_connection.zcard(leaderboard_name) end + # Retrieve the total number of pages in the leaderboard. + # + # @return the total number of pages in the leaderboard. def total_pages total_pages_in(@leaderboard_name) end + # Retrieve the total number of pages in the named leaderboard. + # + # @param leaderboard_name [String] Name of the leaderboard. + # @param page_size [int] Page size to be used when paging through the leaderboard. + # + # @return the total number of pages in the named leaderboard. def total_pages_in(leaderboard_name, page_size = nil) page_size ||= @page_size.to_f (total_members_in(leaderboard_name) / page_size.to_f).ceil end + # Retrieve the total members in a given score range from the leaderboard. + # + # @param min_score [float] Minimum score. + # @param max_score [float] Maximum score. + # + # @return the total members in a given score range from the leaderboard. def total_members_in_score_range(min_score, max_score) total_members_in_score_range_in(@leaderboard_name, min_score, max_score) end + # Retrieve the total members in a given score range from the named leaderboard. + # + # @param leaderboard_name Name of the leaderboard. + # @param min_score [float] Minimum score. + # @param max_score [float] Maximum score. + # + # @return the total members in a given score range from the named leaderboard. def total_members_in_score_range_in(leaderboard_name, min_score, max_score) @redis_connection.zcount(leaderboard_name, min_score, max_score) end + # Change the score for a member in the leaderboard by a score delta which can be positive or negative. + # + # @param member [String] Member name. + # @param delta [float] Score change. def change_score_for(member, delta) change_score_for_member_in(@leaderboard_name, member, delta) end + # Change the score for a member in the named leaderboard by a delta which can be positive or negative. + # + # @param leaderboard_name [String] Name of the leaderboard. + # @param member [String] Member name. + # @param delta [float] Score change. def change_score_for_member_in(leaderboard_name, member, delta) @redis_connection.zincrby(leaderboard_name, delta, member) end + # Retrieve the rank for a member in the leaderboard. + # + # @param member [String] Member name. + # @param use_zero_index_for_rank [boolean, false] If the returned rank should be 0-indexed. + # + # @return the rank for a member in the leaderboard. def rank_for(member, use_zero_index_for_rank = false) rank_for_in(@leaderboard_name, member, use_zero_index_for_rank) end + # Retrieve the rank for a member in the named leaderboard. + # + # @param leaderboard_name [String] Name of the leaderboard. + # @param member [String] Member name. + # @param use_zero_index_for_rank [boolean, false] If the returned rank should be 0-indexed. + # + # @return the rank for a member in the leaderboard. def rank_for_in(leaderboard_name, member, use_zero_index_for_rank = false) if use_zero_index_for_rank return @redis_connection.zrevrank(leaderboard_name, member) else return @redis_connection.zrevrank(leaderboard_name, member) + 1 rescue nil end end + # Retrieve the score for a member in the leaderboard. + # + # @param member Member name. + # + # @return the score for a member in the leaderboard. def score_for(member) score_for_in(@leaderboard_name, member) end + # Retrieve the score for a member in the named leaderboard. + # + # @param leaderboard_name Name of the leaderboard. + # @param member [String] Member name. + # + # @return the score for a member in the leaderboard. def score_for_in(leaderboard_name, member) @redis_connection.zscore(leaderboard_name, member).to_f end + # Check to see if a member exists in the leaderboard. + # + # @param member [String] Member name. + # + # @return +true+ if the member exists in the leaderboard, +false+ otherwise. def check_member?(member) check_member_in?(@leaderboard_name, member) end + # Check to see if a member exists in the named leaderboard. + # + # @param leaderboard_name [String] Name of the leaderboard. + # @param member [String] Member name. + # + # @return +true+ if the member exists in the named leaderboard, +false+ otherwise. def check_member_in?(leaderboard_name, member) !@redis_connection.zscore(leaderboard_name, member).nil? end + # Retrieve the score and rank for a member in the leaderboard. + # + # @param member [String] Member name. + # @param use_zero_index_for_rank [boolean, false] If the returned rank should be 0-indexed. + # + # @return the score and rank for a member in the leaderboard as a Hash. def score_and_rank_for(member, use_zero_index_for_rank = false) score_and_rank_for_in(@leaderboard_name, member, use_zero_index_for_rank) end + # Retrieve the score and rank for a member in the named leaderboard. + # + # @param leaderboard_name [String]Name of the leaderboard. + # @param member [String] Member name. + # @param use_zero_index_for_rank [boolean, false] If the returned rank should be 0-indexed. + # + # @return the score and rank for a member in the named leaderboard as a Hash. def score_and_rank_for_in(leaderboard_name, member, use_zero_index_for_rank = false) responses = @redis_connection.multi do |transaction| transaction.zscore(leaderboard_name, member) transaction.zrevrank(leaderboard_name, member) end @@ -152,35 +280,70 @@ end {:member => member, :score => responses[0], :rank => responses[1]} end + # Remove members from the leaderboard in a given score range. + # + # @param min_score [float] Minimum score. + # @param max_score [float] Maximum score. def remove_members_in_score_range(min_score, max_score) remove_members_in_score_range_in(@leaderboard_name, min_score, max_score) end + # Remove members from the named leaderboard in a given score range. + # + # @param leaderboard_name [String] Name of the leaderboard. + # @param min_score [float] Minimum score. + # @param max_score [float] Maximum score. def remove_members_in_score_range_in(leaderboard_name, min_score, max_score) @redis_connection.zremrangebyscore(leaderboard_name, min_score, max_score) end + # Retrieve the percentile for a member in the leaderboard. + # + # @param member [String] Member name. + # + # @return the percentile for a member in the leaderboard. Return +nil+ for a non-existent member. def percentile_for(member) percentile_for_in(@leaderboard_name, member) end + # Retrieve the percentile for a member in the named leaderboard. + # + # @param leaderboard_name [String] Name of the leaderboard. + # @param member [String] Member name. + # + # @return the percentile for a member in the named leaderboard. def percentile_for_in(leaderboard_name, member) + return nil unless check_member_in?(leaderboard_name, member) + responses = @redis_connection.multi do |transaction| transaction.zcard(leaderboard_name) transaction.zrevrank(leaderboard_name, member) end ((responses[0] - responses[1] - 1).to_f / responses[0].to_f * 100).ceil end + # Retrieve a page of leaders from the leaderboard. + # + # @param current_page [int] Page to retrieve from the leaderboard. + # @param options [Hash] Options to be used when retrieving the page from the leaderboard. + # + # @return a page of leaders from the leaderboard. def leaders(current_page, options = {}) leaders_in(@leaderboard_name, current_page, options) end + # Retrieve a page of leaders from the named leaderboard. + # + # @param leaderboard_name [String] Name of the leaderboard. + # @param current_page [int] Page to retrieve from the named leaderboard. + # @param options [Hash] Options to be used when retrieving the page from the named leaderboard. + # + # @return a page of leaders from the named leaderboard. def leaders_in(leaderboard_name, current_page, options = {}) leaderboard_options = DEFAULT_LEADERBOARD_REQUEST_OPTIONS.dup leaderboard_options.merge!(options) if current_page < 1 @@ -208,19 +371,34 @@ else return [] end end + # Retrieve a page of leaders from the leaderboard around a given member. + # + # @param member [String] Member name. + # @param options [Hash] Options to be used when retrieving the page from the leaderboard. + # + # @return a page of leaders from the leaderboard around a given member. def around_me(member, options = {}) around_me_in(@leaderboard_name, member, options) end + # Retrieve a page of leaders from the named leaderboard around a given member. + # + # @param leaderboard_name [String] Name of the leaderboard. + # @param member [String] Member name. + # @param options [Hash] Options to be used when retrieving the page from the named leaderboard. + # + # @return a page of leaders from the named leaderboard around a given member. Returns an empty array for a non-existent member. def around_me_in(leaderboard_name, member, options = {}) leaderboard_options = DEFAULT_LEADERBOARD_REQUEST_OPTIONS.dup leaderboard_options.merge!(options) reverse_rank_for_member = @redis_connection.zrevrank(leaderboard_name, member) + + return [] unless reverse_rank_for_member page_size = validate_page_size(leaderboard_options[:page_size]) || @page_size starting_offset = reverse_rank_for_member - (page_size / 2) if starting_offset < 0 @@ -235,14 +413,27 @@ else return [] end end + # Retrieve a page of leaders from the leaderboard for a given list of members. + # + # @param members [Array] Member names. + # @param options [Hash] Options to be used when retrieving the page from the leaderboard. + # + # @return a page of leaders from the leaderboard for a given list of members. def ranked_in_list(members, options = {}) ranked_in_list_in(@leaderboard_name, members, options) end + # Retrieve a page of leaders from the named leaderboard for a given list of members. + # + # @param leaderboard_name [String] Name of the leaderboard. + # @param members [Array] Member names. + # @param options [Hash] Options to be used when retrieving the page from the named leaderboard. + # + # @return a page of leaders from the named leaderboard for a given list of members. def ranked_in_list_in(leaderboard_name, members, options = {}) leaderboard_options = DEFAULT_LEADERBOARD_REQUEST_OPTIONS.dup leaderboard_options.merge!(options) ranks_for_members = [] @@ -260,11 +451,11 @@ if leaderboard_options[:with_scores] if leaderboard_options[:with_rank] if leaderboard_options[:use_zero_index_for_rank] data[:rank] = responses[index * 2] else - data[:rank] = responses[index * 2] + 1 + data[:rank] = responses[index * 2] + 1 rescue nil end data[:score] = responses[index * 2 + 1].to_f else data[:score] = responses[index].to_f @@ -272,32 +463,45 @@ else if leaderboard_options[:with_rank] if leaderboard_options[:use_zero_index_for_rank] data[:rank] = responses[index] else - data[:rank] = responses[index] + 1 + data[:rank] = responses[index] + 1 rescue nil end end end ranks_for_members << data end ranks_for_members end - # Merge leaderboards given by keys with this leaderboard into destination + # Merge leaderboards given by keys with this leaderboard into a named destination leaderboard. + # + # @param destination [String] Destination leaderboard name. + # @param keys [Array] Leaderboards to be merged with the current leaderboard. + # @param options [Hash] Options for merging the leaderboards. def merge_leaderboards(destination, keys, options = {:aggregate => :sum}) @redis_connection.zunionstore(destination, keys.insert(0, @leaderboard_name), options) end - # Intersect leaderboards given by keys with this leaderboard into destination + # Intersect leaderboards given by keys with this leaderboard into a named destination leaderboard. + # + # @param destination [String] Destination leaderboard name. + # @param keys [Array] Leaderboards to be merged with the current leaderboard. + # @param options [Hash] Options for intersecting the leaderboards. def intersect_leaderboards(destination, keys, options = {:aggregate => :sum}) @redis_connection.zinterstore(destination, keys.insert(0, @leaderboard_name), options) end private + # Validate and return the page size. Returns the +DEFAULT_PAGE_SIZE+ if the page size is less than 1. + # + # @param page_size [int] Page size. + # + # @return the page size. Returns the +DEFAULT_PAGE_SIZE+ if the page size is less than 1. def validate_page_size(page_size) if page_size && page_size < 1 page_size = DEFAULT_PAGE_SIZE end \ No newline at end of file