diff --git a/Sources/RediStack/Commands/ListCommands.swift b/Sources/RediStack/Commands/ListCommands.swift index 61926a7..0d28921 100644 --- a/Sources/RediStack/Commands/ListCommands.swift +++ b/Sources/RediStack/Commands/ListCommands.swift @@ -90,15 +90,19 @@ extension RedisClient { return send(command: "LREM", with: args) .convertFromRESPValue() } +} - /// Trims a list to only contain elements within the specified inclusive bounds of 0-based indices. +// MARK: LTrim + +extension RedisClient { + /// Trims a List to only contain elements within the specified inclusive bounds of 0-based indices. /// /// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim) /// - Parameters: - /// - key: The key of the list to trim. + /// - key: The key of the List to trim. /// - start: The index of the first element to keep. /// - stop: The index of the last element to keep. - /// - Returns: An `EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`. + /// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`. @inlinable public func ltrim(_ key: RedisKey, before start: Int, after stop: Int) -> EventLoopFuture { let args: [RESPValue] = [ @@ -109,28 +113,295 @@ extension RedisClient { return send(command: "LTRIM", with: args) .map { _ in () } } + + /// Trims a List to only contain elements within the specified inclusive bounds of 0-based indices. + /// + /// To keep elements 4 through 7: + /// ```swift + /// client.ltrim("myList", keepingIndices: 3...6) + /// ``` + /// + /// To keep the last 4 through 7 elements: + /// ```swift + /// client.ltrim("myList", keepingIndices: (-7)...(-4)) + /// ``` + /// + /// To keep the first and last 4 elements: + /// ```swift + /// client.ltrim("myList", keepingIndices: (-4)...3) + /// ``` + /// + /// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim) + /// - Warning: 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 `ltrim(_:before:after:)` instead. + /// - Parameters: + /// - key: The key of the List to trim. + /// - range: The range of indices that should be kept in the List. + /// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`. + @inlinable + public func ltrim(_ key: RedisKey, keepingIndices range: ClosedRange) -> EventLoopFuture { + return self.ltrim(key, before: range.lowerBound, after: range.upperBound) + } - /// Gets all elements from a list within the the specified inclusive bounds of 0-based indices. + /// Trims a List to only contain elements starting from the specified index. + /// + /// To keep all but the first 3 elements: + /// ```swift + /// client.ltrim("myList", keepingIndices: 3...) + /// ``` + /// + /// To keep the last 4 elements: + /// ```swift + /// client.ltrim("myList", keepingIndices: (-4)...) + /// ``` + /// + /// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim) + /// - Parameters: + /// - key: The key of the List to trim. + /// - range: The range of indices that should be kept in the List. + /// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`. + @inlinable + public func ltrim(_ key: RedisKey, keepingIndices range: PartialRangeFrom) -> EventLoopFuture { + return self.ltrim(key, before: range.lowerBound, after: -1) + } + + /// Trims a List to only contain elements before the specified index. + /// + /// To keep the first 3 elements: + /// ```swift + /// client.ltrim("myList", keepingIndices: ..<3) + /// ``` + /// + /// To keep all but the last 4 elements: + /// ```swift + /// client.ltrim("myList", keepingIndices: ..<(-4)) + /// ``` + /// + /// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim) + /// - Parameters: + /// - key: The key of the List to trim. + /// - range: The range of indices that should be kept in the List. + /// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`. + @inlinable + public func ltrim(_ key: RedisKey, keepingIndices range: PartialRangeUpTo) -> EventLoopFuture { + return self.ltrim(key, before: 0, after: range.upperBound - 1) + } + + /// Trims a List to only contain elements up to the specified index. + /// + /// To keep the first 4 elements: + /// ```swift + /// client.ltrim("myList", keepingIndices: ...3) + /// ``` + /// + /// To keep all but the last 3 elements: + /// ```swift + /// client.ltrim("myList", keepingIndices: ...(-4)) + /// ``` + /// + /// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim) + /// - Parameters: + /// - key: The key of the List to trim. + /// - range: The range of indices that should be kept in the List. + /// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`. + @inlinable + public func ltrim(_ key: RedisKey, keepingIndices range: PartialRangeThrough) -> EventLoopFuture { + return self.ltrim(key, before: 0, after: range.upperBound) + } + + /// Trims a List to only contain the elements from the specified index up to the index provided. + /// + /// To keep the first 4 elements: + /// ```swift + /// client.ltrim("myList", keepingIndices: 0..<4) + /// ``` + /// + /// To keep all but the last 3 elements: + /// ```swift + /// client.ltrim("myList", keepingIndices: 0..<(-3)) + /// ``` + /// + /// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim) + /// - Warning: 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 `ltrim(_:before:after:)` instead. + /// - Parameters: + /// - key: The key of the List to trim. + /// - range: The range of indices that should be kept in the List. + /// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`. + @inlinable + public func ltrim(_ key: RedisKey, keepingIndices range: Range) -> EventLoopFuture { + return self.ltrim(key, before: range.lowerBound, after: range.upperBound - 1) + } +} + +// MARK: LRange + +extension RedisClient { + /// Gets all elements from a List within the the specified inclusive bounds of 0-based indices. /// /// See [https://redis.io/commands/lrange](https://redis.io/commands/lrange) /// - Parameters: - /// - range: The range of inclusive indices of elements to get. - /// - key: The key of the list. - /// - Returns: A list of elements found within the range specified. + /// - key: The key of the List. + /// - 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. + /// - Returns: An array of elements found within the range specified. @inlinable - public func lrange( - within range: (startIndex: Int, endIndex: Int), - from key: RedisKey - ) -> EventLoopFuture<[RESPValue]> { + public func lrange(from key: RedisKey, firstIndex: Int, lastIndex: Int) -> EventLoopFuture<[RESPValue]> { let args: [RESPValue] = [ .init(bulk: key), - .init(bulk: range.startIndex), - .init(bulk: range.endIndex) + .init(bulk: firstIndex), + .init(bulk: lastIndex) ] return send(command: "LRANGE", with: args) .convertFromRESPValue() } + + /// Gets all elements from a List within the specified inclusive bounds of 0-based indices. + /// + /// To get the elements at index 4 through 7: + /// ```swift + /// client.lrange(from: "myList", indices: 4...7) + /// ``` + /// + /// To get the last 4 elements: + /// ```swift + /// client.lrange(from: "myList", indices: (-4)...(-1)) + /// ``` + /// + /// To get the first and last 4 elements: + /// ```swift + /// client.lrange(from: "myList", indices: (-4)...3) + /// ``` + /// + /// To get the first element, and the last 4: + /// ```swift + /// client.lrange(from: "myList", indices: (-4)...0)) + /// ``` + /// + /// See [https://redis.io/commands/lrange](https://redis.io/commands/lrange) + /// - Warning: 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 `lrange(from:firstIndex:lastIndex:)` instead. + /// - Parameters: + /// - key: The key of the List to return elements from. + /// - range: The range of inclusive indices of elements to get. + /// - Returns: An array of elements found within the range specified. + @inlinable + public func lrange(from key: RedisKey, indices range: ClosedRange) -> EventLoopFuture<[RESPValue]> { + return self.lrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound) + } + + /// Gets all the elements from a List starting with the first index bound up to, but not including, the element at the last index bound. + /// + /// To get the elements at index 4 through 7: + /// ```swift + /// client.lrange(from: "myList", indices: 4..<8) + /// ``` + /// + /// To get the last 4 elements: + /// ```swift + /// client.lrange(from: "myList", indices: (-4)..<0) + /// ``` + /// + /// To get the first and last 4 elements: + /// ```swift + /// client.lrange(from: "myList", indices: (-4)..<4) + /// ``` + /// + /// To get the first element, and the last 4: + /// ```swift + /// client.lrange(from: "myList", indices: (-4)..<1) + /// ``` + /// + /// See [https://redis.io/commands/lrange](https://redis.io/commands/lrange) + /// - Warning: 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 `lrange(from:firstIndex:lastIndex:)` instead. + /// - Parameters: + /// - key: The key of the List to return elements from. + /// - range: The range of indices (inclusive lower, exclusive upper) elements to get. + /// - Returns: An array of elements found within the range specified. + @inlinable + public func lrange(from key: RedisKey, indices range: Range) -> EventLoopFuture<[RESPValue]> { + return self.lrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound - 1) + } + /// Gets all elements from the index specified to the end of a List. + /// + /// To get all except the first 2 elements of a List: + /// ```swift + /// client.lrange(from: "myList", fromIndex: 2) + /// ``` + /// + /// To get the last 4 elements of a List: + /// ```swift + /// client.lrange(from: "myList", fromIndex: -4) + /// ``` + /// + /// See `lrange(from:indices:)`, `lrange(from:firstIndex:lastIndex:)`, and [https://redis.io/commands/lrange](https://redis.io/commands/lrange) + /// - Parameters: + /// - key: The key of the List to return elements from. + /// - index: The index of the first element that will be in the returned values. + /// - Returns: An array of elements from the List between the index and the end. + @inlinable + public func lrange(from key: RedisKey, fromIndex index: Int) -> EventLoopFuture<[RESPValue]> { + return self.lrange(from: key, firstIndex: index, lastIndex: -1) + } + + /// Gets all elements from the the start of a List up to, and including, the element at the index specified. + /// + /// To get the first 3 elements of a List: + /// ```swift + /// client.lrange(from: "myList", throughIndex: 2) + /// ``` + /// + /// To get all except the last 3 elements of a List: + /// ```swift + /// client.lrange(from: "myList", throughIndex: -4) + /// ``` + /// + /// See `lrange(from:indices:)`, `lrange(from:firstIndex:lastIndex:)`, and [https://redis.io/commands/lrange](https://redis.io/commands/lrange) + /// - Parameters: + /// - key: The key of the List to return elements from. + /// - index: The index of the last element that will be in the returned values. + /// - Returns: An array of elements from the start of a List to the index. + @inlinable + public func lrange(from key: RedisKey, throughIndex index: Int) -> EventLoopFuture<[RESPValue]> { + return self.lrange(from: key, firstIndex: 0, lastIndex: index) + } + + /// Gets all elements from the the start of a List up to, but not including, the element at the index specified. + /// + /// To get the first 3 elements of a List: + /// ```swift + /// client.lrange(from: "myList", upToIndex: 3) + /// ``` + /// + /// To get all except the last 3 elements of a List: + /// ```swift + /// client.lrange(from: "myList", upToIndex: -3) + /// ``` + /// + /// See `lrange(from:indices:)`, `lrange(from:firstIndex:lastIndex:)`, and [https://redis.io/commands/lrange](https://redis.io/commands/lrange) + /// - Parameters: + /// - key: The key of the List to return elements from. + /// - index: The index of the element to not include in the returned values. + /// - Returns: An array of elements from the start of the List and up to the index. + @inlinable + public func lrange(from key: RedisKey, upToIndex index: Int) -> EventLoopFuture<[RESPValue]> { + return self.lrange(from: key, firstIndex: 0, lastIndex: index - 1) + } +} + +// MARK: Pop & Push + +extension RedisClient { /// Pops the last element from a source list and pushes it to a destination list. /// /// See [https://redis.io/commands/rpoplpush](https://redis.io/commands/rpoplpush) diff --git a/Sources/RediStack/Commands/SortedSetCommands.swift b/Sources/RediStack/Commands/SortedSetCommands.swift index eb646b2..77b087d 100644 --- a/Sources/RediStack/Commands/SortedSetCommands.swift +++ b/Sources/RediStack/Commands/SortedSetCommands.swift @@ -267,49 +267,268 @@ extension RedisClient { // MARK: Count +/// 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 { + 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 + } + } +} + +extension RedisZScoreBound: CustomStringConvertible { + public var description: String { + switch self { + case let .inclusive(value): return value.description + case let .exclusive(value): return "(\(value.description)" + } + } +} + +extension RedisZScoreBound: ExpressibleByFloatLiteral { + public typealias FloatLiteralType = Double + + public init(floatLiteral value: Double) { + self = .inclusive(value) + } +} + +extension RedisZScoreBound: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = Int64 + + public init(integerLiteral value: Int64) { + self = .inclusive(Double(value)) + } +} + extension RedisClient { - /// Returns the number of elements in a sorted set with a score within the range specified. + /// Returns the count of elements in a SortedSet with a score within the range specified (inclusive by default). /// - /// See [https://redis.io/commands/zcount](https://redis.io/commands/zcount) + /// To get a count of elements that have at least the score of 3, but no greater than 10: + /// ```swift + /// client.zcount(of: "mySortedSet", withScoresBetween: (3, 10)) + /// ``` + /// + /// To get a count of elements that have at least the score of 3, but less than 10: + /// ```swift + /// client.zcount(of: "mySortedSet", withScoresBetween: (3, .exclusive(10))) + /// ``` + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/zcount](https://redis.io/commands/zcount) /// - Parameters: - /// - key: The key of the sorted set to count. - /// - range: The min and max range of scores to filter for. - /// - Returns: The number of elements in the sorted set that fit within the score range. + /// - 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. + /// - Returns: The count of elements in the SortedSet with a score matching the range specified. @inlinable public func zcount( of key: RedisKey, - within range: (min: String, max: String) + withScoresBetween range: (min: RedisZScoreBound, max: RedisZScoreBound) ) -> EventLoopFuture { + guard range.min.rawValue <= range.max.rawValue else { return self.eventLoop.makeSucceededFuture(0) } let args: [RESPValue] = [ .init(bulk: key), - .init(bulk: range.min), - .init(bulk: range.max) + .init(bulk: range.min.description), + .init(bulk: range.max.description) ] - return send(command: "ZCOUNT", with: args) + return self.send(command: "ZCOUNT", with: args) .convertFromRESPValue() } - - /// Returns the number of elements in a sorted set whose lexiographical values are between the range specified. - /// - Important: This assumes all elements in the sorted set have the same score. If not, the returned elements are unspecified. + + /// Returns the count of elements in a SortedSet with a score within the inclusive range specified. /// - /// See [https://redis.io/commands/zlexcount](https://redis.io/commands/zlexcount) + /// To get a count of elements that have at least the score of 3, but no greater than 10: + /// ```swift + /// client.zcount(of: "mySortedSet", withScores: 3...10) + /// ``` + /// + /// See [https://redis.io/commands/zcount](https://redis.io/commands/zcount) /// - Parameters: - /// - key: The key of the sorted set to count. - /// - range: The min and max range of values to filter for. - /// - Returns: The number of elements in the sorted set that fit within the value range. + /// - key: The key of the SortedSet that will be counted. + /// - range: The inclusive range of scores to filter elements to count. + /// - Returns: The count of elements in the SortedSet with a score within the range specified. @inlinable - public func zlexcount( + public func zcount(of key: RedisKey, withScores range: ClosedRange) -> EventLoopFuture { + return self.zcount(of: key, withScoresBetween: (.inclusive(range.lowerBound), .inclusive(range.upperBound))) + } + + /// Returns the count of elements in a SortedSet with a minimum score up to, but not including, a max score. + /// + /// To get a count of elements that have at least the score of 3, but less than 10: + /// ```swift + /// client.zcount(of: "mySortedSet", withScores: 3..<10) + /// ``` + /// + /// See [https://redis.io/commands/zcount](https://redis.io/commands/zcount) + /// - 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. + /// - Returns: The count of elements in the SortedSet with a score within the range specified. + @inlinable + public func zcount(of key: RedisKey, withScores range: Range) -> EventLoopFuture { + return self.zcount(of: key, withScoresBetween: (.inclusive(range.lowerBound), .exclusive(range.upperBound))) + } + + /// Returns the count of elements in a SortedSet whose score is greater than a minimum score value. + /// + /// By default, the value provided will be treated as _inclusive_, meaning any element that has a score matching the value **will** be counted. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/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. + /// - Returns: The count of elements in the SortedSet above the `minScore` threshold. + @inlinable + public func zcount(of key: RedisKey, withMinimumScoreOf minScore: RedisZScoreBound) -> EventLoopFuture { + return self.zcount(of: key, withScoresBetween: (minScore, .inclusive(.infinity))) + } + + /// Returns the count of elements in a SortedSet whose score is less than a maximum score value. + /// + /// By default, the value provided will be treated as _inclusive_, meaning any element that has a score matching the value **will** be counted. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/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. + /// - exclusive: Should the `maxScore` provided be exclusive? If `true`, scores matching the `maxScore` will **not** be counted. + /// - Returns: The count of elements in the SortedSet below the `maxScore` threshold. + @inlinable + public func zcount(of key: RedisKey, withMaximumScoreOf maxScore: RedisZScoreBound) -> EventLoopFuture { + return self.zcount(of: key, withScoresBetween: (.inclusive(-.infinity), maxScore)) + } +} + +// MARK: Lexiographical Count + +/// 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 [https://redis.io/commands/zrangebylex#details-on-strings-comparison](https://redis.io/commands/zrangebylex#details-on-strings-comparison) +public enum RedisZLexBound { + case inclusive(Value) + case exclusive(Value) + case positiveInfinity + case negativeInfinity +} + +extension RedisZLexBound: CustomStringConvertible { + 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)" + } + } +} + +extension RedisClient { + /// Returns the count of elements in a SortedSet whose lexiographical values are between the range specified. + /// + /// For example: + /// ```swift + /// // "mySortedSet" contains the values [1, 2, 3, 10] each with a score of 1. + /// client.zlexcount(of: "mySortedSet", withValuesBetween: (.inclusive(1), .inclusive(3))) + /// // the response will resolve to 4, as both 10 and 1 have the value "1" + /// ``` + /// + /// See `RedisZLexBound` and [https://redis.io/commands/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. + /// - range: The min and max value bounds that an element should have in order to be counted. + /// - Returns: The count of elements in the SortedSet with values matching the range specified. + @inlinable + public func zlexcount( of key: RedisKey, - within range: (min: String, max: String) + withValuesBetween range: (min: RedisZLexBound, max: RedisZLexBound) ) -> EventLoopFuture { let args: [RESPValue] = [ .init(bulk: key), - .init(bulk: range.min), - .init(bulk: range.max) + .init(bulk: range.min.description), + .init(bulk: range.max.description) ] - return send(command: "ZLEXCOUNT", with: args) + return self.send(command: "ZLEXCOUNT", with: args) .convertFromRESPValue() } + + /// Returns the count of elements in a SortedSet whose lexiographical value is greater than a minimum value. + /// + /// For example with a SortedSet that contains the values [1, 2, 3, 10] and each a score of 1: + /// ```swift + /// client.zlexcount(of: "mySortedSet", withMinimumValueOf: .inclusive(2)) + /// // the response will resolve to 2, as "10" lexiographically comes before element "2" + /// + /// client.zlexcount(of: "mySortedSet", withMinimumValueOf: .inclusive(10)) + /// // the response will resolve to 3, as the set is ordered as ["1", "10", "2", "3"] + /// ``` + /// + /// See `RedisZLexBound` and [https://redis.io/commands/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. + /// - Returns: The count of elements in the SortedSet above the `minValue` threshold. + @inlinable + public func zlexcount( + of key: RedisKey, + withMinimumValueOf minValue: RedisZLexBound + ) -> EventLoopFuture { + return self.zlexcount(of: key, withValuesBetween: (minValue, .positiveInfinity)) + } + + /// Returns the count of elements in a SortedSet whose lexiographical value is less than a maximum value. + /// + /// For example with a SortedSet that contains the values [1, 2, 3, 10] and each a score of 1: + /// ```swift + /// client.zlexcount(of: "mySortedSet", withMaximumValueOf: .exclusive(10)) + /// // the response will resolve to 1, as "1" and "10" are sorted into the first 2 elements + /// + /// client.zlexcount(of: "mySortedSet", withMaximumValueOf: .inclusive(3)) + /// // the response will resolve to 4, as the set is ordered as ["1", "10", "2", "3"] + /// ``` + /// + /// See `RedisZLexBound` and [https://redis.io/commands/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. + /// - Returns: The count of elements in the SortedSet below the `maxValue` threshold. + @inlinable + public func zlexcount( + of key: RedisKey, + withMaximumValueOf maxValue: RedisZLexBound + ) -> EventLoopFuture { + return self.zlexcount(of: key, withValuesBetween: (.negativeInfinity, maxValue)) + } } // MARK: Pop @@ -647,44 +866,392 @@ extension RedisClient { // MARK: Range extension RedisClient { - /// Gets the specified range of elements in a sorted set. - /// - Note: This treats the ordered set as ordered from low to high. - /// - /// For the inverse, see `zrevrange(within:from:withScores:)`. + /// Gets all elements from a SortedSet within the specified inclusive bounds of 0-based indices. /// /// See [https://redis.io/commands/zrange](https://redis.io/commands/zrange) + /// - Important: This treats the SortedSet as ordered from **low** to **high**. + /// + /// For the inverse, see `zrevrange(from:firstIndex:lastIndex:includeScoresInResponse:)`. /// - Parameters: - /// - range: The start and stop 0-based indices of the range of elements to include. - /// - key: The key of the sorted set to search. - /// - withScores: Should the list contain the elements AND their scores? [Item_1, Score_1, Item_2, ...] - /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. + /// - 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. + /// - Returns: An array of elements found within the range specified. @inlinable public func zrange( - within range: (start: Int, stop: Int), from key: RedisKey, - withScores: Bool = false + firstIndex: Int, + lastIndex: Int, + includeScoresInResponse includeScores: Bool = false ) -> EventLoopFuture<[RESPValue]> { - return _zrange(command: "ZRANGE", key, range.start, range.stop, withScores) + return self._zrange(command: "ZRANGE", key, firstIndex, lastIndex, includeScores) } - - /// Gets the specified range of elements in a sorted set. - /// - Note: This treats the ordered set as ordered from high to low. + + /// Gets all elements from a SortedSet within the specified inclusive bounds of 0-based indices. /// - /// For the inverse, see `zrange(within:from:withScores:)`. + /// To get the elements at index 4 through 7: + /// ```swift + /// client.zrange(from: "mySortedSet", indices: 4...7) + /// ``` + /// + /// To get the last 4 elements: + /// ```swift + /// client.zrange(from: "mySortedSet", indices: (-4)...(-1)) + /// ``` + /// + /// To get the first and last 4 elements: + /// ```swift + /// client.zrange(from: "mySortedSet", indices: (-4)...3) + /// ``` + /// + /// To get the first element, and the last 4: + /// ```swift + /// client.zrange(from: "mySortedSet", indices: (-4)...0)) + /// ``` + /// + /// See [https://redis.io/commands/zrange](https://redis.io/commands/zrange) + /// - Warning: 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:)` instead. + /// - Important: This treats the SortedSet as ordered from **low** to **high**. + /// + /// For the inverse, see `zrevrange(from:indices:includeScoresInResponse:)`. + /// - Parameters: + /// - key: The key of the SortedSet to return elements from. + /// - range: The range of inclusive indices of elements to get. + /// - Returns: An array of elements found within the range specified. + @inlinable + public func zrange( + from key: RedisKey, + indices range: ClosedRange, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound, includeScoresInResponse: includeScores) + } + + /// Gets all the elements from a SortedSet starting with the first index bound up to, but not including, the element at the last index bound. + /// + /// To get the elements at index 4 through 7: + /// ```swift + /// client.zrange(from: "mySortedSet", indices: 4..<8) + /// ``` + /// + /// To get the last 4 elements: + /// ```swift + /// client.zrange(from: "mySortedSet", indices: (-4)..<0) + /// ``` + /// + /// To get the first and last 4 elements: + /// ```swift + /// client.zrange(from: "mySortedSet", indices: (-4)..<4) + /// ``` + /// + /// To get the first element, and the last 4: + /// ```swift + /// client.zrange(from: "mySortedSet", indices: (-4)..<1) + /// ``` + /// + /// See [https://redis.io/commands/zrange](https://redis.io/commands/zrange) + /// - Warning: 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:includeScoresInResponse:)`. + /// - Parameters: + /// - key: The key of the SortedSet to return elements from. + /// - range: The range of indices (inclusive lower, exclusive upper) elements to get. + /// - Returns: An array of elements found within the range specified. + @inlinable + public func zrange( + from key: RedisKey, + indices range: Range, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound - 1, includeScoresInResponse: includeScores) + } + + /// Gets all elements from the index specified to the end of a SortedSet. + /// + /// To get all except the first 2 elements of a SortedSet: + /// ```swift + /// client.zrange(from: "mySortedSet", fromIndex: 2) + /// ``` + /// + /// To get the last 4 elements of a SortedSet: + /// ```swift + /// client.zrange(from: "mySortedSet", fromIndex: -4) + /// ``` + /// + /// See `zrange(from:indices:)`, `zrange(from:firstIndex:lastIndex:)`, and [https://redis.io/commands/zrange](https://redis.io/commands/zrange) + /// - Important: This treats the SortedSet as ordered from **low** to **high**. + /// + /// For the inverse, see `zrevrange(from:fromIndex:includeScoresInResponse:)`. + /// - 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. + /// - Returns: An array of elements from the SortedSet between the index and the end. + @inlinable + public func zrange( + from key: RedisKey, + fromIndex index: Int, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrange(from: key, firstIndex: index, lastIndex: -1, includeScoresInResponse: includeScores) + } + + /// Gets all elements from the start of a SortedSet up to, and including, the element at the index specified. + /// + /// To get the first 3 elements of a SortedSet: + /// ```swift + /// client.zrange(from: "mySortedSet", throughIndex: 2) + /// ``` + /// + /// To get all except the last 3 elements of a SortedSet: + /// ```swift + /// client.zrange(from: "mySortedSet", throughIndex: -4) + /// ``` + /// + /// See `zrange(from:indices:)`, `zrange(from:firstIndex:lastIndex:)`, and [https://redis.io/commands/zrange](https://redis.io/commands/zrange) + /// - Important: This treats the SortedSet as ordered from **low** to **high**. + /// + /// For the inverse, see `zrevrange(from:throughIndex:includeScoresInResponse:)`. + /// - 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. + /// - Returns: An array of elements from the start of a SortedSet to the index. + @inlinable + public func zrange( + from key: RedisKey, + throughIndex index: Int, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrange(from: key, firstIndex: 0, lastIndex: index, includeScoresInResponse: includeScores) + } + + /// Gets all elements from the start of a SortedSet up to, but not including, the element at the index specified. + /// + /// To get the first 3 elements of a List: + /// ```swift + /// client.zrange(from: "myList", upToIndex: 3) + /// ``` + /// + /// To get all except the last 3 elements of a List: + /// ```swift + /// client.zrange(from: "myList", upToIndex: -3) + /// ``` + /// + /// See `zrange(from:indices:)`, `zrange(from:upToIndex:lastIndex:)`, and [https://redis.io/commands/zrange](https://redis.io/commands/zrange) + /// - Important: This treats the SortedSet as ordered from **low** to **high**. + /// + /// For the inverse, see `zrevrange(from:upToIndex:includeScoresInResponse:)`. + /// - 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. + /// - Returns: An array of elements from the start of the SortedSet and up to the index. + @inlinable + public func zrange( + from key: RedisKey, + upToIndex index: Int, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrange(from: key, firstIndex: 0, lastIndex: index - 1, includeScoresInResponse: includeScores) + } + + /// Gets all elements from a SortedSet within the specified inclusive bounds of 0-based indices. /// /// See [https://redis.io/commands/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:includeScoresInResponse:)`. /// - Parameters: - /// - range: The start and stop 0-based indices of the range of elements to include. - /// - key: The key of the sorted set to search. - /// - withScores: Should the list contain the elements AND their scores? [Item_1, Score_1, Item_2, ...] - /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. + /// - 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. + /// - Returns: An array of elements found within the range specified. @inlinable public func zrevrange( - within range: (start: Int, stop: Int), from key: RedisKey, - withScores: Bool = false + firstIndex: Int, + lastIndex: Int, + includeScoresInResponse includeScores: Bool = false ) -> EventLoopFuture<[RESPValue]> { - return _zrange(command: "ZREVRANGE", key, range.start, range.stop, withScores) + return self._zrange(command: "ZREVRANGE", key, firstIndex, lastIndex, includeScores) + } + + /// Gets all elements from a SortedSet within the specified inclusive bounds of 0-based indices. + /// + /// To get the elements at index 4 through 7: + /// ```swift + /// client.zrevrange(from: "mySortedSet", indices: 4...7) + /// ``` + /// + /// To get the last 4 elements: + /// ```swift + /// client.zrevrange(from: "mySortedSet", indices: (-4)...(-1)) + /// ``` + /// + /// To get the first and last 4 elements: + /// ```swift + /// client.zrevrange(from: "mySortedSet", indices: (-4)...3) + /// ``` + /// + /// To get the first element, and the last 4: + /// ```swift + /// client.zrevrange(from: "mySortedSet", indices: (-4)...0)) + /// ``` + /// + /// See [https://redis.io/commands/zrevrange](https://redis.io/commands/zrevrange) + /// - Warning: 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:includeScoresInResponse:)`. + /// - Parameters: + /// - key: The key of the SortedSet to return elements from. + /// - range: The range of inclusive indices of elements to get. + /// - Returns: An array of elements found within the range specified. + @inlinable + public func zrevrange( + from key: RedisKey, + indices range: ClosedRange, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound, includeScoresInResponse: includeScores) + } + + /// Gets all the elements from a SortedSet starting with the first index bound up to, but not including, the element at the last index bound. + /// + /// To get the elements at index 4 through 7: + /// ```swift + /// client.zrevrange(from: "mySortedSet", indices: 4..<8) + /// ``` + /// + /// To get the last 4 elements: + /// ```swift + /// client.zrevrange(from: "mySortedSet", indices: (-4)..<0) + /// ``` + /// + /// To get the first and last 4 elements: + /// ```swift + /// client.zrevrange(from: "mySortedSet", indices: (-4)..<4) + /// ``` + /// + /// To get the first element, and the last 4: + /// ```swift + /// client.zrevrange(from: "mySortedSet", indices: (-4)..<1) + /// ``` + /// + /// See [https://redis.io/commands/zrevrange](https://redis.io/commands/zrevrange) + /// - Warning: 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:includeScoresInResponse:)`. + /// - Parameters: + /// - key: The key of the SortedSet to return elements from. + /// - range: The range of indices (inclusive lower, exclusive upper) elements to get. + /// - Returns: An array of elements found within the range specified. + @inlinable + public func zrevrange( + from key: RedisKey, + indices range: Range, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound - 1, includeScoresInResponse: includeScores) + } + + /// Gets all elements from the index specified to the end of a SortedSet. + /// + /// To get all except the first 2 elements of a SortedSet: + /// ```swift + /// client.zrevrange(from: "mySortedSet", fromIndex: 2) + /// ``` + /// + /// To get the last 4 elements of a SortedSet: + /// ```swift + /// client.zrevrange(from: "mySortedSet", fromIndex: -4) + /// ``` + /// + /// See `zrevrange(from:indices:)`, `zrevrange(from:firstIndex:lastIndex:)`, and [https://redis.io/commands/zrevrange](https://redis.io/commands/zrevrange) + /// - Important: This treats the SortedSet as ordered from **high** to **low**. + /// + /// For the inverse, see `zrange(from:fromIndex:includeScoresInResponse:)`. + /// - 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. + /// - Returns: An array of elements from the SortedSet between the index and the end. + @inlinable + public func zrevrange( + from key: RedisKey, + fromIndex index: Int, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrange(from: key, firstIndex: index, lastIndex: -1, includeScoresInResponse: includeScores) + } + + /// Gets all elements from the start of a SortedSet up to, and including, the element at the index specified. + /// + /// To get the first 3 elements of a SortedSet: + /// ```swift + /// client.zrevrange(from: "mySortedSet", throughIndex: 2) + /// ``` + /// + /// To get all except the last 3 elements of a SortedSet: + /// ```swift + /// client.zrevrange(from: "mySortedSet", throughIndex: -4) + /// ``` + /// + /// See `zrevrange(from:indices:)`, `zrevrange(from:firstIndex:lastIndex:)`, and [https://redis.io/commands/zrevrange](https://redis.io/commands/zrevrange) + /// - Important: This treats the SortedSet as ordered from **high** to **low**. + /// + /// For the inverse, see `zrange(from:throughIndex:includeScoresInResponse:)`. + /// - 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. + /// - Returns: An array of elements from the start of a SortedSet to the index. + @inlinable + public func zrevrange( + from key: RedisKey, + throughIndex index: Int, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrange(from: key, firstIndex: 0, lastIndex: index, includeScoresInResponse: includeScores) + } + + /// Gets all elements from the start of a SortedSet up to, but not including, the element at the index specified. + /// + /// To get the first 3 elements of a List: + /// ```swift + /// client.zrevrange(from: "myList", upToIndex: 3) + /// ``` + /// + /// To get all except the last 3 elements of a List: + /// ```swift + /// client.zrevrange(from: "myList", upToIndex: -3) + /// ``` + /// + /// See `zrevrange(from:indices:)`, `zrevrange(from:upToIndex:lastIndex:)`, and [https://redis.io/commands/zrevrange](https://redis.io/commands/zrevrange) + /// - Important: This treats the SortedSet as ordered from **high** to **low**. + /// + /// For the inverse, see `zrange(from:upToIndex:includeScoresInResponse:)`. + /// - 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. + /// - Returns: An array of elements from the start of the SortedSet and up to the index. + @inlinable + public func zrevrange( + from key: RedisKey, + upToIndex index: Int, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrange(from: key, firstIndex: 0, lastIndex: index - 1, includeScoresInResponse: includeScores) } @usableFromInline @@ -711,48 +1278,264 @@ extension RedisClient { // MARK: Range by Score extension RedisClient { - /// Gets elements from a sorted set whose score fits within the range specified. - /// - Note: This treats the ordered set as ordered from low to high. + /// Gets all elements from a SortedSet whose score is within the range specified. /// - /// For the inverse, see `zrevrangebyscore(within:from:withScores:limitBy:)`. + /// See `RedisZScoreBound` and [https://redis.io/commands/zrangebyscore](https://redis.io/commands/zrangebyscore) + /// - Important: This treats the SortedSet as ordered from **low** to **high**. /// - /// See [https://redis.io/commands/zrangebyscore](https://redis.io/commands/zrangebyscore) + /// For the inverse, see `zrevrangebyscore(from:withScoresBetween:limitBy:includeScoresInResponse:)`. /// - Parameters: - /// - range: The range of min and max scores to filter elements by. - /// - key: The key of the sorted set to search. - /// - withScores: Should the list contain the elements AND their scores? [Item_1, Score_1, Item_2, ...] + /// - 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. - /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. + /// - includeScores: Should the response array contain the elements AND their scores? If `true`, the response array will follow the pattern [Item_1, Score_1, Item_2, ...] + /// - Returns: An array of elements from the SortedSet that were within the range provided, and optionally their scores. @inlinable public func zrangebyscore( - within range: (min: String, max: String), from key: RedisKey, - withScores: Bool = false, - limitBy limit: (offset: Int, count: Int)? = nil + withScoresBetween range: (min: RedisZScoreBound, max: RedisZScoreBound), + limitBy limit: (offset: Int, count: Int)? = nil, + includeScoresInResponse includeScores: Bool = false ) -> EventLoopFuture<[RESPValue]> { - return _zrangebyscore(command: "ZRANGEBYSCORE", key, range, withScores, limit) + return _zrangebyscore(command: "ZRANGEBYSCORE", key, (range.min.description, range.max.description), includeScores, limit) + } + + /// Gets all elements from a SortedSet whose score is within the inclusive range specified. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/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:includeScoresInResponse:)`. + /// - 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. + /// - includeScores: Should the response array contain the elements AND their scores? If `true`, the response array will follow the pattern [Item_1, Score_1, Item_2, ...] + /// - Returns: An array of elements from the SortedSet that were within the range provided, and optionally their scores. + @inlinable + public func zrangebyscore( + from key: RedisKey, + withScores range: ClosedRange, + limitBy limit: (offset: Int, count: Int)? = nil, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrangebyscore( + from: key, + withScoresBetween: (.inclusive(range.lowerBound), .inclusive(range.upperBound)), + limitBy: limit, + includeScoresInResponse: includeScores + ) + } + + /// Gets all elements from a SortedSet whose score is at least a minimum score up to, but not including, a max score. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/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:includeScoresInResponse:)`. + /// - 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. + /// - includeScores: Should the response array contain the elements AND their scores? If `true`, the response array will follow the pattern [Item_1, Score_1, Item_2, ...] + /// - Returns: An array of elements from the SortedSet that were within the range provided, and optionally their scores. + @inlinable + public func zrangebyscore( + from key: RedisKey, + withScores range: Range, + limitBy limit: (offset: Int, count: Int)? = nil, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrangebyscore( + from: key, + withScoresBetween: (.inclusive(range.lowerBound), .exclusive(range.upperBound)), + limitBy: limit, + includeScoresInResponse: includeScores + ) + } + + /// Gets all elements from a SortedSet whose score is greater than a minimum score value. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/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:includeScoresInResponse:)`. + /// - 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. + /// - includeScores: Should the response array contain the elements AND their scores? If `true`, the response array will follow the pattern [Item_1, Score_1, Item_2, ...] + /// - Returns: An array of elements from the SortedSet that were within the range provided, and optionally their scores. + @inlinable + public func zrangebyscore( + from key: RedisKey, + withMinimumScoreOf minScore: RedisZScoreBound, + limitBy limit: (offset: Int, count: Int)? = nil, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrangebyscore( + from: key, + withScoresBetween: (minScore, .inclusive(.infinity)), + limitBy: limit, + includeScoresInResponse: includeScores + ) } - /// Gets elements from a sorted set whose score fits within the range specified. - /// - Note: This treats the ordered set as ordered from high to low. + /// Gets all elements from a SortedSet whose score is less than a maximum score value. /// - /// For the inverse, see `zrangebyscore(within:from:withScores:limitBy:)`. + /// See `RedisZScoreBound` and [https://redis.io/commands/zrangebyscore](https://redis.io/commands/zrangebyscore) + /// - Important: This treats the SortedSet as ordered from **low** to **high**. /// - /// See [https://redis.io/commands/zrevrangebyscore](https://redis.io/commands/zrevrangebyscore) + /// For the inverse, see `zrevrangebyscore(from:withMaximumScoreOf:limitBy:includeScoresInResponse:)`. /// - Parameters: - /// - range: The range of min and max scores to filter elements by. - /// - key: The key of the sorted set to search. - /// - withScores: Should the list contain the elements AND their scores? [Item_1, Score_1, Item_2, ...] + /// - 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. - /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. + /// - includeScores: Should the response array contain the elements AND their scores? If `true`, the response array will follow the pattern [Item_1, Score_1, Item_2, ...] + /// - Returns: An array of elements from the SortedSet that were within the range provided, and optionally their scores. + @inlinable + public func zrangebyscore( + from key: RedisKey, + withMaximumScoreOf maxScore: RedisZScoreBound, + limitBy limit: (offset: Int, count: Int)? = nil, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrangebyscore( + from: key, + withScoresBetween: (.inclusive(-.infinity), maxScore), + limitBy: limit, + includeScoresInResponse: includeScores + ) + } + + /// Gets all elements from a SortedSet whose score is within the range specified. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/zrangebyscore](https://redis.io/commands/zrangebyscore) + /// - Important: This treats the SortedSet as ordered from **high** to **low**. + /// + /// For the inverse, see `zrangebyscore(from:withScoresBetween:limitBy:includeScoresInResponse:)`. + /// - 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. + /// - includeScores: Should the response array contain the elements AND their scores? If `true`, the response array will follow the pattern [Item_1, Score_1, Item_2, ...] + /// - Returns: An array of elements from the SortedSet that were within the range provided, and optionally their scores. @inlinable public func zrevrangebyscore( - within range: (min: String, max: String), from key: RedisKey, - withScores: Bool = false, - limitBy limit: (offset: Int, count: Int)? = nil + withScoresBetween range: (min: RedisZScoreBound, max: RedisZScoreBound), + limitBy limit: (offset: Int, count: Int)? = nil, + includeScoresInResponse includeScores: Bool = false ) -> EventLoopFuture<[RESPValue]> { - return _zrangebyscore(command: "ZREVRANGEBYSCORE", key, (range.max, range.min), withScores, limit) + return _zrangebyscore(command: "ZREVRANGEBYSCORE", key, (range.max.description, range.min.description), includeScores, limit) + } + + /// Gets all elements from a SortedSet whose score is within the inclusive range specified. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/zrangebyscore](https://redis.io/commands/zrangebyscore) + /// - Important: This treats the SortedSet as ordered from **high** to **low**. + /// + /// For the inverse, see `zrangebyscore(from:withScores:limitBy:includeScoresInResponse:)`. + /// - 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. + /// - includeScores: Should the response array contain the elements AND their scores? If `true`, the response array will follow the pattern [Item_1, Score_1, Item_2, ...] + /// - Returns: An array of elements from the SortedSet that were within the range provided, and optionally their scores. + @inlinable + public func zrevrangebyscore( + from key: RedisKey, + withScores range: ClosedRange, + limitBy limit: (offset: Int, count: Int)? = nil, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrevrangebyscore( + from: key, + withScoresBetween: (.inclusive(range.lowerBound), .inclusive(range.upperBound)), + limitBy: limit, + includeScoresInResponse: includeScores + ) + } + + /// Gets all elements from a SortedSet whose score is at least a minimum score up to, but not including, a max score. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/zrangebyscore](https://redis.io/commands/zrangebyscore) + /// - Important: This treats the SortedSet as ordered from **high** to **low**. + /// + /// For the inverse, see `zrangebyscore(from:withScores:limitBy:includeScoresInResponse:)`. + /// - 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. + /// - includeScores: Should the response array contain the elements AND their scores? If `true`, the response array will follow the pattern [Item_1, Score_1, Item_2, ...] + /// - Returns: An array of elements from the SortedSet that were within the range provided, and optionally their scores. + @inlinable + public func zrevrangebyscore( + from key: RedisKey, + withScores range: Range, + limitBy limit: (offset: Int, count: Int)? = nil, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrevrangebyscore( + from: key, + withScoresBetween: (.inclusive(range.lowerBound), .exclusive(range.upperBound)), + limitBy: limit, + includeScoresInResponse: includeScores + ) + } + + /// Gets all elements from a SortedSet whose score is greater than a minimum score value. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/zrangebyscore](https://redis.io/commands/zrangebyscore) + /// - Important: This treats the SortedSet as ordered from **high** to **low**. + /// + /// For the inverse, see `zrangebyscore(from:withMinimumScoreOf:limitBy:includeScoresInResponse:)`. + /// - 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. + /// - includeScores: Should the response array contain the elements AND their scores? If `true`, the response array will follow the pattern [Item_1, Score_1, Item_2, ...] + /// - Returns: An array of elements from the SortedSet that were within the range provided, and optionally their scores. + @inlinable + public func zrevrangebyscore( + from key: RedisKey, + withMinimumScoreOf minScore: RedisZScoreBound, + limitBy limit: (offset: Int, count: Int)? = nil, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrevrangebyscore( + from: key, + withScoresBetween: (minScore, .inclusive(.infinity)), + limitBy: limit, + includeScoresInResponse: includeScores + ) + } + + /// Gets all elements from a SortedSet whose score is less than a maximum score value. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/zrangebyscore](https://redis.io/commands/zrangebyscore) + /// - Important: This treats the SortedSet as ordered from **high** to **low**. + /// + /// For the inverse, see `zrangebyscore(from:withMaximumScoreOf:limitBy:includeScoresInResponse:)`. + /// - 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. + /// - includeScores: Should the response array contain the elements AND their scores? If `true`, the response array will follow the pattern [Item_1, Score_1, Item_2, ...] + /// - Returns: An array of elements from the SortedSet that were within the range provided, and optionally their scores. + @inlinable + public func zrevrangebyscore( + from key: RedisKey, + withMaximumScoreOf maxScore: RedisZScoreBound, + limitBy limit: (offset: Int, count: Int)? = nil, + includeScoresInResponse includeScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { + return self.zrevrangebyscore( + from: key, + withScoresBetween: (.inclusive(-.infinity), maxScore), + limitBy: limit, + includeScoresInResponse: includeScores + ) } @usableFromInline @@ -784,46 +1567,168 @@ extension RedisClient { // MARK: Range by Lexiographical extension RedisClient { - /// Gets elements from a sorted set whose lexiographical values are between the range specified. - /// - Important: This assumes all elements in the sorted set have the same score. If not, the returned elements are unspecified. - /// - Note: This treats the ordered set as ordered from low to high. + /// Gets all elements from a SortedSet whose lexiographical values are between the range specified. /// - /// For the inverse, see `zrevrangebylex(within:from:limitBy:)`. + /// For example: + /// ``` + /// // "mySortedSet" contains the values [1, 2, 3, 10] each with a score of 1 + /// client.zrangebylex(of: "mySortedSet", withValuesBetween: (.inclusive(1), .exclusive(3))) + /// // the response resolves to [1, 10, 2] + /// ``` /// - /// See [https://redis.io/commands/zrangebylex](https://redis.io/commands/zrangebylex) + /// See `RedisZLexBound` and [https://redis.io/commands/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: - /// - range: The value range to filter elements by. - /// - key: The key of the sorted set to search. - /// - limit: The optional offset and count of elements to query. - /// - Returns: A list of elements from the sorted set that were within the range provided. + /// - 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. + /// - Returns: An array of elements from the SortedSet that were within the range provided. @inlinable - public func zrangebylex( - within range: (min: String, max: String), + public func zrangebylex( from key: RedisKey, + withValuesBetween range: (min: RedisZLexBound, max: RedisZLexBound), limitBy limit: (offset: Int, count: Int)? = nil ) -> EventLoopFuture<[RESPValue]> { - return _zrangebylex(command: "ZRANGEBYLEX", key, range, limit) + return self._zrangebylex(command: "ZRANGEBYLEX", key, (range.min.description, range.max.description), limit) } - - /// Gets elements from a sorted set whose lexiographical values are between the range specified. - /// - Important: This assumes all elements in the sorted set have the same score. If not, the returned elements are unspecified. - /// - Note: This treats the ordered set as ordered from high to low. + + /// Gets all elements from a SortedSet whose lexiographical value is greater than a minimum value. /// - /// For the inverse, see `zrangebylex(within:from:limitBy:)`. + /// ``` + /// // "mySortedSet" contains the values [1, 2, 3, 10] each with a score of 1 + /// client.zrangebylex(of: "mySortedSet", withMinimumValueOf: .inclusive(1)) + /// // the response resolves to [1, 10, 2, 3] + /// ``` /// - /// See [https://redis.io/commands/zrevrangebylex](https://redis.io/commands/zrevrangebylex) + /// See `RedisZLexBound` and [https://redis.io/commands/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: - /// - range: The value range to filter elements by. - /// - key: The key of the sorted set to search. - /// - limit: The optional offset and count of elements to query. - /// - Returns: A list of elements from the sorted set that were within the range provided. + /// - 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 + /// - Returns: An array of elements from the SortedSet above the `minValue` threshold. @inlinable - public func zrevrangebylex( - within range: (min: String, max: String), + public func zrangebylex( from key: RedisKey, + withMinimumValueOf minValue: RedisZLexBound, limitBy limit: (offset: Int, count: Int)? = nil ) -> EventLoopFuture<[RESPValue]> { - return _zrangebylex(command: "ZREVRANGEBYLEX", key, (range.max, range.min), limit) + return self.zrangebylex(from: key, withValuesBetween: (minValue, .positiveInfinity), limitBy: limit) + } + + /// Gets all elements from a SortedSet whose lexiographical value is less than a maximum value. + /// + /// ``` + /// // "mySortedSet" contains the values [1, 2, 3, 10] each with a score of 1 + /// client.zlexcount(of: "mySortedSet", withMaximumValueOf: .exclusive(2)) + /// // the response resolves to [1, 10] + /// ``` + /// + /// See `RedisZLexBound` and [https://redis.io/commands/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 + /// - Returns: An array of elements from the SortedSet below the `maxValue` threshold. + @inlinable + public func zrangebylex( + from key: RedisKey, + withMaximumValueOf maxValue: RedisZLexBound, + limitBy limit: (offset: Int, count: Int)? = nil + ) -> EventLoopFuture<[RESPValue]> { + return self.zrangebylex(from: key, withValuesBetween: (.negativeInfinity, maxValue), limitBy: limit) + } + + /// Gets all elements from a SortedSet whose lexiographical values are between the range specified. + /// + /// For example: + /// ``` + /// // "mySortedSet" contains the values [1, 2, 3, 10] each with a score of 1 + /// client.zrevrangebylex(of: "mySortedSet", withValuesBetween: (.inclusive(1), .exclusive(3))) + /// // the response resolves to [2, 10 1] + /// ``` + /// + /// See `RedisZLexBound` and [https://redis.io/commands/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. + /// - Returns: An array of elements from the SortedSet that were within the range provided. + @inlinable + public func zrevrangebylex( + from key: RedisKey, + withValuesBetween range: (min: RedisZLexBound, max: RedisZLexBound), + limitBy limit: (offset: Int, count: Int)? = nil + ) -> EventLoopFuture<[RESPValue]> { + return self._zrangebylex(command: "ZREVRANGEBYLEX", key, (range.max.description, range.min.description), limit) + } + + /// Gets all elements from a SortedSet whose lexiographical value is greater than a minimum value. + /// + /// ``` + /// // "mySortedSet" contains the values [1, 2, 3, 10] each with a score of 1 + /// client.zrevrangebylex(of: "mySortedSet", withMinimumValueOf: .inclusive(1)) + /// // the response resolves to [3, 2, 10, 1] + /// ``` + /// + /// See `RedisZLexBound` and [https://redis.io/commands/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 + /// - Returns: An array of elements from the SortedSet above the `minValue` threshold. + @inlinable + public func zrevrangebylex( + from key: RedisKey, + withMinimumValueOf minValue: RedisZLexBound, + limitBy limit: (offset: Int, count: Int)? = nil + ) -> EventLoopFuture<[RESPValue]> { + return self.zrevrangebylex(from: key, withValuesBetween: (minValue, .positiveInfinity), limitBy: limit) + } + + /// Gets all elements from a SortedSet whose lexiographical value is less than a maximum value. + /// + /// ``` + /// // "mySortedSet" contains the values [1, 2, 3, 10] each with a score of 1 + /// client.zrevrangebylex(of: "mySortedSet", withMaximumValueOf: .exclusive(2)) + /// // the response resolves to [10, 1] + /// ``` + /// + /// See `RedisZLexBound` and [https://redis.io/commands/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 + /// - Returns: An array of elements from the SortedSet below the `maxValue` threshold. + @inlinable + public func zrevrangebylex( + from key: RedisKey, + withMaximumValueOf maxValue: RedisZLexBound, + limitBy limit: (offset: Int, count: Int)? = nil + ) -> EventLoopFuture<[RESPValue]> { + return self.zrevrangebylex(from: key, withValuesBetween: (.negativeInfinity, maxValue), limitBy: limit) } @usableFromInline @@ -883,68 +1788,248 @@ extension RedisClient { public func zrem(_ elements: Value..., from key: RedisKey) -> EventLoopFuture { return self.zrem(elements, from: key) } +} - /// Removes elements from a sorted set whose lexiographical values are between the range specified. - /// - Important: This assumes all elements in the sorted set have the same score. If not, the elements selected are unspecified. +// MARK: Remove by Lexiographical + +extension RedisClient { + /// Removes elements from a SortedSet whose lexiographical values are between the range specified. /// - /// See [https://redis.io/commands/zremrangebylex](https://redis.io/commands/zremrangebylex) + /// For example: + /// ```swift + /// // "mySortedSet" contains the values [1, 2, 3, 10] each with a score of 1. + /// client.zremrangebylex(from: "mySortedSet", withValuesBetween: (.inclusive(10), .exclusive(3)) + /// // elements 10 and 2 were removed + /// ``` + /// + /// See `RedisZLexBound` and [https://redis.io/commands/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: - /// - range: The value range to filter for elements to remove. - /// - key: The key of the sorted set to search. - /// - Returns: The number of elements removed from the sorted set. + /// - 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. + /// - Returns: The count of elements that were removed from the SortedSet. @inlinable - public func zremrangebylex( - within range: (min: String, max: String), - from key: RedisKey + public func zremrangebylex( + from key: RedisKey, + withValuesBetween range: (min: RedisZLexBound, max: RedisZLexBound) ) -> EventLoopFuture { let args: [RESPValue] = [ .init(bulk: key), - .init(bulk: range.min), - .init(bulk: range.max) + .init(bulk: range.min.description), + .init(bulk: range.max.description) ] return send(command: "ZREMRANGEBYLEX", with: args) .convertFromRESPValue() } + + /// Removes elements from a SortedSet whose lexiographical values are greater than a minimum value. + /// + /// For example: + /// ```swift + /// // "mySortedSet" contains the values [1, 2, 3, 10] each with a score of 1. + /// client.zremrangebylex(from: "mySortedSet", withMinimumValueOf: .inclusive(10)) + /// // elements 10, 2, and 3 are removed + /// ``` + /// + /// See `RedisZLexBound` and [https://redis.io/commands/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. + /// - Returns: The count of elements that were removed from the SortedSet. + @inlinable + public func zremrangebylex( + from key: RedisKey, + withMinimumValueOf minValue: RedisZLexBound + ) -> EventLoopFuture { + return self.zremrangebylex(from: key, withValuesBetween: (minValue, .positiveInfinity)) + } + + /// Removes elements from a SortedSet whose lexiographical values are less than a maximum value. + /// + /// For example: + /// ```swift + /// // "mySortedSet" contains the values [1, 2, 3, 10] each with a score of 1. + /// client.zremrangebylex(from: "mySortedSet", withMaximumValueOf: .exclusive(2)) + /// // elements 1 and 10 are removed + /// ``` + /// + /// See `RedisZLexBound` and [https://redis.io/commands/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. + /// - Returns: The count of elements that were removed from the SortedSet. + @inlinable + public func zremrangebylex( + from key: RedisKey, + withMaximumValueOf maxValue: RedisZLexBound + ) -> EventLoopFuture { + return self.zremrangebylex(from: key, withValuesBetween: (.negativeInfinity, maxValue)) + } +} - /// Removes elements from a sorted set whose index is between the provided range. +// MARK: Remove by Rank + +extension RedisClient { + /// Removes all elements from a SortedSet within the specified inclusive bounds of 0-based indices. /// /// See [https://redis.io/commands/zremrangebyrank](https://redis.io/commands/zremrangebyrank) /// - Parameters: - /// - range: The index range of elements to remove. - /// - key: The key of the sorted set to search. - /// - Returns: The number of elements removed from the sorted set. + /// - 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. + /// - Returns: The count of elements that were removed from the SortedSet. @inlinable - public func zremrangebyrank( - within range: (start: Int, stop: Int), - from key: RedisKey - ) -> EventLoopFuture { + public func zremrangebyrank(from key: RedisKey, firstIndex: Int, lastIndex: Int) -> EventLoopFuture { let args: [RESPValue] = [ .init(bulk: key), - .init(bulk: range.start), - .init(bulk: range.stop) + .init(bulk: firstIndex), + .init(bulk: lastIndex) ] - return send(command: "ZREMRANGEBYRANK", with: args) + return self.send(command: "ZREMRANGEBYRANK", with: args) .convertFromRESPValue() } - - /// Removes elements from a sorted set whose score is within the range specified. + + /// Removes all elements from a SortedSet within the specified inclusive bounds of 0-based indices. /// - /// See [https://redis.io/commands/zremrangebyscore](https://redis.io/commands/zremrangebyscore) + /// See [https://redis.io/commands/zremrangebyrank](https://redis.io/commands/zremrangebyrank) + /// - Warning: 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: - /// - range: The score range to filter for elements to remove. - /// - key: The key of the sorted set to search. - /// - Returns: The number of elements removed from the sorted set. + /// - key: The key of the SortedSet to remove elements from. + /// - range: The range of inclusive indices of elements to remove. + /// - Returns: The count of elements that were removed from the SortedSet. @inlinable - public func zremrangebyscore( - within range: (min: String, max: String), - from key: RedisKey - ) -> EventLoopFuture { - let args: [RESPValue] = [ - .init(bulk: key), - .init(bulk: range.min), - .init(bulk: range.max) - ] - return send(command: "ZREMRANGEBYSCORE", with: args) - .convertFromRESPValue() + public func zremrangebyrank(from key: RedisKey, indices range: ClosedRange) -> EventLoopFuture { + return self.zremrangebyrank(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound) + } + + /// Removes all elements from a SortedSet starting with the first index bound up to, but not including, the element at the last index bound. + /// + /// See [https://redis.io/commands/zremrangebyrank](https://redis.io/commands/zremrangebyrank) + /// - Warning: 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. + /// - Returns: The count of elements that were removed from the SortedSet. + @inlinable + public func zremrangebyrank(from key: RedisKey, indices range: Range) -> EventLoopFuture { + return self.zremrangebyrank(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound - 1) + } + + /// Removes all elements from the index specified to the end of a SortedSet. + /// + /// See [https://redis.io/commands/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. + /// - Returns: The count of elements that were removed from the SortedSet. + @inlinable + public func zremrangebyrank(from key: RedisKey, fromIndex index: Int) -> EventLoopFuture { + return self.zremrangebyrank(from: key, firstIndex: index, lastIndex: -1) + } + + /// Removes all elements from the start of a SortedSet up to, and including, the element at the index specified. + /// + /// See [https://redis.io/commands/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. + /// - Returns: The count of elements that were removed from the SortedSet. + @inlinable + public func zremrangebyrank(from key: RedisKey, throughIndex index: Int) -> EventLoopFuture { + return self.zremrangebyrank(from: key, firstIndex: 0, lastIndex: index) + } + + /// Removes all elements from the start of a SortedSet up to, but not including, the element at the index specified. + /// + /// See [https://redis.io/commands/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. + /// - Returns: The count of elements that were removed from the SortedSet. + @inlinable + public func zremrangebyrank(from key: RedisKey, upToIndex index: Int) -> EventLoopFuture { + return self.zremrangebyrank(from: key, firstIndex: 0, lastIndex: index - 1) + } +} + +// MARK: Remove by Score + +extension RedisClient { + /// Removes elements from a SortedSet whose score is within the range specified. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/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. + /// - Returns: The count of elements that were removed from the SortedSet. + @inlinable + public func zremrangebyscore( + from key: RedisKey, + withScoresBetween range: (min: RedisZScoreBound, max: RedisZScoreBound) + ) -> EventLoopFuture { + let args: [RESPValue] = [ + .init(bulk: key), + .init(bulk: range.min.description), + .init(bulk: range.max.description) + ] + return self.send(command: "ZREMRANGEBYSCORE", with: args) + .convertFromRESPValue() + } + + /// Removes elements from a SortedSet whose score is within the inclusive range specified. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/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. + /// - Returns: The count of elements that were removed from the SortedSet. + @inlinable + public func zremrangebyscore(from key: RedisKey, withScores range: ClosedRange) -> EventLoopFuture { + return self.zremrangebyscore(from: key, withScoresBetween: (.inclusive(range.lowerBound), .inclusive(range.upperBound))) + } + + /// Removes elements from a SortedSet whose score is at least a minimum score up to, but not including, a max score. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/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. + /// - Returns: The count of elements that were removed from the SortedSet. + @inlinable + public func zremrangebyscore(from key: RedisKey, withScores range: Range) -> EventLoopFuture { + return self.zremrangebyscore(from: key, withScoresBetween: (.inclusive(range.lowerBound), .exclusive(range.upperBound))) + } + + /// Removes elements from a SortedSet whose score is greater than a minimum score value. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/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. + /// - Returns: The count of elements that were removed from the SortedSet. + @inlinable + public func zremrangebyscore(from key: RedisKey, withMinimumScoreOf minScore: RedisZScoreBound) -> EventLoopFuture { + return self.zremrangebyscore(from: key, withScoresBetween: (minScore, .inclusive(.infinity))) + } + + /// Removes elements from a SortedSet whose score is less than a maximum score value. + /// + /// See `RedisZScoreBound` and [https://redis.io/commands/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. + /// - Returns: The count of elements that were removed from the SortedSet. + @inlinable + public func zremrangebyscore(from key: RedisKey, withMaximumScoreOf maxScore: RedisZScoreBound) -> EventLoopFuture { + return self.zremrangebyscore(from: key, withScoresBetween: (.inclusive(-.infinity), maxScore)) } } diff --git a/Tests/RediStackIntegrationTests/Commands/ListCommandsTests.swift b/Tests/RediStackIntegrationTests/Commands/ListCommandsTests.swift index 8bff601..33ce499 100644 --- a/Tests/RediStackIntegrationTests/Commands/ListCommandsTests.swift +++ b/Tests/RediStackIntegrationTests/Commands/ListCommandsTests.swift @@ -53,23 +53,28 @@ final class ListCommandsTests: RediStackIntegrationTestCase { } func test_lrange() throws { - var elements = try connection.lrange(within: (0, 10), from: #function).wait() + var elements = try connection.lrange(from: #function, indices: 0...10).wait() XCTAssertEqual(elements.count, 0) _ = try connection.lpush([5, 4, 3, 2, 1], into: #function).wait() - elements = try connection.lrange(within: (0, 4), from: #function).wait() + elements = try connection.lrange(from: #function, throughIndex: 4).wait() XCTAssertEqual(elements.count, 5) XCTAssertEqual(Int(fromRESP: elements[0]), 1) XCTAssertEqual(Int(fromRESP: elements[4]), 5) + + elements = try connection.lrange(from: #function, fromIndex: 1).wait() + XCTAssertEqual(elements.count, 4) + elements = try connection.lrange(from: #function, fromIndex: -3).wait() + XCTAssertEqual(elements.count, 3) - elements = try connection.lrange(within: (2, 0), from: #function).wait() + elements = try connection.lrange(from: #function, firstIndex: 2, lastIndex: 0).wait() XCTAssertEqual(elements.count, 0) - elements = try connection.lrange(within: (4, 5), from: #function).wait() + elements = try connection.lrange(from: #function, indices: 4...5).wait() XCTAssertEqual(elements.count, 1) - elements = try connection.lrange(within: (0, -4), from: #function).wait() + elements = try connection.lrange(from: #function, upToIndex: -3).wait() XCTAssertEqual(elements.count, 2) } @@ -109,13 +114,13 @@ final class ListCommandsTests: RediStackIntegrationTestCase { _ = try connection.lpush([10], into: #function).wait() _ = try connection.linsert(20, into: #function, after: 10).wait() - var elements = try connection.lrange(within: (0, 1), from: #function) + var elements = try connection.lrange(from: #function, throughIndex: 1) .map { response in response.compactMap { Int(fromRESP: $0) } } .wait() XCTAssertEqual(elements, [10, 20]) _ = try connection.linsert(30, into: #function, before: 10).wait() - elements = try connection.lrange(within: (0, 2), from: #function) + elements = try connection.lrange(from: #function, throughIndex: 2) .map { response in response.compactMap { Int(fromRESP: $0) } } .wait() XCTAssertEqual(elements, [30, 10, 20]) @@ -236,4 +241,36 @@ final class ListCommandsTests: RediStackIntegrationTestCase { .wait() XCTAssertEqual(element, 10) } + + func test_ltrim() throws { + let setup = { + _ = try self.connection.delete(#function).wait() + _ = try self.connection.lpush([5, 4, 3, 2, 1], into: #function).wait() + } + let getElements = { return try self.connection.lrange(from: #function, fromIndex: 0).wait() } + + try setup() + + XCTAssertNoThrow(try connection.ltrim(#function, before: 1, after: 3).wait()) + XCTAssertNoThrow(try connection.ltrim(#function, keepingIndices: 0...1).wait()) + var elements = try getElements() + XCTAssertEqual(elements.count, 2) + + try setup() + + XCTAssertNoThrow(try connection.ltrim(#function, keepingIndices: (-3)...).wait()) + elements = try getElements() + XCTAssertEqual(elements.count, 3) + + try setup() + + XCTAssertNoThrow(try connection.ltrim(#function, keepingIndices: ...(-4)).wait()) + elements = try getElements() + XCTAssertEqual(elements.count, 2) + + try setup() + XCTAssertNoThrow(try connection.ltrim(#function, keepingIndices: ..<(-2)).wait()) + elements = try getElements() + XCTAssertEqual(elements.count, 3) + } } diff --git a/Tests/RediStackIntegrationTests/Commands/SortedSetCommandsTests.swift b/Tests/RediStackIntegrationTests/Commands/SortedSetCommandsTests.swift index 49a92e6..fc5e1a4 100644 --- a/Tests/RediStackIntegrationTests/Commands/SortedSetCommandsTests.swift +++ b/Tests/RediStackIntegrationTests/Commands/SortedSetCommandsTests.swift @@ -126,17 +126,41 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase { } func test_zcount() throws { - var count = try connection.zcount(of: key, within: ("1", "3")).wait() + var count = try connection.zcount(of: key, withScores: 1...3).wait() XCTAssertEqual(count, 3) - count = try connection.zcount(of: key, within: ("(1", "(3")).wait() + + count = try connection.zcount(of: key, withScoresBetween: (.exclusive(1), .exclusive(3))).wait() XCTAssertEqual(count, 1) + + count = try connection.zcount(of: key, withScores: 3..<8).wait() + XCTAssertEqual(count, 5) + + count = try connection.zcount(of: key, withMinimumScoreOf: .exclusive(7)).wait() + XCTAssertEqual(count, 3) + + count = try connection.zcount(of: key, withMaximumScoreOf: 10).wait() + XCTAssertEqual(count, 10) + + count = try connection.zcount(of: key, withScoresBetween: (3, 0)).wait() + XCTAssertEqual(count, 0) } func test_zlexcount() throws { - var count = try connection.zlexcount(of: key, within: ("[1", "[3")).wait() + for i in 1...10 { + _ = try connection.zadd((i, 1), to: #function).wait() + } + + var count = try connection.zlexcount(of: #function, withValuesBetween: (.inclusive(1), .inclusive(3))).wait() + XCTAssertEqual(count, 4) + + count = try connection.zlexcount(of: #function, withValuesBetween: (.exclusive(1), .exclusive(3))).wait() + XCTAssertEqual(count, 2) + + count = try connection.zlexcount(of: #function, withMinimumValueOf: .inclusive(2)).wait() + XCTAssertEqual(count, 8) + + count = try connection.zlexcount(of: #function, withMaximumValueOf: .exclusive(3)).wait() XCTAssertEqual(count, 3) - count = try connection.zlexcount(of: key, within: ("(1", "(3")).wait() - XCTAssertEqual(count, 1) } func test_zpopmin() throws { @@ -254,9 +278,22 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase { } func test_zrange() throws { - var elements = try connection.zrange(within: (1, 3), from: key).wait() + var elements = try connection.zrange(from: key, indices: 1...3).wait() XCTAssertEqual(elements.count, 3) - elements = try connection.zrange(within: (1, 3), from: key, withScores: true).wait() + + elements = try connection.zrange(from: key, indices: 3..<9).wait() + XCTAssertEqual(elements.count, 6) + + elements = try connection.zrange(from: key, upToIndex: 4).wait() + XCTAssertEqual(elements.count, 4) + + elements = try connection.zrange(from: key, throughIndex: 4).wait() + XCTAssertEqual(elements.count, 5) + + elements = try connection.zrange(from: key, fromIndex: 7).wait() + XCTAssertEqual(elements.count, 3) + + elements = try connection.zrange(from: key, firstIndex: 1, lastIndex: 3, includeScoresInResponse: true).wait() XCTAssertEqual(elements.count, 6) let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false) @@ -268,9 +305,22 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase { } func test_zrevrange() throws { - var elements = try connection.zrevrange(within: (1, 3), from: key).wait() + var elements = try connection.zrevrange(from: key, indices: 1...3).wait() XCTAssertEqual(elements.count, 3) - elements = try connection.zrevrange(within: (1, 3), from: key, withScores: true).wait() + + elements = try connection.zrevrange(from: key, indices: 3..<9).wait() + XCTAssertEqual(elements.count, 6) + + elements = try connection.zrevrange(from: key, upToIndex: 4).wait() + XCTAssertEqual(elements.count, 4) + + elements = try connection.zrevrange(from: key, throughIndex: 4).wait() + XCTAssertEqual(elements.count, 5) + + elements = try connection.zrevrange(from: key, fromIndex: 7).wait() + XCTAssertEqual(elements.count, 3) + + elements = try connection.zrevrange(from: key, firstIndex: 1, lastIndex: 3, includeScoresInResponse: true).wait() XCTAssertEqual(elements.count, 6) let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false) @@ -282,9 +332,19 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase { } func test_zrangebyscore() throws { - var elements = try connection.zrangebyscore(within: ("(1", "3"), from: key).wait() + var elements = try connection.zrangebyscore(from: key, withScoresBetween: (.exclusive(1), 3)).wait() XCTAssertEqual(elements.count, 2) - elements = try connection.zrangebyscore(within: ("1", "3"), from: key, withScores: true).wait() + + elements = try connection.zrangebyscore(from: key, withScores: 7..<10, limitBy: (offset: 2, count: 3)).wait() + XCTAssertEqual(elements.count, 1) + + elements = try connection.zrangebyscore(from: key, withMinimumScoreOf: .exclusive(5)).wait() + XCTAssertEqual(elements.count, 5) + + elements = try connection.zrangebyscore(from: key, withMaximumScoreOf: 5).wait() + XCTAssertEqual(elements.count, 5) + + elements = try connection.zrangebyscore(from: key, withScores: 1...3, includeScoresInResponse: true).wait() XCTAssertEqual(elements.count, 6) let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false) @@ -296,9 +356,19 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase { } func test_zrevrangebyscore() throws { - var elements = try connection.zrevrangebyscore(within: ("(1", "3"), from: key).wait() + var elements = try connection.zrevrangebyscore(from: key, withScoresBetween: (.exclusive(1), 3)).wait() XCTAssertEqual(elements.count, 2) - elements = try connection.zrevrangebyscore(within: ("1", "3"), from: key, withScores: true).wait() + + elements = try connection.zrevrangebyscore(from: key, withScores: 7..<10, limitBy: (offset: 2, count: 3)).wait() + XCTAssertEqual(elements.count, 1) + + elements = try connection.zrevrangebyscore(from: key, withMinimumScoreOf: .exclusive(5)).wait() + XCTAssertEqual(elements.count, 5) + + elements = try connection.zrevrangebyscore(from: key, withMaximumScoreOf: 5).wait() + XCTAssertEqual(elements.count, 5) + + elements = try connection.zrevrangebyscore(from: key, withScores: 1...3, includeScoresInResponse: true).wait() XCTAssertEqual(elements.count, 6) let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false) @@ -310,35 +380,74 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase { } func test_zrangebylex() throws { - _ = try connection.zadd([(1, 0), (2, 0), (3, 0)], to: #function).wait() - - var elements = try connection.zrangebylex(within: ("[1", "[2"), from: #function) + for i in 1...10 { + _ = try connection.zadd((i, 1), to: #function).wait() + } + + var elements = try connection.zrangebylex(from: #function, withMinimumValueOf: .exclusive(10)) .wait() - .map { Int(fromRESP: $0) } - XCTAssertEqual(elements.count, 2) + .map(Int.init(fromRESP:)) + XCTAssertEqual(elements.count, 8) + + elements = try connection.zrangebylex(from: #function, withMaximumValueOf: .inclusive(5)) + .wait() + .map(Int.init(fromRESP:)) + XCTAssertEqual(elements.count, 6) + + elements = try connection.zrangebylex(from: #function, withValuesBetween: (.inclusive(1), .inclusive(2))) + .wait() + .map(Int.init(fromRESP:)) + + XCTAssertEqual(elements.count, 3) XCTAssertEqual(elements[0], 1) - XCTAssertEqual(elements[1], 2) + XCTAssertEqual(elements[1], 10) + XCTAssertEqual(elements[2], 2) - elements = try connection.zrangebylex(within: ("[1", "(4"), from: #function, limitBy: (offset: 1, count: 1)) + elements = try connection + .zrangebylex( + from: #function, + withValuesBetween: (.inclusive(1), .exclusive(4)), + limitBy: (offset: 1, count: 1) + ) .wait() - .map { Int(fromRESP: $0) } + .map(Int.init(fromRESP:)) XCTAssertEqual(elements.count, 1) - XCTAssertEqual(elements[0], 2) + XCTAssertEqual(elements[0], 10) } func test_zrevrangebylex() throws { - _ = try connection.zadd([(1, 0), (2, 0), (3, 0), (4, 0)], to: #function).wait() - - var elements = try connection.zrevrangebylex(within: ("(2", "[4"), from: #function) + for i in 1...10 { + _ = try connection.zadd((i, 1), to: #function).wait() + } + + var elements = try connection.zrevrangebylex(from: #function, withMinimumValueOf: .inclusive(1)) .wait() - .map { Int(fromRESP: $0) } + .map(Int.init(fromRESP:)) + XCTAssertEqual(elements.count, 10) + XCTAssertEqual(elements[0], 9) + XCTAssertEqual(elements[9], 1) + + elements = try connection.zrevrangebylex(from: #function, withMaximumValueOf: .exclusive(2)) + .wait() + .map(Int.init(fromRESP:)) + XCTAssertEqual(elements.count, 2) + XCTAssertEqual(elements[0], 10) + + elements = try connection.zrevrangebylex(from: #function, withValuesBetween: (.exclusive(2), .inclusive(4))) + .wait() + .map(Int.init(fromRESP:)) XCTAssertEqual(elements.count, 2) XCTAssertEqual(elements[0], 4) XCTAssertEqual(elements[1], 3) - elements = try connection.zrevrangebylex(within: ("[1", "(4"), from: #function, limitBy: (offset: 1, count: 2)) + elements = try connection + .zrevrangebylex( + from: #function, + withValuesBetween: (.inclusive(1), .exclusive(4)), + limitBy: (offset: 1, count: 2) + ) .wait() - .map { Int(fromRESP: $0) } + .map(Int.init(fromRESP:)) XCTAssertEqual(elements.count, 2) XCTAssertEqual(elements[0], 2) } @@ -356,31 +465,54 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase { } func test_zremrangebylex() throws { - _ = try connection.zadd([("bar", 0), ("car", 0), ("tar", 0)], to: #function).wait() + for value in ["bar", "car", "tar"] { + _ = try connection.zadd((value, 0), to: #function).wait() + } - var count = try connection.zremrangebylex(within: ("(a", "[t"), from: #function).wait() + var count = try connection.zremrangebylex(from: #function, withValuesBetween: (.exclusive("a"), .inclusive("t"))).wait() XCTAssertEqual(count, 2) - count = try connection.zremrangebylex(within: ("-", "[t"), from: #function).wait() + + count = try connection.zremrangebylex(from: #function, withMaximumValueOf: .inclusive("t")).wait() XCTAssertEqual(count, 0) - count = try connection.zremrangebylex(within: ("[t", "+"), from: #function).wait() + + count = try connection.zremrangebylex(from: #function, withMinimumValueOf: .inclusive("t")).wait() XCTAssertEqual(count, 1) } func test_zremrangebyrank() throws { - var count = try connection.zremrangebyrank(within: (0, 3), from: key).wait() - XCTAssertEqual(count, 4) - count = try connection.zremrangebyrank(within: (0, 10), from: key).wait() - XCTAssertEqual(count, 6) - count = try connection.zremrangebyrank(within: (0, 3), from: key).wait() - XCTAssertEqual(count, 0) + var count = try connection.zremrangebyrank(from: key, fromIndex: 9).wait() + XCTAssertEqual(count, 1) + + count = try connection.zremrangebyrank(from: key, indices: 0...1).wait() + XCTAssertEqual(count, 2) + + count = try connection.zremrangebyrank(from: key, indices: 0..<2).wait() + XCTAssertEqual(count, 2) + + count = try connection.zremrangebyrank(from: key, upToIndex: 1).wait() + XCTAssertEqual(count, 1) + + count = try connection.zremrangebyrank(from: key, throughIndex: 1).wait() + XCTAssertEqual(count, 2) + + count = try connection.zremrangebyrank(from: key, upToIndex: 0).wait() + XCTAssertEqual(count, 2) } func test_zremrangebyscore() throws { - var count = try connection.zremrangebyscore(within: ("(8", "10"), from: key).wait() + var count = try connection.zremrangebyscore(from: key, withScoresBetween: (.exclusive(8), 10)).wait() XCTAssertEqual(count, 2) - count = try connection.zremrangebyscore(within: ("4", "(7"), from: key).wait() + + count = try connection.zremrangebyscore(from: key, withScores: 4..<7).wait() XCTAssertEqual(count, 3) - count = try connection.zremrangebyscore(within: ("-inf", "+inf"), from: key).wait() - XCTAssertEqual(count, 5) + + count = try connection.zremrangebyscore(from: key, withScores: 2...3).wait() + XCTAssertEqual(count, 2) + + count = try connection.zremrangebyscore(from: key, withMinimumScoreOf: .exclusive(1)).wait() + XCTAssertEqual(count, 2) + + count = try connection.zremrangebyscore(from: key, withMaximumScoreOf: .inclusive(1)).wait() + XCTAssertEqual(count, 1) } }