SortedSetCommands.swift 35.9 KB
Newer Older
1 2
//===----------------------------------------------------------------------===//
//
3
// This source file is part of the RediStack open source project
4
//
5
// Copyright (c) 2019 RediStack project authors
6 7 8
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
9
// See CONTRIBUTORS.txt for the list of RediStack project authors
10 11 12 13 14
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

15 16 17 18
import NIO

// MARK: Static Helpers

19
extension RedisClient {
20
    @usableFromInline
21 22 23 24
    static func _mapSortedSetResponse(
        _ response: [RESPValue],
        scoreIsFirst: Bool
    ) throws -> [(RESPValue, Double)] {
25 26 27 28 29 30 31 32
        guard response.count > 0 else { return [] }

        var result: [(RESPValue, Double)] = []

        var index = 0
        repeat {
            let scoreItem = response[scoreIsFirst ? index : index + 1]

33
            guard let score = Double(fromRESP: scoreItem) else {
34
                throw RedisClientError.assertionFailure(message: "Unexpected response: '\(scoreItem)'")
35 36
            }

37 38
            let elementIndex = scoreIsFirst ? index + 1 : index
            result.append((response[elementIndex], score))
39 40 41 42 43 44 45 46 47 48

            index += 2
        } while (index < response.count)

        return result
    }
}

// MARK: General

49
/// The supported options for the `zadd` command with Redis SortedSet types.
50
///
51 52 53 54 55 56 57 58
/// See [https://redis.io/commands/zadd#zadd-options-redis-302-or-greater](https://redis.io/commands/zadd#zadd-options-redis-302-or-greater)
public enum RedisSortedSetAddOption: String {
    /// When adding elements, any that do not already exist in the SortedSet will be ignored and the score of the existing element will be updated.
    case onlyUpdateExistingElements = "XX"
    /// When adding elements, any that already exist in the SortedSet will be ignored and the score of the existing element will not be updated.
    case onlyAddNewElements = "NX"
}

59
extension RedisClient {
60
    /// Adds elements to a sorted set, assigning their score to the values provided.
61
    /// - Note: `INCR` is not supported by this library in `zadd`. Use the `zincrby(:element:in:)` method instead.
62 63 64
    ///
    /// See [https://redis.io/commands/zadd](https://redis.io/commands/zadd)
    /// - Parameters:
65 66
    ///     - elements: A list of elements and their score to add to the sorted set.
    ///     - key: The key of the sorted set.
67 68 69 70 71 72
    ///     - option: An option for modifying the behavior of the command.
    ///     - returnChangedCount: `zadd` normally returns the number of new elements added to the set,
    ///         but setting this to `true` will instead have the command return the number of elements changed.
    ///
    ///         "Changed" in this context are new elements added, and elements that had their score updated.
    /// - Returns: The number of elements added to the sorted set, unless `returnChangedCount` was set to `true`.
73
    @inlinable
74 75
    public func zadd<Value: RESPValueConvertible>(
        _ elements: [(element: Value, score: Double)],
76
        to key: String,
77 78
        option: RedisSortedSetAddOption? = nil,
        returnChangedCount: Bool = false
79
    ) -> EventLoopFuture<Int> {
80
        var args: [RESPValue] = [.init(bulk: key)]
81 82 83 84 85 86
        
        if let opt = option {
            args.append(.init(bulk: opt.rawValue))
        }
        if returnChangedCount {
            args.append(.init(bulk: "CH"))
87 88 89 90
        }
        args.add(contentsOf: elements, overestimatedCountBeingAdded: elements.count * 2) { (array, next) in
            array.append(.init(bulk: next.score.description))
            array.append(next.element.convertedToRESPValue())
91 92 93
        }

        return send(command: "ZADD", with: args)
94
            .convertFromRESPValue()
95 96 97 98 99 100
    }

