Mordil/RediStack

View on GitHub
Sources/RediStack/Commands/SortedSetCommands.swift

Summary

Maintainability
A
1 hr
Test Coverage
//===----------------------------------------------------------------------===//
//
// This source file is part of the RediStack open source project
//
// Copyright (c) 2019-2022 RediStack project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of RediStack project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import struct Logging.Logger
import NIOCore

// MARK: Sorted Sets

extension RedisCommand {
    /// [BZPOPMIN](https://redis.io/commands/bzpopmin)
    /// - Warning:
    ///     This will block the connection from completing further commands until an element is available to pop from the group of sets.
    ///
    ///     It is **highly** recommended to set a reasonable `timeout` or to use the non-blocking `zpopmin` method where possible.
    /// - Parameters:
    ///     - key: The key identifying the sorted set in Redis.
    ///     - timeout: The max time to wait for a value to use. `0`seconds means to wait indefinitely.
    public static func bzpopmin(
        from key: RedisKey,
        timeout: TimeAmount = .seconds(0)
    ) -> RedisCommand<(Double, RESPValue)?> {
        return ._bzpop(keyword: "BZPOPMIN", [key], timeout, { result in
            result.map { ($0.1, $0.2) }
        })
    }

    /// [BZPOPMIN](https://redis.io/commands/bzpopmin)
    /// - Warning:
    ///     This will block the connection from completing further commands until an element is available to pop from the group of sets.
    ///
    ///     It is **highly** recommended to set a reasonable `timeout` or to use the non-blocking `zpopmin` method where possible.
    /// - Parameters:
    ///     - keys: A list of sorted set keys in Redis.
    ///     - timeout: The max time to wait for a value to use. `0`seconds means to wait indefinitely.
    public static func bzpopmin(
        from keys: [RedisKey],
        timeout: TimeAmount = .seconds(0)
    ) -> RedisCommand<(String, Double, RESPValue)?> { ._bzpop(keyword: "BZPOPMIN", keys, timeout, { $0 }) }

    /// [BZPOPMIN](https://redis.io/commands/bzpopmin)
    /// - Warning:
    ///     This will block the connection from completing further commands until an element is available to pop from the group of sets.
    ///
    ///     It is **highly** recommended to set a reasonable `timeout` or to use the non-blocking `zpopmin` method where possible.
    /// - Parameters:
    ///     - keys: A list of sorted set keys in Redis.
    ///     - timeout: The max time to wait for a value to use. `0`seconds means to wait indefinitely.
    public static func bzpopmin(
        from keys: RedisKey...,
        timeout: TimeAmount = .seconds(0)
    ) -> RedisCommand<(String, Double, RESPValue)?> { .bzpopmin(from: keys, timeout: timeout) }

    /// [BZPOPMAX](https://redis.io/commands/bzpopmax)
    /// - Warning:
    ///     This will block the connection from completing further commands until an element is available to pop from the set.
    ///
    ///     It is **highly** recommended to set a reasonable `timeout` or to use the non-blocking `zpopmax` method where possible.
    /// - Parameters:
    ///     - key: The key identifying the sorted set in Redis.
    ///     - timeout: The max time to wait for a value to use. `0`seconds means to wait indefinitely.
    public static func bzpopmax(
        from key: RedisKey,
        timeout: TimeAmount = .seconds(0)
    ) -> RedisCommand<(Double, RESPValue)?> {
        return ._bzpop(keyword: "BZPOPMAX", [key], timeout, { result in
            result.map { ($0.1, $0.2) }
        })
    }

    /// [BZPOPMAX](https://redis.io/commands/bzpopmax)
    /// - Warning:
    ///     This will block the connection from completing further commands until an element is available to pop from the set.
    ///
    ///     It is **highly** recommended to set a reasonable `timeout` or to use the non-blocking `zpopmax` method where possible.
    /// - Parameters:
    ///     - key: The key identifying the sorted set in Redis.
    ///     - timeout: The max time to wait for a value to use. `0`seconds means to wait indefinitely.
    public static func bzpopmax(
        from keys: [RedisKey],
        timeout: TimeAmount = .seconds(0)
    ) -> RedisCommand<(String, Double, RESPValue)?> { ._bzpop(keyword: "BZPOPMAX", keys, timeout, { $0 }) }

    /// [BZPOPMAX](https://redis.io/commands/bzpopmax)
    /// - Warning:
    ///     This will block the connection from completing further commands until an element is available to pop from the set.
    ///
    ///     It is **highly** recommended to set a reasonable `timeout` or to use the non-blocking `zpopmax` method where possible.
    /// - Parameters:
    ///     - key: The key identifying the sorted set in Redis.
    ///     - timeout: The max time to wait for a value to use. `0`seconds means to wait indefinitely.
    public static func bzpopmax(
        from keys: RedisKey...,
        timeout: TimeAmount = .seconds(0)
    ) -> RedisCommand<(String, Double, RESPValue)?> { .bzpopmax(from: keys, timeout: timeout) }

    /// [ZADD](https://redis.io/commands/zadd)
    /// - Parameters:
    ///     - element: The element and its score to add to the sorted set.
    ///     - key: The key of the sorted set.
    ///     - insertBehavior: The desired behavior of handling new and existing elements in the SortedSet.
    ///     - returnBehavior: The desired behavior of what the return value should represent.
    @inlinable
    public static func zadd<Value: RESPValueConvertible>(
        _ element: (value: Value, score: Double),
        to key: RedisKey,
        inserting insertBehavior: RedisZaddInsertBehavior = .allElements,
        returning returnBehavior: RedisZaddReturnBehavior = .insertedElementsCount
    ) -> RedisCommand<Bool> {
        var args = [RESPValue(from: key)]
        args.append(convertingContentsOf: [insertBehavior.string, returnBehavior.string].compactMap({ $0 }))
        args.append(contentsOf: [.init(bulk: element.score.description), element.value.convertedToRESPValue()])
        return .init(keyword: "ZADD", arguments: args)
    }

    /// [ZADD](https://redis.io/commands/zadd)
    /// - Parameters:
    ///     - elements: A list of elements and their score to add to the sorted set.
    ///     - key: The key of the sorted set.
    ///     - insertBehavior: The desired behavior of handling new and existing elements in the SortedSet.
    ///     - returnBehavior: The desired behavior of what the return value should represent.
    @inlinable
    public static func zadd<Value: RESPValueConvertible>(
        _ elements: [(element: Value, score: Double)],
        to key: RedisKey,
        inserting insertBehavior: RedisZaddInsertBehavior = .allElements,
        returning returnBehavior: RedisZaddReturnBehavior = .insertedElementsCount
    ) -> RedisCommand<Int> {
        var args = [RESPValue(from: key)]

        args.append(convertingContentsOf: [insertBehavior.string, returnBehavior.string].compactMap({ $0 }))
        args.add(contentsOf: elements, overestimatedCountBeingAdded: elements.count * 2) { array, next in
            array.append(.init(bulk: next.score.description))
            array.append(next.element.convertedToRESPValue())
        }

        return .init(keyword: "ZADD", arguments: args)
    }

