etehtsea/oxblood

View on GitHub
lib/oxblood/commands/sorted_sets.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'oxblood/commands/scan'

module Oxblood
  module Commands
    module SortedSets
      # Add one or more members to a sorted set, or update its score if it already
      # exists.
      # @see http://redis.io/commands/zadd
      #
      # @todo Add support for zadd options
      #   http://redis.io/commands/zadd#zadd-options-redis-302-or-greater
      #
      # @param [String] key under which store set
      # @param [[Float, String], Array<[Float, String]>] args scores and members
      # @param [Hash] opts
      #
      # @option opts [Boolean] :xx Only update elements that already exist.
      #   Never add elements.
      # @option opts [Boolean] :nx Don't update already existing elements.
      #   Always add new elements.
      # @option opts [Boolean] :ch Modify the return value from the number of
      #   new elements added, to the total number of elements changed.
      # @option opts [Boolean] :incr When this option is specified ZADD acts
      #   like ZINCRBY. Only one score-element pair can be specified in this mode.
      #
      # @return [Integer] The number of elements added to the sorted sets, not
      #   including elements already existing for which the score was updated
      def zadd(key, *args, **opts)
        args.unshift(:XX) if opts[:xx]
        args.unshift(:NX) if opts[:nx]
        args.unshift(:CH) if opts[:ch]
        args.unshift(:INCR) if opts[:incr]

        run(*args.unshift(:ZADD, key))
      end

      # Get the number of members in a sorted set
      # @see http://redis.io/commands/zcard
      #
      # @param [String] key
      #
      # @return [Integer] the cardinality (number of elements) of the sorted set,
      #   or 0 if key does not exists
      def zcard(key)
        run(:ZCARD, key)
      end

      # Count the members in a sorted set with scores within the given values
      # @see http://redis.io/commands/zcount
      #
      # @param [String] key
      # @param [String] min
      # @param [String] max
      #
      # @return [Integer] the number of elements in the specified score range
      def zcount(key, min, max)
        run(:ZCOUNT, key, min, max)
      end

      # Increment the score of a member in a sorted set
      # @see http://redis.io/commands/zincrby
      #
      # @param [String] key
      # @param [Float] increment
      # @param [String] member
      #
      # @return [String] the new score of member (a double precision floating
      #   point number), represented as string
      def zincrby(key, increment, member)
        run(:ZINCRBY, key, increment, member)
      end

      # Return a range of members in a sorted set, by lexicographical range.
      # @see https://redis.io/commands/zrangebylex
      #
      # @param [String] key
      # @param [String] min
      # @param [String] max
      # @param [Hash] opts
      #
      # @option opts [Array<Integer, Integer>] :limit Get a range of the matching
      #   elements (similar to SELECT LIMIT offset, count in SQL)
      #
      # @return [Array<String>] list of elements in the specified score range.
      def zrangebylex(key, min, max, opts = {})
        common_rangebylex(:ZRANGEBYLEX, key, min, max, opts)
      end

      # Remove all members in a sorted set between the given lexicographical range.
      # @see https://redis.io/commands/zremrangebylex
      #
      # @param [String] key
      # @param [String] min
      # @param [String] max
      #
      # @return [Integer] the number of elements removed.
      def zremrangebylex(key, min, max)
        run(:ZREMRANGEBYLEX, key, min, max)
      end

      # Return a range of members in a sorted set, by lexicographical range, ordered
      # from higher to lower strings.
      # @see https://redis.io/commands/zrevrangebylex
      #
      # @param [String] key
      # @param [String] min
      # @param [String] max
      # @param [Hash] opts
      #
      # @option opts [Array<Integer, Integer>] :limit Get a range of the matching
      #   elements (similar to SELECT LIMIT offset, count in SQL)
      #
      # @return [Array<String>] list of elements in the specified score range.
      def zrevrangebylex(key, min, max, opts = {})
        common_rangebylex(:ZREVRANGEBYLEX, key, min, max, opts)
      end

      # Count the number of members in a sorted set between a given
      # lexicographical range
      # @see http://redis.io/commands/zlexcount
      #
      # @param [String] key
      # @param [String] min
      # @param [String] max
      #
      # @return the number of elements in the specified score range
      def zlexcount(key, min, max)
        run(:ZLEXCOUNT, key, min, max)
      end

      # Return a range of members in a sorted set, by index
      # @see http://redis.io/commands/zrange
      #
      # @example
      #   session.zrange('myzset', 0, -1)
      #   # => ['one', 'two']
      #
      # @example
      #   session.zrange('myzset', 0, -1, withscores: true)
      #   # => [['one', '1'], ['two', '2']]
      #
      # @param [String] key
      # @param [Integer] start index
      # @param [Integer] stop index
      # @param [Hash] opts
      #
      # @option opts [Boolean] :withscores (false) Return the scores of
      #   the elements together with the elements
      #
      # @return [Array] list of elements in the specified range (optionally with
      #   their scores, in case the :withscores option is given)
      def zrange(key, start, stop, opts = {})
        common_range(:ZRANGE, key, start, stop, opts)
      end

      # Return a range of members in a sorted set, by score
      # @see http://redis.io/commands/zrangebyscore
      #
      # @param [String] key under which set is stored
      # @param [String] min score
      # @param [String] max score
      # @param [Hash] opts
      #
      # @option opts [Boolean] :withscores (false) Return the scores of
      #   the elements together with the elements
      # @option opts [Array<Integer, Integer>] :limit Get a range of the matching
      #   elements (similar to SELECT LIMIT offset, count in SQL)
      #
      # @example
      #   session.zrangebyscore('myzset', '-inf', '+inf')
      #   # => ['one', 'two', 'three']
      #
      # @example
      #   session.zrangebyscore('myzset', '(1', 2, withscores: true)
      #   # => [['two', '2']]
      #
      # @example
      #   opts = { withscores: true, limit: [1, 1] }
      #   session.zrangebyscore('myzset', '-inf', '+inf', opts)
      #   # => [['two', '2']]
      #
      # @return [Array] list of elements in the specified score range (optionally
      #   with their scores, in case the :withscores option is given)
      def zrangebyscore(key, min, max, opts = {})
        common_rangebyscore(:ZRANGEBYSCORE, key, min, max, opts)
      end

      # Determine the index of a member in a sorted set
      # @see http://redis.io/commands/zrank
      #
      # @param [String] key
      # @param [String] member
      #
      # @return [Integer, nil] the rank of member or nil if member does not exist
      # in the sorted set or key does not exist
      def zrank(key, member)
        run(:ZRANK, key, member)
      end

      # Remove one or more members from a sorted set
      # @see http://redis.io/commands/zrem
      #
      # @param [String] key
      # @param [Array<String>] members to delete
      #
      # @return [Integer] number of deleted members
      # @return [RError] when key exists and does not hold a sorted set.
      def zrem(key, *members)
        run(*members.unshift(:ZREM, key))
      end

      # Remove all members in a sorted set within the given indexes
      # @see http://redis.io/commands/zremrangebyrank
      #
      # @param [String] key
      # @param [String] start
      # @param [String] stop
      #
      # @return [Integer] the number of elements removed
      def zremrangebyrank(key, start, stop)
        run(:ZREMRANGEBYRANK, key, start, stop)
      end

      # Remove all members in a sorted set within the given scores
      # @see http://redis.io/commands/zremrangebyscore
      #
      # @param [String] key
      # @param [String] min score
      # @param [String] max score
      #
      # @return [Integer] the number of elements removed
      def zremrangebyscore(key, min, max)
        run(:ZREMRANGEBYSCORE, key, min, max)
      end

      # Return a range of members in a sorted set, by index, with scores ordered
      # from high to low
      # @see http://redis.io/commands/zrevrange
      #
      # @example
      #   session.zrevrange('myzset', 0, -1)
      #   # => ['two', 'one']
      #
      # @example
      #   session.zrevrange('myzset', 0, -1, withscores: true)
      #   # => [['two', '2'], ['one', '1']]
      #
      # @param [String] key
      # @param [Integer] start index
      # @param [Integer] stop index
      # @param [Hash] opts
      #
      # @option opts [Boolean] :withscores (false) Return the scores of
      #   the elements together with the elements
      #
      # @return [Array] list of elements in the specified range (optionally with
      #   their scores, in case the :withscores option is given)
      def zrevrange(key, start, stop, opts = {})
        common_range(:ZREVRANGE, key, start, stop, opts)
      end

      # Return a range of members in a sorted set, by score, with scores ordered
      # from high to low
      # @see http://redis.io/commands/zrevrangebyscore
      #
      # @param [String] key under which set is stored
      # @param [String] min score
      # @param [String] max score
      # @param [Hash] opts
      #
      # @option opts [Boolean] :withscores (false) Return the scores of
      #   the elements together with the elements
      # @option opts [Array<Integer, Integer>] :limit Get a range of the matching
      #   elements (similar to SELECT LIMIT offset, count in SQL)
      #
      # @example
      #   session.zrevrangebyscore('myzset', '+inf', '-inf')
      #   # => ['three', 'two', 'one']
      #
      # @example
      #   session.zrevrangebyscore('myzset', 2, '(1', withscores: true)
      #   # => [['two', '2']]
      #
      # @example
      #   opts = { withscores: true, limit: [1, 1] }
      #   session.zrevrangebyscore('myzset', '+inf', '-inf', opts)
      #   # => [['two', '2']]
      #
      # @return [Array] list of elements in the specified score range (optionally
      #   with their scores, in case the :withscores option is given)
      def zrevrangebyscore(key, min, max, opts = {})
        common_rangebyscore(:ZREVRANGEBYSCORE, key, min, max, opts)
      end

      # Determine the index of a member in a sorted set, with scores ordered from
      # high to low
      # @see http://redis.io/commands/zrevrank
      #
      # @param [String] key
      # @param [String] member
      #
      # @return [Integer, nil] the rank of member, or nil if member does not
      # exists in the sorted set or key does not exists
      def zrevrank(key, member)
        run(:ZREVRANK, key, member)
      end

      # Get the score associated with the given member in a sorted set
      # @see http://redis.io/commands/zscore
      #
      # @param [String] key
      # @param [String] member
      #
      # @return [String, nil] the score of member (a double precision floating
      # point number), represented as string, or nil if member does not exist in
      # the sorted set, or key does not exists
      def zscore(key, member)
        run(:ZSCORE, key, member)
      end

      # Incrementally iterate sorted sets elements and associated scores
      # @see https://redis.io/commands/zscan
      #
      # @param [Integer] cursor
      # @param [Hash] opts
      #
      # @option opts [Integer] :count Amount of work that should be done at
      #   every call in order to retrieve elements from the collection.
      # @option opts [String] :match
      #
      # @return [Array] two elements array, where the first element is String
      #   representing an unsigned 64 bit number (the cursor), and the second
      #   element is an Array of elements.
      def zscan(key, cursor, opts = {})
        args = [:ZSCAN, key, cursor]
        Scan.merge_opts!(args, opts)
        run(*args)
      end

      # Add multiple sorted sets and store the resulting sorted set in a new key
      # @see https://redis.io/commands/zunionstore
      #
      # @param [String] destination key
      # @param [Integer] numkeys number of sorted sets
      # @param [String, Array<String>] keys
      # @param [Hash] opts
      #
      # @option opts [Array<Float>] :weights multiplication factor for each
      #   input sorted set.
      # @option opts [Symbol] :aggregate how the results of the union
      #   are aggregated.
      #
      # @return [Integer] the number of elements in the resulting sorted set
      #   at destination.
      def zunionstore(destination, numkeys, *keys, **opts)
        common_store(:ZUNIONSTORE, destination, numkeys, keys, opts)
      end

      # Intersect multiple sorted sets and store the resulting sorted set in
      #   a new key.
      # @see https://redis.io/commands/zinterstore
      #
      # @param [String] destination key
      # @param [Integer] numkeys number of sorted sets
      # @param [String, Array<String>] keys
      # @param [Hash] opts
      #
      # @option opts [Array<Float>] :weights multiplication factor for each
      #   input sorted set.
      # @option opts [Symbol] :aggregate how the results of the union
      #   are aggregated.
      #
      # @return [Integer] the number of elements in the resulting sorted set
      #   at destination.
      def zinterstore(destination, numkeys, *keys, **opts)
        common_store(:ZINTERSTORE, destination, numkeys, keys, opts)
      end

      private

      def common_rangebyscore(command_name, key, min, max, opts)
        args = [command_name, key, min, max]
        args << :WITHSCORES if opts[:withscores]
        args.push(:LIMIT).concat(opts[:limit]) if opts[:limit].is_a?(Array)

        run(*args)
      end

      def common_range(command_name, key, start, stop, opts)
        args = [command_name, key, start, stop]
        args << :WITHSCORES if opts[:withscores]
        run(*args)
      end

      def common_store(command_name, destination, numkeys, *keys, **opts)
        args = keys.unshift(command_name, destination, numkeys)

        if v = opts[:weights]
          args.push(:WEIGHTS).concat(v)
        end

        if v = opts[:aggregate]
          args.push(:AGGREGATE, v)
        end

        run(*args)
      end

      def common_rangebylex(command_name, key, min, max, opts)
        args = [command_name, key, min, max]
        args.push(:LIMIT).concat(opts[:limit]) if opts[:limit].is_a?(Array)

        run(*args)
      end
    end
  end
end