    /// Adds an element to a sorted set, assigning their score to the value provided.
    ///
    /// See [https://redis.io/commands/zadd](https://redis.io/commands/zadd)
    /// - Parameters:
101 102
    ///     - element: The element and its score to add to the sorted set.
    ///     - key: The key of the sorted set.
103 104 105 106 107 108
    ///     - option: An option for modifying the behavior of the command.
    ///     - returnChangedCount: `zadd` normally returns the number of new elements added to the set,
    ///         but setting this to `true` will instead have the command return the number of elements changed.
    ///
    ///         "Changed" in this context are new elements added, and elements that had their score updated.
    /// - Returns: `true` if the element was added or score was updated in the sorted set, depending on the `option` and `returnChangedCount` settings set.
109
    @inlinable
110 111
    public func zadd<Value: RESPValueConvertible>(
        _ element: (element: Value, score: Double),
112
        to key: String,
113 114
        option: RedisSortedSetAddOption? = nil,
        returnChangedCount: Bool = false
115
    ) -> EventLoopFuture<Bool> {
116
        return zadd([element], to: key, option: option, returnChangedCount: returnChangedCount)
117 118 119
            .map { return $0 == 1 }
    }

120
    /// Gets the number of elements in a sorted set.
121 122
    ///
    /// See [https://redis.io/commands/zcard](https://redis.io/commands/zcard)
123
    /// - Parameter key: The key of the sorted set.
124 125 126
    /// - Returns: The number of elements in the sorted set.
    @inlinable
    public func zcard(of key: String) -> EventLoopFuture<Int> {
127 128
        let args = [RESPValue(bulk: key)]
        return send(command: "ZCARD", with: args)
129
            .convertFromRESPValue()
130 131
    }

132
    /// Gets the score of the specified element in a stored set.
133 134 135
    ///
    /// See [https://redis.io/commands/zscore](https://redis.io/commands/zscore)
    /// - Parameters:
136 137 138
    ///     - element: The element in the sorted set to get the score for.
    ///     - key: The key of the sorted set.
    /// - Returns: The score of the element provided, or `nil` if the element is not found in the set or the set does not exist.
139
    @inlinable
140 141 142 143 144 145
    public func zscore<Value: RESPValueConvertible>(of element: Value, in key: String) -> EventLoopFuture<Double?> {
        let args: [RESPValue] = [
            .init(bulk: key),
            element.convertedToRESPValue()
        ]
        return send(command: "ZSCORE", with: args)
146
            .map { return Double(fromRESP: $0) }
147 148
    }

149
    /// Incrementally iterates over all elements in a sorted set.
150 151 152 153
    ///
    /// See [https://redis.io/commands/zscan](https://redis.io/commands/zscan)
    /// - Parameters:
    ///     - key: The key identifying the sorted set.
154
    ///     - position: The position to start the scan from.
155
    ///     - count: The number of elements to advance by. Redis default is 10.
156 157
    ///     - match: A glob-style pattern to filter values to be selected from the result set.
    /// - Returns: A cursor position for additional invocations with a limited collection of elements found in the sorted set with their scores.
158 159 160 161 162
    @inlinable
    public func zscan(
        _ key: String,
        startingFrom position: Int = 0,
        count: Int? = nil,
163 164
        matching match: String? = nil
    ) -> EventLoopFuture<(Int, [(RESPValue, Double)])> {
165 166 167 168 169 170 171 172 173 174
        return _scan(command: "ZSCAN", resultType: [RESPValue].self, key, position, count, match)
            .flatMapThrowing {
                let values = try Self._mapSortedSetResponse($0.1, scoreIsFirst: false)
                return ($0.0, values)
            }
    }
}

// MARK: Rank

175
extension RedisClient {
176 177
    /// Returns the rank (index) of the specified element in a sorted set.
    /// - Note: This treats the ordered set as ordered from low to high.
178
    /// For the inverse, see `zrevrank(of:in:)`.
179 180 181
    ///
    /// See [https://redis.io/commands/zrank](https://redis.io/commands/zrank)
    /// - Parameters:
182 183
    ///     - element: The element in the sorted set to search for.
    ///     - key: The key of the sorted set to search.
184 185
    /// - Returns: The index of the element, or `nil` if the key was not found.
    @inlinable
186 187 188 189 190 191
    public func zrank<Value: RESPValueConvertible>(of element: Value, in key: String) -> EventLoopFuture<Int?> {
        let args: [RESPValue] = [
            .init(bulk: key),
            element.convertedToRESPValue()
        ]
        return send(command: "ZRANK", with: args)
192
            .convertFromRESPValue()
193 194 195 196
    }

    /// Returns the rank (index) of the specified element in a sorted set.
    /// - Note: This treats the ordered set as ordered from high to low.
197
    /// For the inverse, see `zrank(of:in:)`.
198 199 200
    ///
    /// See [https://redis.io/commands/zrevrank](https://redis.io/commands/zrevrank)
    /// - Parameters:
201 202
    ///     - element: The element in the sorted set to search for.
    ///     - key: The key of the sorted set to search.
203 204
    /// - Returns: The index of the element, or `nil` if the key was not found.
    @inlinable
205 206 207 208 209 210
    public func zrevrank<Value: RESPValueConvertible>(of element: Value, in key: String) -> EventLoopFuture<Int?> {
        let args: [RESPValue] = [
            .init(bulk: key),
            element.convertedToRESPValue()
        ]
        return send(command: "ZREVRANK", with: args)
211
            .convertFromRESPValue()
212 213 214 215 216
    }
}