    /// [ZADD](https://redis.io/commands/zadd)
    /// - Parameters:
    ///     - elements: A list of elements and their score to add to the sorted set.
    ///     - key: The key of the sorted set.
    ///     - insertBehavior: The desired behavior of handling new and existing elements in the SortedSet.
    ///     - returnBehavior: The desired behavior of what the return value should represent.
    @inlinable
    public static func zadd<Value: RESPValueConvertible>(
        _ elements: (element: Value, score: Double)...,
        to key: RedisKey,
        inserting insertBehavior: RedisZaddInsertBehavior = .allElements,
        returning returnBehavior: RedisZaddReturnBehavior = .insertedElementsCount
    ) -> RedisCommand<Int> { .zadd(elements, to: key, inserting: insertBehavior, returning: returnBehavior) }

    /// [ZCARD](https://redis.io/commands/zcard)
    /// - Parameter key: The key of the sorted set.
    public static func zcard(of key: RedisKey) -> RedisCommand<Int> {
        let args = [RESPValue(from: key)]
        return .init(keyword: "ZCARD", arguments: args)
    }

    /// [ZCOUNT](https://redis.io/commands/zcount)
    ///
    /// Example usage:
    /// To get a count of elements that have at least the score of 3, but no greater than 10:
    /// ```swift
    /// // count elements with score of 3...10
    /// client.send(.zcount(of: "mySortedSet", withScoresBetween: (3, 10)))
    ///
    /// // count elements with score 3..<10
    /// client.send(.zcount(of: "mySortedSet", withScoresBetween: (3, .exclusive(10))))
    /// ```
    /// - Parameters:
    ///     - key: The key of the SortedSet that will be counted.
    ///     - range: The min and max score bounds that an element should have in order to be counted.
    public static func zcount(
        of key: RedisKey,
        withScoresBetween range: (min: RedisZScoreBound, max: RedisZScoreBound)
    ) -> RedisCommand<Int> {
        let args: [RESPValue] = [
            .init(from: key),
            .init(bulk: range.min.description),
            .init(bulk: range.max.description)
        ]
        return .init(keyword: "ZCOUNT", arguments: args)
    }

    /// [ZCOUNT](https://redis.io/commands/zcount)
    ///
    /// Example usage:
    /// ```swift
    /// // count of elements with at least score of 3, but no greater than 10
    /// client.send(.zcount(of: "mySortedSet", withScores: 3...10)
    /// ```
    /// - Parameters:
    ///     - key: The key of the SortedSet that will be counted.
    ///     - range: The inclusive range of scores to filter elements to count.
    public static func zcount(of key: RedisKey, withScores range: ClosedRange<Double>) -> RedisCommand<Int> {
        return .zcount(of: key, withScoresBetween: (.inclusive(range.lowerBound), .inclusive(range.upperBound)))
    }

    /// [ZCOUNT](https://redis.io/commands/zcount)
    ///
    /// Example usage:
    /// ```swift
    /// // count of elements with at least score of 3, but less than 10
    /// client.send(.zcount(of: "mySortedSet", withScores: 3..<10)
    /// ```
    /// - Parameters:
    ///     - key: The key of the SortedSet that will be counted.
    ///     - range: A range with an inclusive lower and exclusive upper bound of scores to filter elements to count.
    public static func zcount(of key: RedisKey, withScores range: Range<Double>) -> RedisCommand<Int> {
        return .zcount(of: key, withScoresBetween: (.inclusive(range.lowerBound), .exclusive(range.upperBound)))
    }

    /// [ZCOUNT](https://redis.io/commands/zcount)
    /// - Parameters:
    ///     - key: The key of the SortedSet that will be counted.
    ///     - minScore: The minimum score bound an element in the SortedSet should have in order to be counted.
    public static func zcount(of key: RedisKey, withMinimumScoreOf minScore: RedisZScoreBound) -> RedisCommand<Int> {
        return .zcount(of: key, withScoresBetween: (minScore, .inclusive(.infinity)))
    }

    /// [ZCOUNT](https://redis.io/commands/zcount)
    /// - Parameters:
    ///     - key: The key of the SortedSet that will be counted.
    ///     - maxScore: The maximum score bound an element in the SortedSet should have in order to be counted.
    public static func zcount(of key: RedisKey, withMaximumScoreOf maxScore: RedisZScoreBound) -> RedisCommand<Int> {
        return .zcount(of: key, withScoresBetween: (.inclusive(-.infinity), maxScore))
    }

    /// [ZINCRBY](https://redis.io/commands/zincrby)
    /// - Parameters:
    ///     - element: The element to increment.
    ///     - key: The key of the sorted set.
    ///     - amount: The amount to increment this element's score by.
    @inlinable
    public static func zincrby<Value: RESPValueConvertible>(
        _ element: Value,
        in key: RedisKey,
        by amount: Double
    ) -> RedisCommand<Double> {
        let args: [RESPValue] = [
            .init(from: key),
            .init(bulk: amount.description),
            element.convertedToRESPValue()
        ]
        return .init(keyword: "ZINCRBY", arguments: args)
    }

    /// [ZINTERSTORE](https://redis.io/commands/zinterstore)
    /// - Warning: This operation overwrites any value stored at the `destination` key.
    /// - Parameters:
    ///     - destination: The key of the new sorted set from the result.
    ///     - sources: The list of sorted set keys to treat as the source of the intersection.
    ///     - weights: The multiplying factor to apply to the corresponding `sources` key based on index of the two parameters.
    ///     - aggregateMethod: The method of aggregating the values of the intersection. If one isn't specified, Redis will default to `.sum`.
    public static func zinterstore(
        as destination: RedisKey,
        sources: [RedisKey],
        weights: [Int]? = nil,
        aggregateMethod aggregate: RedisSortedSetAggregateMethod? = nil
    ) -> RedisCommand<Int> { ._zstore(keyword: "ZINTERSTORE", sources, destination, weights, aggregate) }

    /// [ZLEXCOUNT](https://redis.io/commands/zlexcount)
    ///
    /// Example usage:
    /// ```swift
    /// // "mySortedSet" contains the values [1, 2, 3, 10] each with a score of 1.
    ///
    /// client.send(.zlexcount(
    ///     of: "mySortedSet",
    ///     withValuesBetween: (.inclusive(1), .inclusive(3))
    /// ))
    /// // the response will resolve to 4, as both 10 and 1 have the value "1"
    /// ```
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the returned elements are unspecified.
    /// - Parameters:
    ///     - key: The key of the SortedSet that will be counted.
    ///     - range: The min and max value bounds that an element should have in order to be counted.
    @inlinable
    public static func zlexcount<Value: CustomStringConvertible>(
        of key: RedisKey,
        withValuesBetween range: (min: RedisZLexBound<Value>, max: RedisZLexBound<Value>)
    ) -> RedisCommand<Int> {
        let args: [RESPValue] = [
            .init(from: key),
            .init(bulk: range.min.description),
            .init(bulk: range.max.description)
        ]
        return .init(keyword: "ZLEXCOUNT", arguments: args)
    }

    /// [ZLEXCOUNT](https://redis.io/commands/zlexcount)
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the returned elements are unspecified.
    /// - Parameters:
    ///     - key: The key of the SortedSet that will be counted.
    ///     - minValue: The minimum lexiographical value an element in the SortedSet should have in order to be counted.
    @inlinable
    public static func zlexcount<Value: CustomStringConvertible>(
        of key: RedisKey,
        withMinimumValueOf minValue: RedisZLexBound<Value>
    ) -> RedisCommand<Int> { .zlexcount(of: key, withValuesBetween: (minValue, .positiveInfinity)) }

