Skip to content

Commit 393406e

Browse files
authored
support for infinity in sorted set commands params & results (zio#981)
1 parent cdbd145 commit 393406e

File tree

6 files changed

+198
-31
lines changed

6 files changed

+198
-31
lines changed

modules/redis-it/src/test/scala/zio/redis/SortedSetsSpec.scala

+95-5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ trait SortedSetsSpec extends IntegrationSpec {
2828
result <- redis.bzPopMax(duration, key1, key2, key3).returning[String]
2929
} yield assert(result)(isSome(equalTo((key1, tokyo))))
3030
),
31+
test("infinity score in set")(
32+
for {
33+
redis <- ZIO.service[Redis]
34+
key1 <- uuid
35+
key2 <- uuid
36+
duration = Duration.fromMillis(1000)
37+
delhi = MemberScore("Delhi", 1d)
38+
london = MemberScore("London", 3d)
39+
tokyo = MemberScore("Tokyo", 5d)
40+
edge = MemberScore("The edge of universe", Double.PositiveInfinity)
41+
_ <- redis.zAdd(key1)(delhi, edge)
42+
_ <- redis.zAdd(key2)(london, tokyo)
43+
result <- redis.bzPopMax(duration, key1, key2).returning[String]
44+
} yield assert(result)(isSome(equalTo((key1, edge))))
45+
),
3146
test("empty set")(
3247
for {
3348
redis <- ZIO.service[Redis]
@@ -53,6 +68,21 @@ trait SortedSetsSpec extends IntegrationSpec {
5368
result <- redis.bzPopMin(duration, key1, key2, key3).returning[String]
5469
} yield assert(result)(isSome(equalTo((key2, delhi))))
5570
),
71+
test("negative infinity score in set")(
72+
for {
73+
redis <- ZIO.service[Redis]
74+
key1 <- uuid
75+
key2 <- uuid
76+
duration = Duration.fromMillis(1000)
77+
delhi = MemberScore("Delhi", 1d)
78+
london = MemberScore("London", 3d)
79+
paris = MemberScore("Paris", 4d)
80+
quark = MemberScore("Quark", Double.NegativeInfinity)
81+
_ <- redis.zAdd(key1)(delhi, quark)
82+
_ <- redis.zAdd(key2)(london, paris)
83+
result <- redis.bzPopMin(duration, key1, key2).returning[String]
84+
} yield assert(result)(isSome(equalTo((key1, quark))))
85+
),
5686
test("empty set")(
5787
for {
5888
redis <- ZIO.service[Redis]
@@ -94,7 +124,18 @@ trait SortedSetsSpec extends IntegrationSpec {
94124
for {
95125
redis <- ZIO.service[Redis]
96126
key <- uuid
97-
added <- redis.zAdd(key)(MemberScore("a", 1d), MemberScore("b", 2d), MemberScore("c", 3d))
127+
added <- redis.zAdd(key)(MemberScore("a", 1d), MemberScore("b", 3.1415e50), MemberScore("c", 3d))
128+
} yield assert(added)(equalTo(3L))
129+
},
130+
test("multiple elements with negative & positive infinity") {
131+
for {
132+
redis <- ZIO.service[Redis]
133+
key <- uuid
134+
added <- redis.zAdd(key)(
135+
MemberScore("neg infinity", Double.NegativeInfinity),
136+
MemberScore("a", 1d),
137+
MemberScore("pos infinity", Double.PositiveInfinity)
138+
)
98139
} yield assert(added)(equalTo(3L))
99140
},
100141
test("error when not set") {
@@ -840,9 +881,15 @@ trait SortedSetsSpec extends IntegrationSpec {
840881
london = MemberScore("London", 3d)
841882
paris = MemberScore("Paris", 4d)
842883
tokyo = MemberScore("Tokyo", 5d)
843-
_ <- redis.zAdd(key)(delhi, mumbai, london, tokyo, paris)
884+
edge = MemberScore("The edge of universe", Double.PositiveInfinity)
885+
quark = MemberScore("Quark", Double.NegativeInfinity)
886+
_ <- redis.zAdd(key)(edge, delhi, mumbai, london, tokyo, paris, quark)
844887
result <- redis.zRange(key, 0 to -1).returning[String]
845-
} yield assert(result.toList)(equalTo(List("Delhi", "Mumbai", "London", "Paris", "Tokyo")))
888+
} yield assert(result.toList)(
889+
equalTo(
890+
List("Quark", "Delhi", "Mumbai", "London", "Paris", "Tokyo", "The edge of universe")
891+
)
892+
)
846893
},
847894
test("empty set") {
848895
for {
@@ -862,10 +909,12 @@ trait SortedSetsSpec extends IntegrationSpec {
862909
london = MemberScore("London", 3d)
863910
paris = MemberScore("Paris", 4d)
864911
tokyo = MemberScore("Tokyo", 5d)
865-
_ <- redis.zAdd(key)(delhi, mumbai, london, tokyo, paris)
912+
edge = MemberScore("The edge of universe", Double.PositiveInfinity)
913+
quark = MemberScore("Quark", Double.NegativeInfinity)
914+
_ <- redis.zAdd(key)(edge, delhi, mumbai, quark, london, tokyo, paris)
866915
result <- redis.zRangeWithScores(key, 0 to -1).returning[String]
867916
} yield assert(result.toList)(
868-
equalTo(List(delhi, mumbai, london, paris, tokyo))
917+
equalTo(List(quark, delhi, mumbai, london, paris, tokyo, edge))
869918
)
870919
},
871920
test("empty set") {
@@ -1398,6 +1447,18 @@ trait SortedSetsSpec extends IntegrationSpec {
13981447
members <- scanAll(key)
13991448
} yield assert(members)(equalTo(Chunk(a, b, c)))
14001449
},
1450+
test("with infinity in set") {
1451+
for {
1452+
redis <- ZIO.service[Redis]
1453+
key <- uuid
1454+
a = MemberScore("a", 1d)
1455+
b = MemberScore("b", 2d)
1456+
inf = MemberScore("inf", Double.PositiveInfinity)
1457+
negInf = MemberScore("neg inf", Double.NegativeInfinity)
1458+
_ <- redis.zAdd(key)(a, b, inf, negInf)
1459+
members <- scanAll(key)
1460+
} yield assert(members)(equalTo(Chunk(negInf, a, b, inf)))
1461+
},
14011462
test("empty set") {
14021463
for {
14031464
redis <- ZIO.service[Redis]
@@ -1470,6 +1531,14 @@ trait SortedSetsSpec extends IntegrationSpec {
14701531
result <- redis.zScore(key, "Delhi")
14711532
} yield assert(result)(isSome(equalTo(10.0)))
14721533
},
1534+
test("infinity score in set") {
1535+
for {
1536+
redis <- ZIO.service[Redis]
1537+
key <- uuid
1538+
_ <- redis.zAdd(key)(MemberScore("Delhi", 10d), MemberScore("Infinity", Double.PositiveInfinity))
1539+
result <- redis.zScore(key, "Infinity")
1540+
} yield assert(result)(isSome(equalTo(Double.PositiveInfinity)))
1541+
},
14731542
test("empty set") {
14741543
for {
14751544
redis <- ZIO.service[Redis]
@@ -1499,6 +1568,27 @@ trait SortedSetsSpec extends IntegrationSpec {
14991568
key <- uuid
15001569
result <- redis.zMScore(key, "Hyderabad")
15011570
} yield assert(result)(equalTo(Chunk(None)))
1571+
},
1572+
test("infinity score") {
1573+
for {
1574+
redis <- ZIO.service[Redis]
1575+
key <- uuid
1576+
_ <- redis.zAdd(key)(
1577+
MemberScore("Delhi", 10d),
1578+
MemberScore("Infinity", Double.PositiveInfinity),
1579+
MemberScore("-Infinity", Double.NegativeInfinity)
1580+
)
1581+
result <- redis.zMScore(key, "Infinity", "-Infinity", "Delhi", "Ankh-Morpork")
1582+
} yield assert(result)(
1583+
equalTo(
1584+
Chunk(
1585+
Some(Double.PositiveInfinity),
1586+
Some(Double.NegativeInfinity),
1587+
Some(10d),
1588+
None
1589+
)
1590+
)
1591+
)
15021592
}
15031593
),
15041594
suite("zUnion")(

modules/redis/src/main/scala/zio/redis/Input.scala

+8-2
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,14 @@ object Input {
337337
}
338338

339339
final case class MemberScoreInput[M: BinaryCodec]() extends Input[MemberScore[M]] {
340-
def encode(data: MemberScore[M]): RespCommand =
341-
RespCommand(RespCommandArgument.Value(data.score.toString), RespCommandArgument.Value(data.member))
340+
def encode(data: MemberScore[M]): RespCommand = {
341+
val score = data.score match {
342+
case Double.NegativeInfinity => "-inf"
343+
case Double.PositiveInfinity => "+inf"
344+
case d: Double => d.toString.toLowerCase
345+
}
346+
RespCommand(RespCommandArgument.Value(score), RespCommandArgument.Value(data.member))
347+
}
342348
}
343349

344350
case object NoAckInput extends Input[NoAck] {

modules/redis/src/main/scala/zio/redis/Output.scala

+20-4
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ object Output {
128128
}
129129
}
130130

131+
case object DoubleOrInfinity extends Output[Double] {
132+
protected def tryDecode(respValue: RespValue): Double =
133+
respValue match {
134+
case RespValue.BulkString(bytes) => decodeDouble(bytes, withInfinity = true)
135+
case other => throw ProtocolError(s"$other isn't a double or an infinity.")
136+
}
137+
}
138+
131139
private object DurationOutput extends Output[Long] {
132140
protected def tryDecode(respValue: RespValue): Long =
133141
respValue match {
@@ -729,11 +737,19 @@ object Output {
729737
}
730738
}
731739

732-
private def decodeDouble(bytes: Chunk[Byte]): Double = {
740+
private def decodeDouble(bytes: Chunk[Byte], withInfinity: Boolean = false): Double = {
733741
val text = new String(bytes.toArray, StandardCharsets.UTF_8)
734-
try text.toDouble
735-
catch {
736-
case _: NumberFormatException => throw ProtocolError(s"'$text' isn't a double.")
742+
text match {
743+
case "inf" if withInfinity => Double.PositiveInfinity
744+
case "-inf" if withInfinity => Double.NegativeInfinity
745+
case _ =>
746+
try text.toDouble
747+
catch {
748+
case _: NumberFormatException =>
749+
throw ProtocolError(
750+
if (withInfinity) s"'$text' isn't a double or an infinity." else s"'$text' isn't a double."
751+
)
752+
}
737753
}
738754
}
739755

modules/redis/src/main/scala/zio/redis/api/SortedSets.scala

+24-20
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
3939
* @return
4040
* A three-element Chunk with the first element being the name of the key where a member was popped, the second
4141
* element is the popped member itself, and the third element is the score of the popped element. An empty chunk is
42-
* returned when no element could be popped and the timeout expired.
42+
* returned when no element could be popped and the timeout expired. Double.PositiveInfinity and
43+
* Double.NegativeInfinity are valid scores as well.
4344
*/
4445
final def bzPopMax[K: Schema](
4546
timeout: Duration,
@@ -49,7 +50,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
4950
new ResultBuilder1[({ type lambda[x] = Option[(K, MemberScore[x])] })#lambda, G] {
5051
def returning[M: Schema]: G[Option[(K, MemberScore[M])]] = {
5152
val memberScoreOutput =
52-
Tuple3Output(ArbitraryOutput[K](), ArbitraryOutput[M](), DoubleOutput).map { case (k, m, s) =>
53+
Tuple3Output(ArbitraryOutput[K](), ArbitraryOutput[M](), DoubleOrInfinity).map { case (k, m, s) =>
5354
(k, MemberScore(m, s))
5455
}
5556

@@ -76,7 +77,8 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
7677
* @return
7778
* A three-element Chunk with the first element being the name of the key where a member was popped, the second
7879
* element is the popped member itself, and the third element is the score of the popped element. An empty chunk is
79-
* returned when no element could be popped and the timeout expired.
80+
* returned when no element could be popped and the timeout expired. Double.PositiveInfinity and
81+
* Double.NegativeInfinity are valid scores as well.
8082
*/
8183
final def bzPopMin[K: Schema](
8284
timeout: Duration,
@@ -86,7 +88,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
8688
new ResultBuilder1[({ type lambda[x] = Option[(K, MemberScore[x])] })#lambda, G] {
8789
def returning[M: Schema]: G[Option[(K, MemberScore[M])]] = {
8890
val memberScoreOutput =
89-
Tuple3Output(ArbitraryOutput[K](), ArbitraryOutput[M](), DoubleOutput).map { case (k, m, s) =>
91+
Tuple3Output(ArbitraryOutput[K](), ArbitraryOutput[M](), DoubleOrInfinity).map { case (k, m, s) =>
9092
(k, MemberScore(m, s))
9193
}
9294

@@ -248,7 +250,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
248250
NonEmptyList(ArbitraryKeyInput[K]()),
249251
WithScoresInput
250252
),
251-
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput)
253+
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity)
252254
.map(_.map { case (m, s) => MemberScore(m, s) })
253255
)
254256
command.run((keys.size + 1, (key, keys.toList), WithScores))
@@ -366,7 +368,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
366368
OptionalInput(WeightsInput),
367369
WithScoresInput
368370
),
369-
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput)
371+
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity)
370372
.map(_.map { case (m, s) => MemberScore(m, s) })
371373
)
372374
command.run((keys.size + 1, (key, keys.toList), aggregate, weights, WithScores))
@@ -437,10 +439,11 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
437439
* Keys of the rest sets
438440
* @return
439441
* List of scores or None associated with the specified member values (a double precision floating point number).
442+
* Double.PositiveInfinity and Double.NegativeInfinity are valid scores as well.
440443
*/
441444
final def zMScore[K: Schema](key: K, keys: K*): G[Chunk[Option[Double]]] = {
442445
val command =
443-
RedisCommand(ZMScore, NonEmptyList(ArbitraryKeyInput[K]()), ChunkOutput(OptionalOutput(DoubleOutput)))
446+
RedisCommand(ZMScore, NonEmptyList(ArbitraryKeyInput[K]()), ChunkOutput(OptionalOutput(DoubleOrInfinity)))
444447
command.run((key, keys.toList))
445448
}
446449

@@ -462,7 +465,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
462465
val command = RedisCommand(
463466
ZPopMax,
464467
Tuple2(ArbitraryKeyInput[K](), OptionalInput(LongInput)),
465-
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput)
468+
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity)
466469
.map(_.map { case (m, s) => MemberScore(m, s) })
467470
)
468471
command.run((key, count))
@@ -487,7 +490,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
487490
val command = RedisCommand(
488491
ZPopMin,
489492
Tuple2(ArbitraryKeyInput[K](), OptionalInput(LongInput)),
490-
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput)
493+
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity)
491494
.map(_.map { case (m, s) => MemberScore(m, s) })
492495
)
493496
command.run((key, count))
@@ -550,7 +553,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
550553
val command = RedisCommand(
551554
ZRandMember,
552555
Tuple3(ArbitraryKeyInput[K](), LongInput, WithScoresInput),
553-
ZRandMemberTuple2Output(ArbitraryOutput[M](), DoubleOutput)
556+
ZRandMemberTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity)
554557
.map(_.map { case (m, s) => MemberScore(m, s) })
555558
)
556559

@@ -593,7 +596,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
593596
val command = RedisCommand(
594597
ZRange,
595598
Tuple3(ArbitraryKeyInput[K](), RangeInput, WithScoresInput),
596-
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput)
599+
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity)
597600
.map(_.map { case (m, s) => MemberScore(m, s) })
598601
)
599602
command.run((key, range, WithScores))
@@ -693,7 +696,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
693696
WithScoresInput,
694697
OptionalInput(LimitInput)
695698
),
696-
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput)
699+
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity)
697700
.map(_.map { case (m, s) => MemberScore(m, s) })
698701
)
699702
command.run((key, scoreRange.min.asString, scoreRange.max.asString, WithScores, limit))
@@ -735,7 +738,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
735738
RedisCommand(
736739
ZRank,
737740
Tuple3(ArbitraryKeyInput[K](), ArbitraryValueInput[M](), WithScoreInput),
738-
OptionalOutput(Tuple2Output(LongOutput, DoubleOutput).map { case (r, s) => RankScore(r, s) })
741+
OptionalOutput(Tuple2Output(LongOutput, DoubleOrInfinity).map { case (r, s) => RankScore(r, s) })
739742
)
740743
command.run((key, member, WithScore))
741744
}
@@ -849,7 +852,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
849852
val command = RedisCommand(
850853
ZRevRange,
851854
Tuple3(ArbitraryKeyInput[K](), RangeInput, WithScoresInput),
852-
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput)
855+
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity)
853856
.map(_.map { case (m, s) => MemberScore(m, s) })
854857
)
855858
command.run((key, range, WithScores))
@@ -953,7 +956,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
953956
WithScoresInput,
954957
OptionalInput(LimitInput)
955958
),
956-
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput)
959+
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity)
957960
.map(_.map { case (m, s) => MemberScore(m, s) })
958961
)
959962
command.run((key, scoreRange.max.asString, scoreRange.min.asString, WithScores, limit))
@@ -993,7 +996,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
993996
val command = RedisCommand(
994997
ZRevRank,
995998
Tuple3(ArbitraryKeyInput[K](), ArbitraryValueInput[M](), WithScoreInput),
996-
OptionalOutput(Tuple2Output(LongOutput, DoubleOutput).map { case (r, s) => RankScore(r, s) })
999+
OptionalOutput(Tuple2Output(LongOutput, DoubleOrInfinity).map { case (r, s) => RankScore(r, s) })
9971000
)
9981001
command.run((key, member, WithScore))
9991002
}
@@ -1021,7 +1024,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
10211024
new ResultBuilder1[({ type lambda[x] = (Long, MemberScores[x]) })#lambda, G] {
10221025
def returning[M: Schema]: G[(Long, Chunk[MemberScore[M]])] = {
10231026
val memberScoresOutput =
1024-
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput).map(_.map { case (m, s) => MemberScore(m, s) })
1027+
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity).map(_.map { case (m, s) => MemberScore(m, s) })
10251028

10261029
val command =
10271030
RedisCommand(
@@ -1042,13 +1045,14 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
10421045
* @param member
10431046
* Member of sorted set
10441047
* @return
1045-
* The score of member (a double precision floating point number.
1048+
* The score of member (a double precision floating point number).
1049+
* Double.PositiveInfinity and Double.NegativeInfinity are valid scores as well.
10461050
*/
10471051
final def zScore[K: Schema, M: Schema](key: K, member: M): G[Option[Double]] = {
10481052
val command = RedisCommand(
10491053
ZScore,
10501054
Tuple2(ArbitraryKeyInput[K](), ArbitraryValueInput[M]()),
1051-
OptionalOutput(DoubleOutput)
1055+
OptionalOutput(DoubleOrInfinity)
10521056
)
10531057
command.run((key, member))
10541058
}
@@ -1122,7 +1126,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] {
11221126
OptionalInput(AggregateInput),
11231127
WithScoresInput
11241128
),
1125-
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput)
1129+
ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity)
11261130
.map(_.map { case (m, s) => MemberScore(m, s) })
11271131
)
11281132
command.run((keys.size + 1, (key, keys.toList), weights, aggregate, WithScores))

0 commit comments

Comments
 (0)