// MARK: Count

217
extension RedisClient {
218 219 220 221
    /// Returns the number of elements in a sorted set with a score within the range specified.
    ///
    /// See [https://redis.io/commands/zcount](https://redis.io/commands/zcount)
    /// - Parameters:
222 223
    ///     - key: The key of the sorted set to count.
    ///     - range: The min and max range of scores to filter for.
224 225
    /// - Returns: The number of elements in the sorted set that fit within the score range.
    @inlinable
226 227 228 229
    public func zcount(
        of key: String,
        within range: (min: String, max: String)
    ) -> EventLoopFuture<Int> {
230 231 232 233 234 235
        let args: [RESPValue] = [
            .init(bulk: key),
            .init(bulk: range.min),
            .init(bulk: range.max)
        ]
        return send(command: "ZCOUNT", with: args)
236
            .convertFromRESPValue()
237 238 239 240 241 242 243
    }

    /// 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.
    ///
    /// See [https://redis.io/commands/zlexcount](https://redis.io/commands/zlexcount)
    /// - Parameters:
244 245
    ///     - key: The key of the sorted set to count.
    ///     - range: The min and max range of values to filter for.
246 247
    /// - Returns: The number of elements in the sorted set that fit within the value range.
    @inlinable
248 249 250 251
    public func zlexcount(
        of key: String,
        within range: (min: String, max: String)
    ) -> EventLoopFuture<Int> {
252 253 254 255 256 257
        let args: [RESPValue] = [
            .init(bulk: key),
            .init(bulk: range.min),
            .init(bulk: range.max)
        ]
        return send(command: "ZLEXCOUNT", with: args)
258
            .convertFromRESPValue()
259 260 261 262 263
    }
}

// MARK: Pop

264
extension RedisClient {
265
    /// Removes elements from a sorted set with the lowest scores.
266 267 268
    ///
    /// See [https://redis.io/commands/zpopmin](https://redis.io/commands/zpopmin)
    /// - Parameters:
269
    ///     - key: The key identifying the sorted set in Redis.
270
    ///     - count: The max number of elements to pop from the set.
271
    /// - Returns: A list of elements popped from the sorted set with their associated score.
272
    @inlinable
273
    public func zpopmin(from key: String, max count: Int) -> EventLoopFuture<[(RESPValue, Double)]> {
274 275 276
        return _zpop(command: "ZPOPMIN", count, key)
    }

277
    /// Removes the element from a sorted set with the lowest score.
278 279
    ///
    /// See [https://redis.io/commands/zpopmin](https://redis.io/commands/zpopmin)
280
    /// - Parameter key: The key identifying the sorted set in Redis.
281 282 283 284 285 286 287
    /// - Returns: The element and its associated score that was popped from the sorted set, or `nil` if set was empty.
    @inlinable
    public func zpopmin(from key: String) -> EventLoopFuture<(RESPValue, Double)?> {
        return _zpop(command: "ZPOPMIN", nil, key)
            .map { return $0.count > 0 ? $0[0] : nil }
    }

288
    /// Removes elements from a sorted set with the highest scores.
289 290 291
    ///
    /// See [https://redis.io/commands/zpopmax](https://redis.io/commands/zpopmax)
    /// - Parameters:
292
    ///     - key: The key identifying the sorted set in Redis.
293
    ///     - count: The max number of elements to pop from the set.
294
    /// - Returns: A list of elements popped from the sorted set with their associated score.
295
    @inlinable
296
    public func zpopmax(from key: String, max count: Int) -> EventLoopFuture<[(RESPValue, Double)]> {
297 298 299
        return _zpop(command: "ZPOPMAX", count, key)
    }

300
    /// Removes the element from a sorted set with the highest score.
301 302
    ///
    /// See [https://redis.io/commands/zpopmax](https://redis.io/commands/zpopmax)
303
    /// - Parameter key: The key identifying the sorted set in Redis.
304 305 306 307 308 309 310 311
    /// - Returns: The element and its associated score that was popped from the sorted set, or `nil` if set was empty.
    @inlinable
    public func zpopmax(from key: String) -> EventLoopFuture<(RESPValue, Double)?> {
        return _zpop(command: "ZPOPMAX", nil, key)
            .map { return $0.count > 0 ? $0[0] : nil }
    }