    /// [ZLEXCOUNT](https://redis.io/commands/zlexcount)
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the returned elements are unspecified.
    /// - Parameters:
    ///     - key: The key of the SortedSet that will be counted.
    ///     - maxValue: The maximum lexiographical value an element in the SortedSet should have in order to be counted.
    @inlinable
    public static func zlexcount<Value: CustomStringConvertible>(
        of key: RedisKey,
        withMaximumValueOf maxValue: RedisZLexBound<Value>
    ) -> RedisCommand<Int> { .zlexcount(of: key, withValuesBetween: (.negativeInfinity, maxValue)) }

    /// [ZPOPMAX](https://redis.io/commands/zpopmax)
    /// - Parameter key: The key identifying the sorted set in Redis.
    public static func zpopmax(from key: RedisKey) -> RedisCommand<(RESPValue, Double)?> {
        return ._zpop(keyword: "ZPOPMAX", nil, key, { $0.isEmpty ? nil : $0[0] })
    }

    /// [ZPOPMAX](https://redis.io/commands/zpopmax)
    /// - Parameters:
    ///     - key: The key identifying the sorted set in Redis.
    ///     - count: The max number of elements to pop from the set.
    public static func zpopmax(from key: RedisKey, max count: Int) -> RedisCommand<[(RESPValue, Double)]> {
        return ._zpop(keyword: "ZPOPMAX", count, key, { $0 })
    }

    /// [ZPOPMIN](https://redis.io/commands/zpopmin)
    /// - Parameter key: The key identifying the sorted set in Redis.
    public static func zpopmin(from key: RedisKey) -> RedisCommand<(RESPValue, Double)?> {
        return ._zpop(keyword: "ZPOPMIN", nil, key, { $0.isEmpty ? nil : $0[0] })
    }

    /// [ZPOPMIN](https://redis.io/commands/zpopmin)
    /// - Parameters:
    ///     - key: The key identifying the sorted set in Redis.
    ///     - count: The max number of elements to pop from the set.
    public static func zpopmin(from key: RedisKey, max count: Int) -> RedisCommand<[(RESPValue, Double)]> {
        return ._zpop(keyword: "ZPOPMIN", count, key, { $0 })
    }

    /// [ZRANGE](https://redis.io/commands/zrange)
    /// - Parameters:
    ///     - key: The key of the SortedSet
    ///     - firstIndex: The index of the first element to include in the range of elements returned.
    ///     - lastIndex: The index of the last element to include in the range of elements returned.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrange<T>(
        from key: RedisKey,
        firstIndex: Int,
        lastIndex: Int,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> { ._zrange(keyword: "ZRANGE", key, firstIndex, lastIndex, resultOption) }

    /// [ZRANGE](https://redis.io/commands/zrange)
    /// - Precondition: A `ClosedRange` cannot be created where `upperBound` is less than `lowerBound`; so while Redis may support `0...-1`,
    ///     `ClosedRange` will trigger a precondition failure.
    ///
    ///     If you need such a range, use ``zrange(from:firstIndex:lastIndex:resultOption:)`` instead.
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see `zrevrange(from:indices:returning:)`.
    /// - Parameters:
    ///     - key: The key of the SortedSet to return elements from.
    ///     - range: The range of inclusive indices of elements to get.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrange<T>(
        from key: RedisKey,
        indices range: ClosedRange<Int>,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound, returning: resultOption)
    }

    /// [ZRANGE](https://redis.io/commands/zrange)
    /// - Precondition: A `Range` cannot be created where `upperBound` is less than `lowerBound`; so while Redis may support `0..<(-1)`,
    ///     `Range` will trigger a precondition failure.
    ///
    ///     If you need such a range, use ``zrange(from:firstIndex:lastIndex:)`` instead.
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see `zrevrange(from:indices:returning:)`.
    /// - Parameters:
    ///     - key: The key of the SortedSet to return elements from.
    ///     - range: The range of indices (inclusive lower, exclusive upper) elements to get.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrange<T>(
        from key: RedisKey,
        indices range: Range<Int>,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound - 1, returning: resultOption)
    }

    /// [ZRANGE](https://redis.io/commands/zrange)
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see ``zrevrange(from:fromIndex:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet to return elements from.
    ///     - index: The index of the first element that will be in the returned values.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrange<T>(
        from key: RedisKey,
        fromIndex index: Int,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrange(from: key, firstIndex: index, lastIndex: -1, returning: resultOption)
    }

    /// [ZRANGE](https://redis.io/commands/zrange)
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see ``zrevrange(from:throughIndex:returning:)`.
    /// - Parameters:
    ///     - key: The key of the SortedSet to return elements from.
    ///     - index: The index of the last element that will be in the returned values.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrange<T>(
        from key: RedisKey,
        throughIndex index: Int,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrange(from: key, firstIndex: 0, lastIndex: index, returning: resultOption)
    }

    /// [ZRANGE](https://redis.io/commands/zrange)
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see ``zrevrange(from:upToIndex:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet to return elements from.
    ///     - index: The index of the last element to not include in the returned values.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrange<T>(
        from key: RedisKey,
        upToIndex index: Int,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrange(from: key, firstIndex: 0, lastIndex: index - 1, returning: resultOption)
    }

    /// [ZRANGEBYLEX](https://redis.io/commands/zrangebylex)
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the returned elements are unspecified.
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see ``zrevrangebylex(from:withValuesBetween:limitBy:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet that will be counted.
    ///     - range: The min and max value bounds for filtering elements by.
    ///     - limitBy: The optional offset and count of elements to query.
    @inlinable
    public static func zrangebylex<Value: CustomStringConvertible>(
        from key: RedisKey,
        withValuesBetween range: (min: RedisZLexBound<Value>, max: RedisZLexBound<Value>),
        limitBy limit: (offset: Int, count: Int)? = nil
    ) -> RedisCommand<[RESPValue]> {
        return ._zrangebylex(keyword: "ZRANGEBYLEX", key, (range.min.description, range.max.description), limit)
    }

    /// [ZRANGEBYLEX](https://redis.io/commands/zrangebylex)
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the returned elements are unspecified.
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see ``zrevrangebylex(from:withMinimumValueOf:limitBy:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - minValue: The minimum lexiographical value an element in the SortedSet should have to be included in the result set.
    ///     - limit: The optional offset and count of elements to query
    @inlinable
    public static func zrangebylex<Value: CustomStringConvertible>(
        from key: RedisKey,
        withMinimumValueOf minValue: RedisZLexBound<Value>,
        limitBy limit: (offset: Int, count: Int)? = nil
    ) -> RedisCommand<[RESPValue]> {
        return .zrangebylex(from: key, withValuesBetween: (minValue, .positiveInfinity), limitBy: limit)
    }

    /// [ZRANGEBYLEX](https://redis.io/commands/zrangebylex)
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the returned elements are unspecified.
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see ``zrevrangebylex(from:withMaximumValueOf:limitBy:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - minValue: The maximum lexiographical value an element in the SortedSet should have to be included in the result set.
    ///     - limit: The optional offset and count of elements to query
    @inlinable
    public static func zrangebylex<Value: CustomStringConvertible>(
        from key: RedisKey,
        withMaximumValueOf maxValue: RedisZLexBound<Value>,
        limitBy limit: (offset: Int, count: Int)? = nil
    ) -> RedisCommand<[RESPValue]> {
        return .zrangebylex(from: key, withValuesBetween: (.negativeInfinity, maxValue), limitBy: limit)
    }

    /// [ZREVRANGEBYLEX](https://redis.io/commands/zrevrangebylex)
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the returned elements are unspecified.
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrangebylex(from:withValuesBetween:limitBy:)`.
    /// - Parameters:
    ///     - key: The key of the SortedSet that will be counted.
    ///     - range: The min and max value bounds for filtering elements by.
    ///     - limitBy: The optional offset and count of elements to query.
    @inlinable
    public static func zrevrangebylex<Value: CustomStringConvertible>(
        from key: RedisKey,
        withValuesBetween range: (min: RedisZLexBound<Value>, max: RedisZLexBound<Value>),
        limitBy limit: (offset: Int, count: Int)? = nil
    ) -> RedisCommand<[RESPValue]> {
        return ._zrangebylex(keyword: "ZREVRANGEBYLEX", key, (range.max.description, range.min.description), limit)
    }

    /// [ZREVRANGEBYLEX](https://redis.io/commands/zrevrangebylex)
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the returned elements are unspecified.
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrangebylex(from:withMinimumValueOf:limitBy:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - minValue: The minimum lexiographical value an element in the SortedSet should have to be included in the result set.
    ///     - limit: The optional offset and count of elements to query
    @inlinable
    public static func zrevrangebylex<Value: CustomStringConvertible>(
        from key: RedisKey,
        withMinimumValueOf minValue: RedisZLexBound<Value>,
        limitBy limit: (offset: Int, count: Int)? = nil
    ) -> RedisCommand<[RESPValue]> {
        return .zrevrangebylex(from: key, withValuesBetween: (minValue, .positiveInfinity), limitBy: limit)
    }

    /// [ZREVRANGEBYLEX](https://redis.io/commands/zrevrangebylex)
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the returned elements are unspecified.
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrangebylex(from:withMaximumValueOf:limitBy:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - minValue: The maximum lexiographical value an element in the SortedSet should have to be included in the result set.
    ///     - limit: The optional offset and count of elements to query
    @inlinable
    public static func zrevrangebylex<Value: CustomStringConvertible>(
        from key: RedisKey,
        withMaximumValueOf maxValue: RedisZLexBound<Value>,
        limitBy limit: (offset: Int, count: Int)? = nil
    ) -> RedisCommand<[RESPValue]> {
        return .zrevrangebylex(from: key, withValuesBetween: (.negativeInfinity, maxValue), limitBy: limit)
    }

    /// [ZRANGEBYSCORE](https://redis.io/commands/zrangebyscore)
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see ``zrevrangebyscore(from:withScoresBetween:limitBy:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - range: The min and max score bounds to filter elements by.
    ///     - limit: The optional offset and count of elements to query.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrangebyscore<T>(
        from key: RedisKey,
        withScoresBetween range: (min: RedisZScoreBound, max: RedisZScoreBound),
        limitBy limit: (offset: Int, count: Int)? = nil,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return ._zrangebyscore(keyword: "ZRANGEBYSCORE", key, (range.min.description, range.max.description), limit, resultOption)
    }

    /// [ZRANGEBYSCORE](https://redis.io/commands/zrangebyscore)
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see ``zrevrangebyscore(from:withScores:limitBy:returning:)-2vp67``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - range: The inclusive range of scores to filter elements by.
    ///     - limit: The optional offset and count of elements to query.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrangebyscore<T>(
        from key: RedisKey,
        withScores range: ClosedRange<Double>,
        limitBy limit: (offset: Int, count: Int)? = nil,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrangebyscore(
            from: key,
            withScoresBetween: (.inclusive(range.lowerBound), .inclusive(range.upperBound)),
            limitBy: limit,
            returning: resultOption
        )
    }

    /// [ZRANGEBYSCORE](https://redis.io/commands/zrangebyscore)
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see ``zrevrangebyscore(from:withScores:limitBy:returning:)-3jdpl``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - range: A range with an inclusive lower and exclusive upper bound of scores to filter elements by.
    ///     - limit: The optional offset and count of elements to query.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrangebyscore<T>(
        from key: RedisKey,
        withScores range: Range<Double>,
        limitBy limit: (offset: Int, count: Int)? = nil,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrangebyscore(
            from: key,
            withScoresBetween: (.inclusive(range.lowerBound), .exclusive(range.upperBound)),
            limitBy: limit,
            returning: resultOption
        )
    }

    /// [ZRANGEBYSCORE](https://redis.io/commands/zrangebyscore)
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see ``zrevrangebyscore(from:withMinimumScoreOf:limitBy:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - range: The minimum score bound an element in the SortedSet should have to be included in the response.
    ///     - limit: The optional offset and count of elements to query.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrangebyscore<T>(
        from key: RedisKey,
        withMinimumScoreOf minScore: RedisZScoreBound,
        limitBy limit: (offset: Int, count: Int)? = nil,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrangebyscore(
            from: key,
            withScoresBetween: (minScore, .inclusive(.infinity)),
            limitBy: limit,
            returning: resultOption
        )
    }

    /// [ZRANGEBYSCORE](https://redis.io/commands/zrangebyscore)
    /// - Important: This treats the SortedSet as ordered from **low** to **high**.
    ///
    /// For the inverse, see ``zrevrangebyscore(from:withMaximumScoreOf:limitBy:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - range: The maximum score bound an element in the SortedSet should have to be included in the response.
    ///     - limit: The optional offset and count of elements to query.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrangebyscore<T>(
        from key: RedisKey,
        withMaximumScoreOf maxScore: RedisZScoreBound,
        limitBy limit: (offset: Int, count: Int)? = nil,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrangebyscore(
            from: key,
            withScoresBetween: (.inclusive(-.infinity), maxScore),
            limitBy: limit,
            returning: resultOption
        )
    }

    /// [ZRANK](https://redis.io/commands/zrank)
    /// - Important: This treats the ordered set as ordered from low to high.
    ///
    /// For the inverse, see ``zrevrank(of:in:)``.
    /// - Parameters:
    ///     - element: The element in the sorted set to search for.
    ///     - key: The key of the sorted set to search.
    @inlinable
    public static func zrank<Value: RESPValueConvertible>(of element: Value, in key: RedisKey) -> RedisCommand<Int?> {
        let args: [RESPValue] = [
            .init(from: key),
            element.convertedToRESPValue()
        ]
        return .init(keyword: "ZRANK", arguments: args)
    }

    /// [ZREM](https://redis.io/commands/zrem)
    /// - Parameters:
    ///     - elements: The values to remove from the sorted set.
    ///     - key: The key of the sorted set.
    @inlinable
    public static func zrem<Value: RESPValueConvertible>(_ elements: [Value], from key: RedisKey) -> RedisCommand<Int> {
        var args = [RESPValue(from: key)]
        args.append(convertingContentsOf: elements)
        return .init(keyword: "ZREM", arguments: args)
    }

    /// [ZREM](https://redis.io/commands/zrem)
    /// - Parameters:
    ///     - elements: The values to remove from the sorted set.
    ///     - key: The key of the sorted set.
    @inlinable
    public static func zrem<Value: RESPValueConvertible>(_ elements: Value..., from key: RedisKey) -> RedisCommand<Int> {
        return .zrem(elements, from: key)
    }

    /// [ZREMRANGEBYLEX](https://redis.io/commands/zremrangebylex)
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the elements removed are unspecified.
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - range: The min and max value bounds that an element should have to be removed.
    @inlinable
    public static func zremrangebylex<Value: CustomStringConvertible>(
        from key: RedisKey,
        withValuesBetween range: (min: RedisZLexBound<Value>, max: RedisZLexBound<Value>)
    ) -> RedisCommand<Int> {
        let args: [RESPValue] = [
            .init(from: key),
            .init(bulk: range.min.description),
            .init(bulk: range.max.description)
        ]
        return .init(keyword: "ZREMRANGEBYLEX", arguments: args)
    }

    /// [ZREMRANGEBYLEX](https://redis.io/commands/zremrangebylex)
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the elements removed are unspecified.
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - minValue: The minimum lexiographical value an element in the SortedSet should have to be removed.
    @inlinable
    public static func zremrangebylex<Value: CustomStringConvertible>(
        from key: RedisKey,
        withMinimumValueOf minValue: RedisZLexBound<Value>
    ) -> RedisCommand<Int> { .zremrangebylex(from: key, withValuesBetween: (minValue, .positiveInfinity)) }

    /// [ZREMRANGEBYLEX](https://redis.io/commands/zremrangebylex)
    /// - Warning: This assumes all elements in the SortedSet have the same score. If not, the elements removed are unspecified.
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - maxValue: The maximum lexiographical value and element in the SortedSet should have to be removed.
    @inlinable
    public static func zremrangebylex<Value: CustomStringConvertible>(
        from key: RedisKey,
        withMaximumValueOf maxValue: RedisZLexBound<Value>
    ) -> RedisCommand<Int> { .zremrangebylex(from: key, withValuesBetween: (.negativeInfinity, maxValue)) }

    /// [ZREMRANGEBYRANK](https://redis.io/commands/zremrangebyrank)
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - firstIndex: The index of the first element to remove.
    ///     - lastIndex: The index of the last element to remove.
    public static func zremrangebyrank(from key: RedisKey, firstIndex: Int, lastIndex: Int) -> RedisCommand<Int> {
        let args: [RESPValue] = [
            .init(from: key),
            .init(bulk: firstIndex),
            .init(bulk: lastIndex)
        ]
        return .init(keyword: "ZREMRANGEBYRANK", arguments: args)
    }

    /// [ZREMRANGEBYRANK](https://redis.io/commands/zremrangebyrank)
    /// - Precondition: A `ClosedRange` cannot be created where `upperBound` is less than `lowerBound`; so while Redis may support `0...-1`,
    ///     `ClosedRange` will trigger a precondition failure.
    ///
    ///     If you need such a range, use ``zremrangebyrank(from:firstIndex:lastIndex:)`` instead.
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - range: The range of inclusive indices of elements to remove.
    public static func zremrangebyrank(from key: RedisKey, indices range: ClosedRange<Int>) -> RedisCommand<Int> {
        return .zremrangebyrank(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound)
    }

    /// [ZREMRANGEBYRANK](https://redis.io/commands/zremrangebyrank)
    /// - Precondition: A `Range` cannot be created where `upperBound` is less than `lowerBound`; so while Redis may support `0...-1`,
    ///     `Range` will trigger a precondition failure.
    ///
    ///     If you need such a range, use ``zremrangebyrank(from:firstIndex:lastIndex:)`` instead.
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - range: The range of indices (inclusive lower, exclusive upper) elements to remove.
    public static func zremrangebyrank(from key: RedisKey, indices range: Range<Int>) -> RedisCommand<Int> {
        return .zremrangebyrank(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound - 1)
    }

    /// [ZREMRANGEBYRANK](https://redis.io/commands/zremrangebyrank)
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - index: The index of the first element that will be removed.
    public static func zremrangebyrank(from key: RedisKey, fromIndex index: Int) -> RedisCommand<Int> {
        return .zremrangebyrank(from: key, firstIndex: index, lastIndex: -1)
    }

    /// [ZREMRANGEBYRANK](https://redis.io/commands/zremrangebyrank)
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - index: The index of the last element that will be removed.
    public static func zremrangebyrank(from key: RedisKey, throughIndex index: Int) -> RedisCommand<Int> {
        return .zremrangebyrank(from: key, firstIndex: 0, lastIndex: index)
    }

    /// [ZREMRANGEBYRANK](https://redis.io/commands/zremrangebyrank)
    /// - Warning: Providing an index of `0` will remove all elements from the SortedSet.
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - index: The index of the last element to not remove.
    public static func zremrangebyrank(from key: RedisKey, upToIndex index: Int) -> RedisCommand<Int> {
        return .zremrangebyrank(from: key, firstIndex: 0, lastIndex: index - 1)
    }

    /// [ZREMRANGEBYSCORE](https://redis.io/commands/zremrangebyscore)
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - range: The min and max score bounds to filter elements by.
    public static func zremrangebyscore(
        from key: RedisKey,
        withScoresBetween range: (min: RedisZScoreBound, max: RedisZScoreBound)
    ) -> RedisCommand<Int> {
        let args: [RESPValue] = [
            .init(from: key),
            .init(bulk: range.min.description),
            .init(bulk: range.max.description)
        ]
        return .init(keyword: "ZREMRANGEBYSCORE", arguments: args)
    }

    /// [ZREMRANGEBYSCORE](https://redis.io/commands/zremrangebyscore)
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - range: The inclusive range of scores to filter elements by.
    public static func zremrangebyscore(from key: RedisKey, withScores range: ClosedRange<Double>) -> RedisCommand<Int> {
        return .zremrangebyscore(from: key, withScoresBetween: (.inclusive(range.lowerBound), .inclusive(range.upperBound)))
    }

    /// [ZREMRANGEBYSCORE](https://redis.io/commands/zremrangebyscore)
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - range: A range with an inclusive lower and exclusive upper bound of scores to filter elements by.
    public static func zremrangebyscore(from key: RedisKey, withScores range: Range<Double>) -> RedisCommand<Int> {
        return .zremrangebyscore(from: key, withScoresBetween: (.inclusive(range.lowerBound), .exclusive(range.upperBound)))
    }

    /// [ZREMRANGEBYSCORE](https://redis.io/commands/zremrangebyscore)
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - minScore: The minimum score bound an element in the SortedSet should have to be removed.
    public static func zremrangebyscore(from key: RedisKey, withMinimumScoreOf minScore: RedisZScoreBound) -> RedisCommand<Int> {
        return .zremrangebyscore(from: key, withScoresBetween: (minScore, .inclusive(.infinity)))
    }

    /// [ZREMRANGEBYSCORE](https://redis.io/commands/zremrangebyscore)
    /// - Parameters:
    ///     - key: The key of the SortedSet to remove elements from.
    ///     - minScore: The maximum score bound an element in the SortedSet should have to be removed.
    public static func zremrangebyscore(from key: RedisKey, withMaximumScoreOf maxScore: RedisZScoreBound) -> RedisCommand<Int> {
        return .zremrangebyscore(from: key, withScoresBetween: (.inclusive(-.infinity), maxScore))
    }

    /// [ZREVRANGE](https://redis.io/commands/zrevrange)
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrange(from:firstIndex:lastIndex:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet
    ///     - firstIndex: The index of the first element to include in the range of elements returned.
    ///     - lastIndex: The index of the last element to include in the range of elements returned.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrevrange<T>(
        from key: RedisKey,
        firstIndex: Int,
        lastIndex: Int,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> { ._zrange(keyword: "ZREVRANGE", key, firstIndex, lastIndex, resultOption) }

    /// [ZREVRANGE](https://redis.io/commands/zrevrange)
    /// - Precondition: A `ClosedRange` cannot be created where `upperBound` is less than `lowerBound`; so while Redis may support `0...-1`,
    ///     `ClosedRange` will trigger a precondition failure.
    ///
    ///     If you need such a range, use ``zrevrange(from:firstIndex:lastIndex:)`` instead.
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrange(from:indices:returning:)-95y9o``.
    /// - Parameters:
    ///     - key: The key of the SortedSet to return elements from.
    ///     - range: The range of inclusive indices of elements to get.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrevrange<T>(
        from key: RedisKey,
        indices range: ClosedRange<Int>,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> { .zrevrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound, returning: resultOption) }

    /// [ZREVRANGE](https://redis.io/commands/zrevrange)
    /// - Precondition: A `Range` cannot be created where `upperBound` is less than `lowerBound`; so while Redis may support `0..<(-1)`,
    ///     `Range` will trigger a precondition failure.
    ///
    ///     If you need such a range, use ``zrevrange(from:firstIndex:lastIndex:)`` instead.
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrange(from:indices:returning:)-4pd8n``.
    /// - Parameters:
    ///     - key: The key of the SortedSet to return elements from.
    ///     - range: The range of indices (inclusive lower, exclusive upper) elements to get.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrevrange<T>(
        from key: RedisKey,
        indices range: Range<Int>,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> { .zrevrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound - 1, returning: resultOption) }

    /// [ZREVRANGE](https://redis.io/commands/zrevrange)
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrange(from:fromIndex:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet to return elements from.
    ///     - index: The index of the first element that will be in the returned values.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrevrange<T>(
        from key: RedisKey,
        fromIndex index: Int,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> { .zrevrange(from: key, firstIndex: index, lastIndex: -1, returning: resultOption) }

    /// [ZREVRANGE](https://redis.io/commands/zrevrange)
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrange(from:throughIndex:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet to return elements from.
    ///     - index: The index of the last element that will be in the returned values.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrevrange<T>(
        from key: RedisKey,
        throughIndex index: Int,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> { .zrevrange(from: key, firstIndex: 0, lastIndex: index, returning: resultOption) }

    /// [ZREVRANGE](https://redis.io/commands/zrevrange)
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrange(from:upToIndex:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet to return elements from.
    ///     - index: The index of the last element to not include in the returned values.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrevrange<T>(
        from key: RedisKey,
        upToIndex index: Int,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> { .zrevrange(from: key, firstIndex: 0, lastIndex: index - 1, returning: resultOption) }

    /// [ZREVRANGEBYSCORE](https://redis.io/commands/zrevrangebyscore)
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrangebyscore(from:withScoresBetween:limitBy:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - range: The min and max score bounds to filter elements by.
    ///     - limit: The optional offset and count of elements to query.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrevrangebyscore<T>(
        from key: RedisKey,
        withScoresBetween range: (min: RedisZScoreBound, max: RedisZScoreBound),
        limitBy limit: (offset: Int, count: Int)? = nil,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return ._zrangebyscore(keyword: "ZREVRANGEBYSCORE", key, (range.max.description, range.min.description), limit, resultOption)
    }

    /// [ZREVRANGEBYSCORE](https://redis.io/commands/zrevrangebyscore)
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrangebyscore(from:withScores:limitBy:returning:)-phw``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - range: The inclusive range of scores to filter elements by.
    ///     - limit: The optional offset and count of elements to query.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrevrangebyscore<T>(
        from key: RedisKey,
        withScores range: ClosedRange<Double>,
        limitBy limit: (offset: Int, count: Int)? = nil,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrevrangebyscore(
            from: key,
            withScoresBetween: (.inclusive(range.lowerBound), .inclusive(range.upperBound)),
            limitBy: limit,
            returning: resultOption
        )
    }

    /// [ZREVRANGEBYSCORE](https://redis.io/commands/zrevrangebyscore)
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrangebyscore(from:withScores:limitBy:returning:)-4ukbv``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - range: A range with an inclusive lower and exclusive upper bound of scores to filter elements by.
    ///     - limit: The optional offset and count of elements to query.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrevrangebyscore<T>(
        from key: RedisKey,
        withScores range: Range<Double>,
        limitBy limit: (offset: Int, count: Int)? = nil,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrevrangebyscore(
            from: key,
            withScoresBetween: (.inclusive(range.lowerBound), .exclusive(range.upperBound)),
            limitBy: limit,
            returning: resultOption
        )
    }

    /// [ZREVRANGEBYSCORE](https://redis.io/commands/zrevrangebyscore)
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrangebyscore(from:withMinimumScoreOf:limitBy:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - range: The minimum score bound an element in the SortedSet should have to be included in the response.
    ///     - limit: The optional offset and count of elements to query.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrevrangebyscore<T>(
        from key: RedisKey,
        withMinimumScoreOf minScore: RedisZScoreBound,
        limitBy limit: (offset: Int, count: Int)? = nil,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrevrangebyscore(
            from: key,
            withScoresBetween: (minScore, .inclusive(.infinity)),
            limitBy: limit,
            returning: resultOption
        )
    }

    /// [ZREVRANGEBYSCORE](https://redis.io/commands/zrevrangebyscore)
    /// - Important: This treats the SortedSet as ordered from **high** to **low**.
    ///
    /// For the inverse, see ``zrangebyscore(from:withMaximumScoreOf:limitBy:returning:)``.
    /// - Parameters:
    ///     - key: The key of the SortedSet.
    ///     - range: The maximum score bound an element in the SortedSet should have to be included in the response.
    ///     - limit: The optional offset and count of elements to query.
    ///     - resultOption: What information should be returned in the result?
    @inlinable
    public static func zrevrangebyscore<T>(
        from key: RedisKey,
        withMaximumScoreOf maxScore: RedisZScoreBound,
        limitBy limit: (offset: Int, count: Int)? = nil,
        returning resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        return .zrevrangebyscore(
            from: key,
            withScoresBetween: (.inclusive(-.infinity), maxScore),
            limitBy: limit,
            returning: resultOption
        )
    }

    /// [ZREVRANK](https://redis.io/commands/zrevrank)
    /// - Important: This treats the ordered set as ordered from high to low.
    ///
    /// For the inverse, see ``zrank(of:in:)``.
    /// - Parameters:
    ///     - element: The element in the sorted set to search for.
    ///     - key: The key of the sorted set to search.
    @inlinable
    public static func zrevrank<Value: RESPValueConvertible>(of element: Value, in key: RedisKey) -> RedisCommand<Int?> {
        let args: [RESPValue] = [
            .init(from: key),
            element.convertedToRESPValue()
        ]
        return .init(keyword: "ZREVRANK", arguments: args)
    }

    /// [ZSCORE](https://redis.io/commands/zscore)
    /// - Parameters:
    ///     - element: The element in the sorted set to get the score for.
    ///     - key: The key of the sorted set.
    @inlinable
    public static func zscore<Value: RESPValueConvertible>(of element: Value, in key: RedisKey) -> RedisCommand<Double?> {
        let args: [RESPValue] = [
            .init(from: key),
            element.convertedToRESPValue()
        ]
        return .init(keyword: "ZSCORE", arguments: args) { try? $0.map() }
    }

    /// [ZUNIONSTORE](https://redis.io/commands/zunionstore)
    /// - Warning: This operation overwrites any value stored at the destination key.
    /// - Parameters:
    ///     - destination: The key of the new sorted set from the result.
    ///     - sources: The list of sorted set keys to treat as the source of the union.
    ///     - weights: The multiplying factor to apply to the corresponding `sources` key based on index of the two parameters.
    ///     - aggregateMethod: The method of aggregating the values of the union. If one isn't specified, Redis will default to `.sum`.
    public static func zunionstore(
        as destination: RedisKey,
        sources: [RedisKey],
        weights: [Int]? = nil,
        aggregateMethod aggregate: RedisSortedSetAggregateMethod? = nil
    ) -> RedisCommand<Int> { ._zstore(keyword: "ZUNIONSTORE", sources, destination, weights, aggregate) }

    /// [ZSCAN](https://redis.io/commands/zscan)
    /// - Parameters:
    ///     - key: The key identifying the sorted set.
    ///     - position: The position to start the scan from.
    ///     - match: A glob-style pattern to filter values to be selected from the result set.
    ///     - count: The number of elements to advance by. Redis default is 10.
    public static func zscan(
        _ key: RedisKey,
        startingFrom position: Int = 0,
        matching match: String? = nil,
        count: Int? = nil
    ) -> RedisCommand<(Int, [(RESPValue, Double)])> {
        return ._scan(keyword: "ZSCAN", key, position, match, count, {
            let response = try $0.map(to: [RESPValue].self)
            return try Self._mapSortedSetResponse(response, scoreIsFirst: false)
        })
    }
}