    @usableFromInline
312 313 314 315 316
    func _zpop(
        command: String,
        _ count: Int?,
        _ key: String
    ) -> EventLoopFuture<[(RESPValue, Double)]> {
317
        var args: [RESPValue] = [.init(bulk: key)]
318

319 320 321
        if let c = count {
            guard c != 0 else { return self.eventLoop.makeSucceededFuture([]) }

322
            args.append(.init(bulk: c))
323
        }
324 325

        return send(command: command, with: args)
326
            .convertFromRESPValue(to: [RESPValue].self)
327 328 329 330
            .flatMapThrowing { return try Self._mapSortedSetResponse($0, scoreIsFirst: true) }
    }
}

331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
// MARK: Blocking Pop

extension RedisClient {
    /// Removes the element from a sorted set with the lowest score, blocking until an element is
    /// available.
    ///
    /// - Important:
    ///     This will block the connection from completing further commands until an element
    ///     is available to pop from the set.
    ///
    ///     It is **highly** recommended to set a reasonable `timeout`
    ///     or to use the non-blocking `zpopmin` method where possible.
    ///
    /// See [https://redis.io/commands/bzpopmin](https://redis.io/commands/bzpopmin)
    /// - Parameters:
    ///     - key: The key identifying the sorted set in Redis.
    ///     - timeout: The time (in seconds) to wait. `0` means indefinitely.
    /// - Returns:
    ///     The element and its associated score that was popped from the sorted set,
    ///     or `nil` if the timeout was reached.
    @inlinable
    public func bzpopmin(
        from key: String,
        timeout: Int = 0
    ) -> EventLoopFuture<(Double, RESPValue)?> {
        return bzpopmin(from: [key], timeout: timeout)
            .map {
                guard let response = $0 else { return nil }
                return (response.1, response.2)
            }
    }

    /// Removes the element from a sorted set with the lowest score, blocking until an element is
    /// available.
    ///
    /// - Important:
    ///     This will block the connection from completing further commands until an element
    ///     is available to pop from the group of sets.
    ///
    ///     It is **highly** recommended to set a reasonable `timeout`
    ///     or to use the non-blocking `zpopmin` method where possible.
    ///
    /// See [https://redis.io/commands/bzpopmin](https://redis.io/commands/bzpopmin)
    /// - Parameters:
    ///     - keys: A list of sorted set keys in Redis.
    ///     - timeout: The time (in seconds) to wait. `0` means indefinitely.
    /// - Returns:
    ///     If timeout was reached, `nil`.
    ///
    ///     Otherwise, the key of the sorted set the element was removed from, the element itself,
    ///     and its associated score is returned.
    @inlinable
    public func bzpopmin(
        from keys: [String],
        timeout: Int = 0
    ) -> EventLoopFuture<(String, Double, RESPValue)?> {
        return self._bzpop(command: "BZPOPMIN", keys, timeout)
    }

    /// Removes the element from a sorted set with the highest score, blocking until an element is
    /// available.
    ///
    /// - Important:
    ///     This will block the connection from completing further commands until an element
    ///     is available to pop from the set.
    ///
    ///     It is **highly** recommended to set a reasonable `timeout`
    ///     or to use the non-blocking `zpopmax` method where possible.
    ///
    /// See [https://redis.io/commands/bzpopmax](https://redis.io/commands/bzpopmax)
    /// - Parameters:
    ///     - key: The key identifying the sorted set in Redis.
    ///     - timeout: The time (in seconds) to wait. `0` means indefinitely.
    /// - Returns:
    ///     The element and its associated score that was popped from the sorted set,
    ///     or `nil` if the timeout was reached.
    @inlinable
    public func bzpopmax(
        from key: String,
        timeout: Int = 0
    ) -> EventLoopFuture<(Double, RESPValue)?> {
        return self.bzpopmax(from: [key], timeout: timeout)
            .map {
                guard let response = $0 else { return nil }
                return (response.1, response.2)
            }
    }