// MARK: -

extension RedisClient {
    /// Incrementally iterates over all elements in a sorted set.
    ///
    /// See ``RedisCommand/zscan(_:startingFrom:matching:count:)``
    /// - Parameters:
    ///     - key: The key identifying the sorted set.
    ///     - position: The position to start the scan from.
    ///     - match: A glob-style pattern to filter values to be selected from the result set.
    ///     - count: The number of elements to advance by. Redis default is 10.
    ///     - eventLoop: An optional event loop to hop to for any further chaining on the returned event loop future.
    ///     - logger: An optional logger instance to use for logs generated from this command.
    /// - Returns: A `NIO.EventLoopFuture` that resolves a cursor position for additional scans,
    ///     with a limited collection of elements with their scores found in the Sorted Set.
    public func scanSortedSetValues(
        in key: RedisKey,
        startingFrom position: Int = 0,
        matching match: String? = nil,
        count: Int? = nil,
        eventLoop: EventLoop? = nil,
        logger: Logger? = nil
    ) -> EventLoopFuture<(Int, [(RESPValue, Double)])> {
        return self.send(.zscan(key, startingFrom: position, matching: match, count: count), eventLoop: eventLoop, logger: logger)
    }
}

// MARK: -

/// The supported insert behavior for a `zadd` command with Redis SortedSet types.
///
/// `zadd` normally inserts all given elements into the SortedSet, updating the score of any element that already exist in the set.
///
/// See [ZADD Options](https://redis.io/commands/zadd#zadd-options).
public struct RedisZaddInsertBehavior {
    /// Insert new elements and update the score of existing elements.
    public static let allElements = RedisZaddInsertBehavior(nil)
    /// Only insert new elements; do not update the score of existing elements.
    public static let onlyNewElements = RedisZaddInsertBehavior(.nx)
    /// Only update the score of existing elements; do not insert new elements.
    public static let onlyExistingElements = RedisZaddInsertBehavior(.xx)

    @usableFromInline
    internal var string: String? { self.option?.rawValue }

    /// Redis representation of this option.
    private enum Option: String {
        case nx = "NX"
        case xx = "XX"
    }

    private let option: Option?
    private init(_ option: Option?) { self.option = option }
}

/// The supported behavior for what a `zadd` command return value should represent.
///
/// `zadd` normally returns the number of new elements inserted into the set,
/// but also supports the option to return the number of elements changed as a result of the command.
///
/// "Changed" in this context refers to both new elements that were inserted and existing elements that had their score updated.
///
/// See [ZADD Options](https://redis.io/commands/zadd#zadd-options)
public struct RedisZaddReturnBehavior {
    /// Count both new elements that were inserted into the SortedSet and existing elements that had their score updated.
    public static let changedElementsCount = RedisZaddReturnBehavior(.ch)
    /// Count only new elements that were inserted into the SortedSet.
    public static let insertedElementsCount = RedisZaddReturnBehavior(nil)

    @usableFromInline
    internal var string: String? { self.option?.rawValue }

    /// Redis representation of this option.
    private enum Option: String {
        case ch = "CH"
    }

    private let option: Option?
    private init(_ option: Option?) { self.option = option }
}

/// Represents a range bound for use with the Redis SortedSet commands related to element scores.
///
/// This type conforms to `ExpressibleByFloatLiteral` and `ExpressibleByIntegerLiteral`, which will initialize to an `.inclusive` bound.
///
/// For example:
/// ```swift
/// let literalBound: RedisZScoreBound = 3 // .inclusive(3)
/// let otherLiteralBound: RedisZScoreBound = 3.0 // .inclusive(3)
/// let exclusiveBound = RedisZScoreBound.exclusive(4)
/// ```
public enum RedisZScoreBound:
    CustomStringConvertible,
    ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral
{
    case inclusive(Double)
    case exclusive(Double)
    
    /// The underlying raw score value this bound represents.
    public var rawValue: Double {
        switch self {
        case let .inclusive(v), let .exclusive(v): return v
        }
    }

    public var description: String {
        switch self {
        case let .inclusive(value): return value.description
        case let .exclusive(value): return "(\(value.description)"
        }
    }
    
    public init(floatLiteral value: Double) { self = .inclusive(value) }
    public init(integerLiteral value: Int64) { self = .inclusive(Double(value)) }
}