    /// Removes the element from a sorted set with the highest score, blocking until an element is
    /// available.
    ///
    /// - Important:
    ///     This will block the connection from completing further commands until an element
    ///     is available to pop from the group of sets.
    ///
    ///     It is **highly** recommended to set a reasonable `timeout`
    ///     or to use the non-blocking `zpopmax` method where possible.
    ///
    /// See [https://redis.io/commands/bzpopmax](https://redis.io/commands/bzpopmax)
    /// - Parameters:
    ///     - keys: A list of sorted set keys in Redis.
    ///     - timeout: The time (in seconds) to wait. `0` means indefinitely.
    /// - Returns:
    ///     If timeout was reached, `nil`.
    ///
    ///     Otherwise, the key of the sorted set the element was removed from, the element itself,
    ///     and its associated score is returned.
    @inlinable
    public func bzpopmax(
        from keys: [String],
        timeout: Int = 0
    ) -> EventLoopFuture<(String, Double, RESPValue)?> {
        return self._bzpop(command: "BZPOPMAX", keys, timeout)
    }

    @usableFromInline
    func _bzpop(
        command: String,
        _ keys: [String],
        _ timeout: Int
    ) -> EventLoopFuture<(String, Double, RESPValue)?> {
452 453 454
        var args = keys.map(RESPValue.init)
        args.append(.init(bulk: timeout))
        
455 456 457 458 459 460
        return send(command: command, with: args)
            // per the Redis docs,
            // we will receive either a nil response,
            // or an array with 3 elements in the form [Set Key, Element Score, Element Value]
            .flatMapThrowing {
                guard !$0.isNull else { return nil }
461
                guard let response = [RESPValue](fromRESP: $0) else {
462
                    throw RedisClientError.failedRESPConversion(to: [RESPValue].self)
463 464 465 466
                }
                assert(response.count == 3, "Unexpected response size returned!")
                guard
                    let key = response[0].string,
467
                    let score = Double(fromRESP: response[1])
468
                else {
469
                    throw RedisClientError.assertionFailure(message: "Unexpected structure in response: \(response)")
470 471 472 473 474 475
                }
                return (key, score, response[2])
            }
    }
}

476 477
// MARK: Increment

478
extension RedisClient {
479
    /// Increments the score of the specified element in a sorted set.
480 481 482
    ///
    /// See [https://redis.io/commands/zincrby](https://redis.io/commands/zincrby)
    /// - Parameters:
483 484
    ///     - amount: The amount to increment this element's score by.
    ///     - element: The element to increment.
485
    ///     - key: The key of the sorted set.
486
    /// - Returns: The new score of the element.
487
    @inlinable
488
    public func zincrby<Value: RESPValueConvertible>(
489
        _ amount: Double,
490
        element: Value,
491 492
        in key: String
    ) -> EventLoopFuture<Double> {
493 494 495 496 497 498
        let args: [RESPValue] = [
            .init(bulk: key),
            .init(bulk: amount.description),
            element.convertedToRESPValue()
        ]
        return send(command: "ZINCRBY", with: args)
499
            .convertFromRESPValue()
500 501 502 503 504
    }
}

// MARK: Intersect and Union

505 506 507 508 509 510 511 512 513 514 515 516 517 518
/// The supported methods for aggregating results from the `zunionstore` or `zinterstore` commands in Redis.
///
/// For more information on these values, see
/// [https://redis.io/commands/zunionstore](https://redis.io/commands/zunionstore)
/// [https://redis.io/commands/zinterstore](https://redis.io/commands/zinterstore)
public enum RedisSortedSetAggregateMethod: String {
    /// Add the score of all matching elements in the source SortedSets.
    case sum = "SUM"
    /// Use the minimum score of the matching elements in the source SortedSets.
    case min = "MIN"
    /// Use the maximum score of the matching elements in the source SortedSets.
    case max = "MAX"
}