/// The supported methods for aggregating results from various Sorted Set algorithm commands in Redis.
///
/// See the documentation for each individual command that uses this object for more details.
public struct RedisSortedSetAggregateMethod {
    /// Add the score of all matching elements in the source SortedSets.
    public static let sum = RedisSortedSetAggregateMethod(.sum)
    /// Use the minimum score of the matching elements in the source SortedSets.
    public static let min = RedisSortedSetAggregateMethod(.min)
    /// Use the maximum score of the matching elements in the source SortedSets.
    public static let max = RedisSortedSetAggregateMethod(.max)

    internal var string: String { self.option.rawValue }

    /// Redis representation of this option.
    private enum Option: String {
        case sum = "SUM"
        case min = "MIN"
        case max = "MAX"
    }

    private let option: Option
    private init(_ option: Option) { self.option = option }
}

/// Represents a range bound for use with the Redis SortedSet lexiographical commands to compare values.
///
/// Cases must be explicitly declared, with wrapped values conforming to `CustomStringConvertible`.
///
/// The cases `.negativeInfinity` and `.positiveInfinity` represent the special characters in Redis of `-` and `+` respectively.
/// These are constants for absolute lower and upper value bounds that are always treated as _inclusive_.
///
/// See [Redis' string comparison documentation](https://redis.io/commands/zrangebylex#details-on-strings-comparison).
public enum RedisZLexBound<Value: CustomStringConvertible>: CustomStringConvertible {
    case inclusive(Value)
    case exclusive(Value)
    case positiveInfinity
    case negativeInfinity

    public var description: String {
        switch self {
        case let .inclusive(value): return "[\(value)"
        case let .exclusive(value): return "(\(value)"
        case .positiveInfinity: return "+"
        case .negativeInfinity: return "-"
        }
    }
}

extension RedisZLexBound where Value: BinaryFloatingPoint {
    public var description: String {
        switch self {
        case .inclusive(.infinity), .exclusive(.infinity), .positiveInfinity: return "+"
        case .inclusive(-.infinity), .exclusive(-.infinity), .negativeInfinity: return "-"
        case let .inclusive(value): return "[\(value)"
        case let .exclusive(value): return "(\(value)"
        }
    }
}

/// A representation of the range of options for the results that can be returned from Sorted Set range operations.
///
/// This correlates to the `WITHSCORES` option in Redis terminology.
public struct RedisZRangeResultOption<ResultType> {
    /// Returns the scores in addition to the values of elements in a Sorted Set.
    public static var valuesAndScores: RedisZRangeResultOption<[(RESPValue, Double)]> {
        return .init(true) {
            return try RedisCommand<Void>._mapSortedSetResponse($0, scoreIsFirst: false)
        }
    }
    /// Returns only the values of elements in a Sorted Set.
    public static var valuesOnly: RedisZRangeResultOption<[RESPValue]> {
        return .init(false, { $0 })
    }
    
    fileprivate let includeScores: Bool
    fileprivate let transform: ([RESPValue]) throws -> ResultType
    private init(_ includeScores: Bool, _ transform: @escaping ([RESPValue]) throws -> ResultType) {
        self.includeScores = includeScores
        self.transform = transform
    }
}

// MARK: - Shared implementations
extension RedisCommand {
    fileprivate static func _bzpop<ResultType>(
        keyword: String,
        _ keys: [RedisKey],
        _ timeout: TimeAmount,
        _ transform: @escaping ((String, Double, RESPValue)?) throws -> ResultType?
    ) -> RedisCommand<ResultType?> {
        var args = keys.map(RESPValue.init(from:))
        args.append(.init(bulk: timeout.seconds))
        return .init(keyword: keyword, arguments: args) {
            guard !$0.isNull else { return nil }

            let response = try $0.map(to: [RESPValue].self)
            assert(response.count == 3, "unexpected response size returned")
            guard
                let key = response[0].string,
                let score = Double(fromRESP: response[1])
            else {
                throw RedisClientError.assertionFailure(message: "unexpected structure in response: \(response)")
            }

            return try transform((key, score, response[2]))
        }
    }

    fileprivate static func _zstore(
        keyword: String,
        _ sources: [RedisKey],
        _ destination: RedisKey,
        _ weights: [Int]?,
        _ aggregate: RedisSortedSetAggregateMethod?
    ) -> RedisCommand<Int> {
        assert(sources.count > 0, "at least 1 source key should be provided")

        var args: [RESPValue] = [
            .init(from: destination),
            .init(bulk: sources.count)
        ]
        args.append(convertingContentsOf: sources)

        if let w = weights {
            assert(w.count > 0, "when passing a value for 'weights', at least 1 value should be provided")
            assert(w.count <= sources.count, "weights should be no larger than the amount of source keys")

            args.append(.init(bulk: "WEIGHTS"))
            args.append(convertingContentsOf: w)
        }
        if let a = aggregate {
            args.append(.init(bulk: "AGGREGATE"))
            args.append(.init(bulk: a.string))
        }

        return .init(keyword: keyword, arguments: args)
    }

    fileprivate static func _zpop<ResultType>(
        keyword: String,
        _ count: Int?,
        _ key: RedisKey,
        _ transform: @escaping ([(RESPValue, Double)]) -> ResultType
    ) -> RedisCommand<ResultType> {
        var args = [RESPValue(from: key)]

        if let c = count { args.append(.init(bulk: c)) }

        return .init(keyword: keyword, arguments: args) {
            let response = try $0.map(to: [RESPValue].self)
            let result = try Self._mapSortedSetResponse(response, scoreIsFirst: true)
            return transform(result)
        }
    }

    @usableFromInline
    internal static func _zrange<T>(
        keyword: String,
        _ key: RedisKey,
        _ start: Int,
        _ stop: Int,
        _ resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        var args: [RESPValue] = [
            .init(from: key),
            .init(bulk: start),
            .init(bulk: stop)
        ]
        if resultOption.includeScores { args.append(.init(bulk: "WITHSCORES")) }
        return .init(keyword: keyword, arguments: args) {
            let response = try $0.map(to: [RESPValue].self)
            return try resultOption.transform(response)
        }
    }

    @usableFromInline
    internal static func _zrangebylex(
        keyword: String,
        _ key: RedisKey,
        _ range: (min: String, max: String),
        _ limit: (offset: Int, count: Int)?
    ) -> RedisCommand<[RESPValue]> {
        var args: [RESPValue] = [
            .init(from: key),
            .init(bulk: range.min),
            .init(bulk: range.max)
        ]
        if let l = limit {
            args.append(.init(bulk: "LIMIT"))
            args.append(.init(bulk: l.offset))
            args.append(.init(bulk: l.count))
        }
        return .init(keyword: keyword, arguments: args)
    }

    @usableFromInline
    internal static func _zrangebyscore<T>(
        keyword: String,
        _ key: RedisKey,
        _ range: (min: String, max: String),
        _ limit: (offset: Int, count: Int)?,
        _ resultOption: RedisZRangeResultOption<T>
    ) -> RedisCommand<T> {
        var args: [RESPValue] = [
            .init(from: key),
            .init(bulk: range.min),
            .init(bulk: range.max)
        ]
        if resultOption.includeScores { args.append(.init(bulk: "WITHSCORES")) }
        if let l = limit {
            args.append(.init(bulk: "LIMIT"))
            args.append(.init(bulk: l.offset))
            args.append(.init(bulk: l.count))
        }
        return .init(keyword: keyword, arguments: args) {
            let response = try $0.map(to: [RESPValue].self)
            return try resultOption.transform(response)
        }
    }

    fileprivate static func _mapSortedSetResponse(_ response: [RESPValue], scoreIsFirst: Bool) throws -> [(RESPValue, Double)] {
        let responseCount = response.count
        guard responseCount > 0 else { return [] }

        var result: [(RESPValue, Double)] = []
        result.reserveCapacity(responseCount / 2) // every other RESPValue is the count
        
        var index = 0
        repeat {
            let scoreItem = response[scoreIsFirst ? index : index + 1]

            guard let score = Double(fromRESP: scoreItem) else {
                throw RedisClientError.assertionFailure(message: "unexpected response: '\(scoreItem)'")
            }

            let elementIndex = scoreIsFirst ? index + 1 : index
            result.append((response[elementIndex], score))

            index += 2
        } while (index < responseCount)

        return result
    }
}