519
extension RedisClient {
520
    /// Calculates the union of two or more sorted sets and stores the result.
521 522 523 524
    /// - Note: This operation overwrites any value stored at the destination key.
    ///
    /// See [https://redis.io/commands/zunionstore](https://redis.io/commands/zunionstore)
    /// - Parameters:
525
    ///     - destination: The key of the new sorted set from the result.
526 527
    ///     - sources: The list of sorted set keys to treat as the source of the union.
    ///     - weights: The multiplying factor to apply to the corresponding `sources` key based on index of the two parameters.
528
    ///     - aggregateMethod: The method of aggregating the values of the union. If one isn't specified, Redis will default to `.sum`.
529
    /// - Returns: The number of elements in the new sorted set.
530 531
    @inlinable
    public func zunionstore(
532 533
        as destination: String,
        sources: [String],
534
        weights: [Int]? = nil,
535
        aggregateMethod aggregate: RedisSortedSetAggregateMethod? = nil
536
    ) -> EventLoopFuture<Int> {
537 538 539
        return _zopstore(command: "ZUNIONSTORE", sources, destination, weights, aggregate)
    }

540
    /// Calculates the intersection of two or more sorted sets and stores the result.
541 542 543 544
    /// - Note: This operation overwrites any value stored at the destination key.
    ///
    /// See [https://redis.io/commands/zinterstore](https://redis.io/commands/zinterstore)
    /// - Parameters:
545
    ///     - destination: The key of the new sorted set from the result.
546 547
    ///     - sources: The list of sorted set keys to treat as the source of the intersection.
    ///     - weights: The multiplying factor to apply to the corresponding `sources` key based on index of the two parameters.
548
    ///     - aggregateMethod: The method of aggregating the values of the intersection. If one isn't specified, Redis will default to `.sum`.
549
    /// - Returns: The number of elements in the new sorted set.
550 551
    @inlinable
    public func zinterstore(
552 553
        as destination: String,
        sources: [String],
554
        weights: [Int]? = nil,
555
        aggregateMethod aggregate: RedisSortedSetAggregateMethod? = nil
556
    ) -> EventLoopFuture<Int> {
557 558 559 560 561 562 563 564 565
        return _zopstore(command: "ZINTERSTORE", sources, destination, weights, aggregate)
    }

    @usableFromInline
    func _zopstore(
        command: String,
        _ sources: [String],
        _ destination: String,
        _ weights: [Int]?,
566 567
        _ aggregate: RedisSortedSetAggregateMethod?
    ) -> EventLoopFuture<Int> {
568 569
        assert(sources.count > 0, "At least 1 source key should be provided.")

570 571 572 573 574
        var args: [RESPValue] = [
            .init(bulk: destination),
            .init(bulk: sources.count)
        ]
        args.append(convertingContentsOf: sources)
575 576 577 578 579

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

580 581
            args.append(.init(bulk: "WEIGHTS"))
            args.append(convertingContentsOf: w)
582 583 584
        }

        if let a = aggregate {
585
            args.append(.init(bulk: "AGGREGATE"))
586
            args.append(.init(bulk: a.rawValue))
587 588 589
        }

        return send(command: command, with: args)
590
            .convertFromRESPValue()
591 592 593 594 595
    }
}

// MARK: Range

596
extension RedisClient {
597
    /// Gets the specified range of elements in a sorted set.
598
    /// - Note: This treats the ordered set as ordered from low to high.
599 600
    ///
    /// For the inverse, see `zrevrange(within:from:withScores:)`.
601 602 603
    ///
    /// See [https://redis.io/commands/zrange](https://redis.io/commands/zrange)
    /// - Parameters:
604 605 606
    ///     - 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, ...]
607 608 609
    /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores.
    @inlinable
    public func zrange(
610
        within range: (start: Int, stop: Int),
611
        from key: String,
612 613
        withScores: Bool = false
    ) -> EventLoopFuture<[RESPValue]> {
614 615 616
        return _zrange(command: "ZRANGE", key, range.start, range.stop, withScores)
    }

617
    /// Gets the specified range of elements in a sorted set.
618
    /// - Note: This treats the ordered set as ordered from high to low.
619 620
    ///
    /// For the inverse, see `zrange(within:from:withScores:)`.
621 622 623
    ///
    /// See [https://redis.io/commands/zrevrange](https://redis.io/commands/zrevrange)
    /// - Parameters:
624 625 626
    ///     - 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, ...]
627 628 629
    /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores.
    @inlinable
    public func zrevrange(
630
        within range: (start: Int, stop: Int),
631
        from key: String,
632 633
        withScores: Bool = false
    ) -> EventLoopFuture<[RESPValue]> {
634 635 636 637
        return _zrange(command: "ZREVRANGE", key, range.start, range.stop, withScores)
    }

    @usableFromInline
638 639 640 641 642 643 644
    func _zrange(
        command: String,
        _ key: String,
        _ start: Int,
        _ stop: Int,
        _ withScores: Bool
    ) -> EventLoopFuture<[RESPValue]> {
645 646 647 648 649
        var args: [RESPValue] = [
            .init(bulk: key),
            .init(bulk: start),
            .init(bulk: stop)
        ]
650

651
        if withScores { args.append(.init(bulk: "WITHSCORES")) }
652 653

        return send(command: command, with: args)
654
            .convertFromRESPValue()
655 656 657 658 659
    }
}

// MARK: Range by Score

660
extension RedisClient {
661
    /// Gets elements from a sorted set whose score fits within the range specified.
662
    /// - Note: This treats the ordered set as ordered from low to high.
663 664
    ///
    /// For the inverse, see `zrevrangebyscore(within:from:withScores:limitBy:)`.
665 666 667
    ///
    /// See [https://redis.io/commands/zrangebyscore](https://redis.io/commands/zrangebyscore)
    /// - Parameters:
668 669 670 671
    ///     - 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, ...]
    ///     - limit: The optional offset and count of elements to query.
672 673 674 675 676 677
    /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores.
    @inlinable
    public func zrangebyscore(
        within range: (min: String, max: String),
        from key: String,
        withScores: Bool = false,
678 679
        limitBy limit: (offset: Int, count: Int)? = nil
    ) -> EventLoopFuture<[RESPValue]> {
680 681 682
        return _zrangebyscore(command: "ZRANGEBYSCORE", key, range, withScores, limit)
    }

683
    /// Gets elements from a sorted set whose score fits within the range specified.
684
    /// - Note: This treats the ordered set as ordered from high to low.
685 686
    ///
    /// For the inverse, see `zrangebyscore(within:from:withScores:limitBy:)`.
687 688 689
    ///
    /// See [https://redis.io/commands/zrevrangebyscore](https://redis.io/commands/zrevrangebyscore)
    /// - Parameters:
690 691 692 693
    ///     - 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, ...]
    ///     - limit: The optional offset and count of elements to query.
694 695 696 697 698 699
    /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores.
    @inlinable
    public func zrevrangebyscore(
        within range: (min: String, max: String),
        from key: String,
        withScores: Bool = false,
700 701
        limitBy limit: (offset: Int, count: Int)? = nil
    ) -> EventLoopFuture<[RESPValue]> {
702 703 704 705
        return _zrangebyscore(command: "ZREVRANGEBYSCORE", key, (range.max, range.min), withScores, limit)
    }

    @usableFromInline
706 707 708 709 710 711 712
    func _zrangebyscore(
        command: String,
        _ key: String,
        _ range: (min: String, max: String),
        _ withScores: Bool,
        _ limit: (offset: Int, count: Int)?
    ) -> EventLoopFuture<[RESPValue]> {
713 714 715 716 717
        var args: [RESPValue] = [
            .init(bulk: key),
            .init(bulk: range.min),
            .init(bulk: range.max)
        ]
718

719
        if withScores { args.append(.init(bulk: "WITHSCORES")) }
720 721

        if let l = limit {
722 723
            args.append(.init(bulk: "LIMIT"))
            args.append(convertingContentsOf: [l.offset, l.count])
724 725 726
        }

        return send(command: command, with: args)
727
            .convertFromRESPValue()
728 729 730 731 732
    }
}

// MARK: Range by Lexiographical

733
extension RedisClient {
734
    /// Gets elements from a sorted set whose lexiographical values are between the range specified.
735 736
    /// - 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.
737 738
    ///
    /// For the inverse, see `zrevrangebylex(within:from:limitBy:)`.
739 740 741
    ///
    /// See [https://redis.io/commands/zrangebylex](https://redis.io/commands/zrangebylex)
    /// - Parameters:
742 743 744
    ///     - 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.
745 746 747 748 749
    /// - Returns: A list of elements from the sorted set that were within the range provided.
    @inlinable
    public func zrangebylex(
        within range: (min: String, max: String),
        from key: String,
750 751
        limitBy limit: (offset: Int, count: Int)? = nil
    ) -> EventLoopFuture<[RESPValue]> {
752 753 754
        return _zrangebylex(command: "ZRANGEBYLEX", key, range, limit)
    }

755
    /// Gets elements from a sorted set whose lexiographical values are between the range specified.
756 757
    /// - 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.
758 759
    ///
    /// For the inverse, see `zrangebylex(within:from:limitBy:)`.
760 761 762
    ///
    /// See [https://redis.io/commands/zrevrangebylex](https://redis.io/commands/zrevrangebylex)
    /// - Parameters:
763 764 765
    ///     - 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.
766 767 768 769 770
    /// - Returns: A list of elements from the sorted set that were within the range provided.
    @inlinable
    public func zrevrangebylex(
        within range: (min: String, max: String),
        from key: String,
771 772
        limitBy limit: (offset: Int, count: Int)? = nil
    ) -> EventLoopFuture<[RESPValue]> {
773 774 775 776
        return _zrangebylex(command: "ZREVRANGEBYLEX", key, (range.max, range.min), limit)
    }

    @usableFromInline
777 778 779 780 781 782
    func _zrangebylex(
        command: String,
        _ key: String,
        _ range: (min: String, max: String),
        _ limit: (offset: Int, count: Int)?
    ) -> EventLoopFuture<[RESPValue]> {
783 784 785 786 787
        var args: [RESPValue] = [
            .init(bulk: key),
            .init(bulk: range.min),
            .init(bulk: range.max)
        ]
788 789

        if let l = limit {
790 791 792 793
            args.reserveCapacity(6) // 3 above, plus 3 being added
            args.append(.init(bulk: "LIMIT"))
            args.append(.init(bulk: l.offset))
            args.append(.init(bulk: l.count))
794 795 796
        }

        return send(command: command, with: args)
797
            .convertFromRESPValue()
798 799 800 801 802
    }
}

// MARK: Remove

803
extension RedisClient {
804
    /// Removes the specified elements from a sorted set.
805 806 807
    ///
    /// See [https://redis.io/commands/zrem](https://redis.io/commands/zrem)
    /// - Parameters:
808 809 810
    ///     - elements: The values to remove from the sorted set.
    ///     - key: The key of the sorted set.
    /// - Returns: The number of elements removed from the set.
811
    @inlinable
812
    public func zrem<Value: RESPValueConvertible>(_ elements: [Value], from key: String) -> EventLoopFuture<Int> {
813
        guard elements.count > 0 else { return self.eventLoop.makeSucceededFuture(0) }
814

815 816 817 818
        var args: [RESPValue] = [.init(bulk: key)]
        args.append(convertingContentsOf: elements)
        
        return send(command: "ZREM", with: args)
819
            .convertFromRESPValue()
820 821 822 823 824 825 826
    }

    /// 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.
    ///
    /// See [https://redis.io/commands/zremrangebylex](https://redis.io/commands/zremrangebylex)
    /// - Parameters:
827 828
    ///     - range: The value range to filter for elements to remove.
    ///     - key: The key of the sorted set to search.
829 830
    /// - Returns: The number of elements removed from the sorted set.
    @inlinable
831 832 833 834
    public func zremrangebylex(
        within range: (min: String, max: String),
        from key: String
    ) -> EventLoopFuture<Int> {
835 836 837 838 839 840
        let args: [RESPValue] = [
            .init(bulk: key),
            .init(bulk: range.min),
            .init(bulk: range.max)
        ]
        return send(command: "ZREMRANGEBYLEX", with: args)
841
            .convertFromRESPValue()
842 843 844 845 846 847
    }

    /// Removes elements from a sorted set whose index is between the provided range.
    ///
    /// See [https://redis.io/commands/zremrangebyrank](https://redis.io/commands/zremrangebyrank)
    /// - Parameters:
848 849
    ///     - range: The index range of elements to remove.
    ///     - key: The key of the sorted set to search.
850 851
    /// - Returns: The number of elements removed from the sorted set.
    @inlinable
852 853 854 855
    public func zremrangebyrank(
        within range: (start: Int, stop: Int),
        from key: String
    ) -> EventLoopFuture<Int> {
856 857 858 859 860 861
        let args: [RESPValue] = [
            .init(bulk: key),
            .init(bulk: range.start),
            .init(bulk: range.stop)
        ]
        return send(command: "ZREMRANGEBYRANK", with: args)
862
            .convertFromRESPValue()
863 864 865 866 867 868
    }

    /// Removes elements from a sorted set whose score is within the range specified.
    ///
    /// See [https://redis.io/commands/zremrangebyscore](https://redis.io/commands/zremrangebyscore)
    /// - Parameters:
869 870
    ///     - range: The score range to filter for elements to remove.
    ///     - key: The key of the sorted set to search.
871 872
    /// - Returns: The number of elements removed from the sorted set.
    @inlinable
873 874 875 876
    public func zremrangebyscore(
        within range: (min: String, max: String),
        from key: String
    ) -> EventLoopFuture<Int> {
877 878 879 880 881 882
        let args: [RESPValue] = [
            .init(bulk: key),
            .init(bulk: range.min),
            .init(bulk: range.max)
        ]
        return send(command: "ZREMRANGEBYSCORE", with: args)
883
            .convertFromRESPValue()
884 885
    }
}