diff --git a/README.md b/README.md index af9f130..ce10221 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,10 @@ redis-cli shutdown |[x]|[x]|[ ]|Strings| |[x]|[x]|[ ]|Lists| |[x]|[x]|[ ]|Sets| -|[ ]|[ ]|[ ]|Sorted Sets| +|[x]|[x]|[ ]|Sorted Sets| |[x]|[x]|[ ]|Hashes| |[ ]|[ ]|[ ]|HyperLogLog| -|[ ]|[ ]|[ ]|sscan, hscan| +|[ ]|[ ]|[ ]|sscan, hscan, zscan| |[ ]|[ ]|[ ]|sort, sortNStore| #### Features diff --git a/src/main/scala/com/github/mogproject/redismock/MockRedisClient.scala b/src/main/scala/com/github/mogproject/redismock/MockRedisClient.scala index 9cb8ba1..598bd36 100644 --- a/src/main/scala/com/github/mogproject/redismock/MockRedisClient.scala +++ b/src/main/scala/com/github/mogproject/redismock/MockRedisClient.scala @@ -17,7 +17,7 @@ trait MockRedisCommand extends Redis with MockOperations with MockStringOperations with MockListOperations with MockSetOperations -//with MockSortedSetOperations +with MockSortedSetOperations with MockHashOperations //with MockEvalOperations //with MockPubOperations diff --git a/src/main/scala/com/github/mogproject/redismock/MockSortedSetOperations.scala b/src/main/scala/com/github/mogproject/redismock/MockSortedSetOperations.scala new file mode 100644 index 0000000..2d58c83 --- /dev/null +++ b/src/main/scala/com/github/mogproject/redismock/MockSortedSetOperations.scala @@ -0,0 +1,225 @@ +package com.github.mogproject.redismock + +import com.github.mogproject.redismock.entity._ +import com.github.mogproject.redismock.storage.Storage +import com.github.mogproject.redismock.util.ops._ +import com.redis.{SortedSetOperations, Redis} +import com.redis.serialization._ + +import scala.collection.SortedSet + +trait MockSortedSetOperations extends SortedSetOperations with MockOperations with Storage { + self: Redis => + + // + // raw functions + // + private def getRaw(key: Any)(implicit format: Format): Option[SortedSetValue] = + currentDB.get(Key(key)).map(x => SortedSetValue(x.as(SORTED_SET))) + + private def getRawOrEmpty(key: Any)(implicit format: Format): SortedSetValue = + getRaw(key).getOrElse(SortedSetValue.empty) + + private def setRaw(key: Any, value: SortedSetValue)(implicit format: Format): Unit = currentDB.update(Key(key), value) + + // ZADD (Variadic: >= 2.4) + // Add the specified members having the specified score to the sorted set stored at key. + override def zadd(key: Any, score: Double, member: Any, scoreVals: (Double, Any)*) + (implicit format: Format): Option[Long] = withDB { + val a = getRawOrEmpty(key) + val b = a ++ ((score, member) :: scoreVals.toList).map { case (s, m) => (s, Bytes(m))} + setRaw(key, b) + Some(b.size - a.size) + } + + // ZREM (Variadic: >= 2.4) + // Remove the specified members from the sorted set value stored at key. + override def zrem(key: Any, member: Any, members: Any*)(implicit format: Format): Option[Long] = withDB { + val a = getRawOrEmpty(key) + val b = a -- (member :: members.toList).map(Bytes.apply) + setRaw(key, b) + Some(a.size - b.size) + } + + // ZINCRBY + // + override def zincrby(key: Any, incr: Double, member: Any)(implicit format: Format): Option[Double] = withDB { + for { + v <- getRaw(key) + x = Bytes(member) + s <- v.index.get(x) + } yield { + s + incr <| { d => setRaw(key, v.updated(d, x))} + } + } + + // ZCARD + // + override def zcard(key: Any)(implicit format: Format): Option[Long] = Some(getRawOrEmpty(key).size) + + // ZSCORE + // + override def zscore(key: Any, element: Any)(implicit format: Format): Option[Double] = + getRaw(key).flatMap(_.index.get(Bytes(element))) + + // ZRANGE + // + + import com.redis.RedisClient._ + + override def zrange[A](key: Any, start: Int = 0, end: Int = -1, sortAs: SortOrder = ASC) + (implicit format: Format, parse: Parse[A]): Option[List[A]] = + getRaw(key).map(_.data |> sliceThenReverse(start, end, sortAs != ASC) |> toValues(parse)) + + override def zrangeWithScore[A](key: Any, start: Int = 0, end: Int = -1, sortAs: SortOrder = ASC) + (implicit format: Format, parse: Parse[A]): Option[List[(A, Double)]] = + getRaw(key).map(_.data |> sliceThenReverse(start, end, sortAs != ASC) |> toValueScores(parse)) + + // ZRANGEBYSCORE + // + override def zrangebyscore[A](key: Any, + min: Double = Double.NegativeInfinity, + minInclusive: Boolean = true, + max: Double = Double.PositiveInfinity, + maxInclusive: Boolean = true, + limit: Option[(Int, Int)], + sortAs: SortOrder = ASC)(implicit format: Format, parse: Parse[A]): Option[List[A]] = + getRaw(key).map(_.data |> + filterByRange(min, minInclusive, max, maxInclusive) |> sliceThenReverse(limit, sortAs != ASC) |> + toValues(parse)) + + + override def zrangebyscoreWithScore[A](key: Any, + min: Double = Double.NegativeInfinity, + minInclusive: Boolean = true, + max: Double = Double.PositiveInfinity, + maxInclusive: Boolean = true, + limit: Option[(Int, Int)], + sortAs: SortOrder = ASC) + (implicit format: Format, parse: Parse[A]): Option[List[(A, Double)]] = + getRaw(key).map(_.data |> + filterByRange(min, minInclusive, max, maxInclusive) |> sliceThenReverse(limit, sortAs != ASC) |> + toValueScores(parse)) + + // ZRANK + // ZREVRANK + // + override def zrank(key: Any, member: Any, reverse: Boolean = false)(implicit format: Format): Option[Long] = + getRaw(key).flatMap { v => v.rank(Bytes(member)).map(_.mapWhenTrue(reverse)(v.size - _ - 1))} + + // ZREMRANGEBYRANK + // + override def zremrangebyrank(key: Any, start: Int = 0, end: Int = -1)(implicit format: Format): Option[Long] = + withDB { + getRaw(key).map { v => + val b = v -- sliceThenReverse(start, end, doReverse = false)(v.data).map(_._2) + setRaw(key, b) + v.size - b.size + } + } + + // ZREMRANGEBYSCORE + // + override def zremrangebyscore(key: Any, + start: Double = Double.NegativeInfinity, + end: Double = Double.PositiveInfinity) + (implicit format: Format): Option[Long] = + withDB { + getRaw(key).map { v => + val b = v -- filterByRange(start, minInclusive = true, end, maxInclusive = true)(v.data).toSeq.map(_._2) + setRaw(key, b) + v.size - b.size + } + } + + // ZUNION + // + override def zunionstore(dstKey: Any, keys: Iterable[Any], aggregate: Aggregate = SUM) + (implicit format: Format): Option[Long] = + zunionstoreWeighted(dstKey, keys.map((_, 1.0)), aggregate) + + override def zunionstoreWeighted(dstKey: Any, kws: Iterable[Product2[Any, Double]], aggregate: Aggregate = SUM) + (implicit format: Format): Option[Long] = storeReduced(_ | _)(dstKey, kws, aggregate) + + // ZINTERSTORE + // + override def zinterstore(dstKey: Any, keys: Iterable[Any], aggregate: Aggregate = SUM) + (implicit format: Format): Option[Long] = + zinterstoreWeighted(dstKey, keys.map((_, 1.0)), aggregate) + + override def zinterstoreWeighted(dstKey: Any, kws: Iterable[Product2[Any, Double]], aggregate: Aggregate = SUM) + (implicit format: Format): Option[Long] = storeReduced(_ & _)(dstKey, kws, aggregate) + + // ZCOUNT + // + override def zcount(key: Any, + min: Double = Double.NegativeInfinity, + max: Double = Double.PositiveInfinity, + minInclusive: Boolean = true, + maxInclusive: Boolean = true) + (implicit format: Format): Option[Long] = + getRaw(key).map(xs => filterByRange(min, minInclusive, max, maxInclusive)(xs.data).size) + + // ZSCAN + // Incrementally iterate sorted sets elements and associated scores (since 2.8) + override def zscan[A](key: Any, cursor: Int, pattern: Any = "*", count: Int = 10)(implicit format: Format, parse: Parse[A]): Option[(Option[Int], Option[List[Option[A]]])] = + // TODO: implement + ??? + + // send("ZSCAN", key :: cursor :: ((x: List[Any]) => if(pattern == "*") x else "match" :: pattern :: x)(if(count == 10) Nil else List("count", count)))(asPair) + + // + // helper functions + // + private def filterByRange(min: Double, minInclusive: Boolean, max: Double, maxInclusive: Boolean) + (xs: SortedSet[(Double, Bytes)]): SortedSet[(Double, Bytes)] = + xs.range(min -> Bytes.empty, max -> Bytes.MaxValue).filter { case (s, _) => + (minInclusive || s != min) && (maxInclusive || s != max) + } + + private def sliceThenReverse(start: Int, end: Int, doReverse: Boolean) + (xs: SortedSet[(Double, Bytes)]): List[(Double, Bytes)] = { + val from = start + (if (start < 0) xs.size else 0) + val until = end + 1 + (if (end < 0) xs.size else 0) + val s = if (doReverse) xs.size - until else from + val t = if (doReverse) xs.size - from else until + xs.slice(s, t).toList.mapWhenTrue(doReverse)(_.reverse) + } + + private def sliceThenReverse(limit: Option[(Int, Int)], doReverse: Boolean) + (xs: SortedSet[(Double, Bytes)]): List[(Double, Bytes)] = { + val (start, end) = limit match { + case Some((offset, count)) => (offset, offset + count - 1) + case None => (0, -1) + } + sliceThenReverse(start, end, doReverse)(xs) + } + + private def toValues[A](parse: Parse[A])(xs: List[(Double, Bytes)]): List[A] = xs.map(_._2.parse(parse)) + + private def toValueScores[A](parse: Parse[A])(xs: List[(Double, Bytes)]): List[(A, Double)] = + xs.map { case (s, v) => (v.parse(parse), s)} + + private def storeReduced(op: (Set[Bytes], Set[Bytes]) => Set[Bytes]) + (dstKey: Any, kws: Iterable[Product2[Any, Double]], aggregate: Aggregate) + (implicit format: Format): Option[Long] = withDB { + + val (aggregateFunc, zero): ((Double, Double) => Double, Double) = aggregate match { + case SUM => (_ + _, 0.0) + case MIN => (math.min, Double.MaxValue) + case MAX => (math.max, Double.MinValue) + } + + val indexWeightSeq = kws.map { case Product2(k, w) => (getRawOrEmpty(k).index, w)} + + val values: Seq[Bytes] = indexWeightSeq.map(_._1.keySet).reduceLeft(op).toSeq + val scores = values.map { v => + indexWeightSeq.foldLeft(zero) { + case (x, (index, w)) => aggregateFunc(x, index.get(v).map(_ * w).getOrElse(zero)) + } + } + + SortedSetValue(scores zip values: _*) <| { v => setRaw(dstKey, v) } |> { v => Some(v.size) } + } +} + diff --git a/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala b/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala index 72d4240..e7a6971 100644 --- a/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala +++ b/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala @@ -2,6 +2,7 @@ package com.github.mogproject.redismock.entity import com.redis.serialization.{Format, Parse} import com.redis.serialization.Parse.parseDefault +import scala.annotation.tailrec import scala.collection.immutable.VectorBuilder import scala.collection.mutable import scala.util.Try @@ -16,7 +17,8 @@ case class Bytes(value: Vector[Byte]) with scala.collection.immutable.IndexedSeq[Byte] with scala.collection.TraversableLike[Byte, Bytes] with scala.collection.IndexedSeqLike[Byte, Bytes] - with scala.Serializable { + with scala.Serializable + with Ordered[Bytes] { override def newBuilder: mutable.Builder[Byte, Bytes] = new BytesBuilder @@ -55,7 +57,28 @@ case class Bytes(value: Vector[Byte]) case _ => false }) - override def hashCode: Int = value.hashCode + override def hashCode(): Int = value.hashCode() + + private def byte2UnsignedInt(b: Byte): Int = (b.toInt + 256) % 256 + + override def compare(that: Bytes): Int = { + @tailrec + def f(v: Seq[Byte], w: Seq[Byte], i: Int): Int = { + if (v.length == i || w.length == i) { + v.length - w.length + } else if (v(i) == w(i)) { + f(v, w, i + 1) + } else { + byte2UnsignedInt(v(i)) - byte2UnsignedInt(w(i)) + } + } + (this.isInstanceOf[Bytes.MaxValue], that.isInstanceOf[Bytes.MaxValue]) match { + case (true, true) => 0 + case (true, false) => 1 + case (false, true) => -1 + case _ => f(value, that.value, 0) + } + } def newString = new String(value.toArray) } @@ -73,7 +96,11 @@ object Bytes { def fill(n: Int)(elem: => Byte): Bytes = Bytes(Vector.fill(n)(elem)) - lazy val empty = Bytes(Vector.empty[Byte]) + lazy val empty = Bytes(Vector.empty) + + class MaxValue extends Bytes(Vector.empty) + + lazy val MaxValue = new MaxValue } diff --git a/src/main/scala/com/github/mogproject/redismock/entity/SortedSetValue.scala b/src/main/scala/com/github/mogproject/redismock/entity/SortedSetValue.scala new file mode 100644 index 0000000..d650854 --- /dev/null +++ b/src/main/scala/com/github/mogproject/redismock/entity/SortedSetValue.scala @@ -0,0 +1,41 @@ +package com.github.mogproject.redismock.entity + +import scala.collection.{GenTraversableOnce, SortedSet} + + +case class SortedSetValue(value: SORTED_SET.DataType) extends Value { + val valueType = SORTED_SET + + val (data, index) = value + + def size: Int = index.size + + def updated(score: Double, value: Bytes): SortedSetValue = this + (score -> value) + + def +(sv: (Double, Bytes)): SortedSetValue = { + val (score, value) = sv + val newData = subtractData(value) + ((score, value)) + val newIndex = index.updated(value, score) + copy(value = (newData, newIndex)) + } + + def ++(xs: GenTraversableOnce[(Double, Bytes)]): SortedSetValue = xs.foldLeft(this)(_ + _) + + def -(value: Bytes): SortedSetValue = { + copy(value = (subtractData(value), index - value)) + } + + def --(xs: GenTraversableOnce[Bytes]): SortedSetValue = xs.foldLeft(this)(_ - _) + + /** find the 0-indexed rank of the specified value */ + def rank(value: Bytes): Option[Int] = data.zipWithIndex.find(_._1._2 == value).map(_._2) + + private def subtractData(value: Bytes): SortedSet[(Double, Bytes)] = data -- index.get(value).map((_, value)) + +} + +object SortedSetValue { + lazy val empty = new SortedSetValue((SortedSet.empty, Map.empty)) + + def apply(xs: (Double, Bytes)*): SortedSetValue = empty ++ xs +} diff --git a/src/main/scala/com/github/mogproject/redismock/entity/Value.scala b/src/main/scala/com/github/mogproject/redismock/entity/Value.scala index 06e681a..baf0cf6 100644 --- a/src/main/scala/com/github/mogproject/redismock/entity/Value.scala +++ b/src/main/scala/com/github/mogproject/redismock/entity/Value.scala @@ -1,7 +1,5 @@ package com.github.mogproject.redismock.entity -import com.redis.serialization.Format - trait Value { val valueType: ValueType @@ -25,12 +23,3 @@ case class ListValue(value: LIST.DataType) extends Value { val valueType = LIST case class SetValue(value: SET.DataType) extends Value { val valueType = SET } case class HashValue(value: HASH.DataType) extends Value { val valueType = HASH } - - -object StringValue - -object ListValue - -object SetValue - -object HashValue diff --git a/src/main/scala/com/github/mogproject/redismock/entity/ValueType.scala b/src/main/scala/com/github/mogproject/redismock/entity/ValueType.scala index bd78017..cb78f81 100644 --- a/src/main/scala/com/github/mogproject/redismock/entity/ValueType.scala +++ b/src/main/scala/com/github/mogproject/redismock/entity/ValueType.scala @@ -1,5 +1,7 @@ package com.github.mogproject.redismock.entity +import scala.collection.SortedSet + sealed trait ValueType { type DataType } @@ -10,10 +12,8 @@ case object LIST extends ValueType { type DataType = Vector[Bytes] } case object SET extends ValueType { type DataType = Set[Bytes] } -case object SORTED_SET extends ValueType { type DataType = Set[Bytes] } // FIXME +case object SORTED_SET extends ValueType { type DataType = (SortedSet[(Double, Bytes)], Map[Bytes, Double]) } case object HASH extends ValueType { type DataType = Map[Bytes, Bytes] } -case object BITMAP extends ValueType { type DataType = Bytes } // FIXME - case object HYPER_LOG_LOG extends ValueType { type DataType = Bytes } // FIXME diff --git a/src/main/scala/com/github/mogproject/redismock/util/ops/package.scala b/src/main/scala/com/github/mogproject/redismock/util/ops/package.scala index 1700c4b..6c20f5c 100644 --- a/src/main/scala/com/github/mogproject/redismock/util/ops/package.scala +++ b/src/main/scala/com/github/mogproject/redismock/util/ops/package.scala @@ -20,6 +20,8 @@ package object ops { f(self) self } + + final def mapWhenTrue(b: Boolean)(f: A => A): A = if (b) f(self) else self } implicit class BooleanOps(val self: Boolean) extends AnyVal { diff --git a/src/test/scala/com/github/mogproject/redismock/MockSortedSetOperationsSpec.scala b/src/test/scala/com/github/mogproject/redismock/MockSortedSetOperationsSpec.scala new file mode 100644 index 0000000..0712c49 --- /dev/null +++ b/src/test/scala/com/github/mogproject/redismock/MockSortedSetOperationsSpec.scala @@ -0,0 +1,197 @@ +package com.github.mogproject.redismock + +import org.scalatest.FunSpec +import org.scalatest.BeforeAndAfterEach +import org.scalatest.BeforeAndAfterAll +import org.scalatest.Matchers +import com.redis.RedisClient.{DESC, SUM} + + +class MockSortedSetOperationsSpec extends FunSpec +with Matchers +with BeforeAndAfterEach +with BeforeAndAfterAll { + + val r = TestUtil.getRedisClient + + override def beforeEach = { + } + + override def afterEach = { + r.flushdb + } + + override def afterAll = { + r.disconnect + } + + import r._ + + private def add = { + zadd("hackers", 1965, "yukihiro matsumoto") should equal(Some(1)) + zadd("hackers", 1953, "richard stallman", (1916, "claude shannon"), (1969, "linus torvalds"), (1940, "alan kay"), (1912, "alan turing")) should equal(Some(5)) + } + + describe("zadd") { + it("should add based on proper sorted set semantics") { + add + zadd("hackers", 1912, "alan turing") should equal(Some(0)) + zcard("hackers").get should equal(6) + } + } + + describe("zrem") { + it("should remove") { + add + zrem("hackers", "alan turing") should equal(Some(1)) + zrem("hackers", "alan kay", "linus torvalds") should equal(Some(2)) + zrem("hackers", "alan kay", "linus torvalds") should equal(Some(0)) + } + } + + describe("zrange") { + it("should get the proper range") { + add + zrange("hackers").get should have size (6) + zrangeWithScore("hackers").get should have size(6) + } + } + + describe("zrank") { + it ("should give proper rank") { + add + zrank("hackers", "yukihiro matsumoto") should equal(Some(4)) + zrank("hackers", "yukihiro matsumoto", reverse = true) should equal(Some(1)) + } + } + + describe("zremrangebyrank") { + it ("should remove based on rank range") { + add + zremrangebyrank("hackers", 0, 2) should equal(Some(3)) + } + } + + describe("zremrangebyscore") { + it ("should remove based on score range") { + add + zremrangebyscore("hackers", 1912, 1940) should equal(Some(3)) + zremrangebyscore("hackers", 0, 3) should equal(Some(0)) + } + } + + describe("zunion") { + it ("should do a union") { + zadd("hackers 1", 1965, "yukihiro matsumoto") should equal(Some(1)) + zadd("hackers 1", 1953, "richard stallman") should equal(Some(1)) + zadd("hackers 2", 1916, "claude shannon") should equal(Some(1)) + zadd("hackers 2", 1969, "linus torvalds") should equal(Some(1)) + zadd("hackers 3", 1940, "alan kay") should equal(Some(1)) + zadd("hackers 4", 1912, "alan turing") should equal(Some(1)) + + // union with weight = 1 + zunionstore("hackers", List("hackers 1", "hackers 2", "hackers 3", "hackers 4")) should equal(Some(6)) + zcard("hackers") should equal(Some(6)) + + zrangeWithScore("hackers").get.map(_._2) should equal(List(1912, 1916, 1940, 1953, 1965, 1969)) + + // union with modified weights + zunionstoreWeighted("hackers weighted", Map("hackers 1" -> 1.0, "hackers 2" -> 2.0, "hackers 3" -> 3.0, "hackers 4" -> 4.0)) should equal(Some(6)) + zrangeWithScore("hackers weighted").get.map(_._2.toInt) should equal(List(1953, 1965, 3832, 3938, 5820, 7648)) + } + } + + describe("zinter") { + it ("should do an intersection") { + zadd("hackers", 1912, "alan turing") should equal(Some(1)) + zadd("hackers", 1916, "claude shannon") should equal(Some(1)) + zadd("hackers", 1927, "john mccarthy") should equal(Some(1)) + zadd("hackers", 1940, "alan kay") should equal(Some(1)) + zadd("hackers", 1953, "richard stallman") should equal(Some(1)) + zadd("hackers", 1954, "larry wall") should equal(Some(1)) + zadd("hackers", 1956, "guido van rossum") should equal(Some(1)) + zadd("hackers", 1965, "paul graham") should equal(Some(1)) + zadd("hackers", 1965, "yukihiro matsumoto") should equal(Some(1)) + zadd("hackers", 1969, "linus torvalds") should equal(Some(1)) + + zadd("baby boomers", 1948, "phillip bobbit") should equal(Some(1)) + zadd("baby boomers", 1953, "richard stallman") should equal(Some(1)) + zadd("baby boomers", 1954, "cass sunstein") should equal(Some(1)) + zadd("baby boomers", 1954, "larry wall") should equal(Some(1)) + zadd("baby boomers", 1956, "guido van rossum") should equal(Some(1)) + zadd("baby boomers", 1961, "lawrence lessig") should equal(Some(1)) + zadd("baby boomers", 1965, "paul graham") should equal(Some(1)) + zadd("baby boomers", 1965, "yukihiro matsumoto") should equal(Some(1)) + + // intersection with weight = 1 + zinterstore("baby boomer hackers", List("hackers", "baby boomers")) should equal(Some(5)) + zcard("baby boomer hackers") should equal(Some(5)) + + zrange("baby boomer hackers").get should equal(List("richard stallman", "larry wall", "guido van rossum", "paul graham", "yukihiro matsumoto")) + + // intersection with modified weights + zinterstoreWeighted("baby boomer hackers weighted", Map("hackers" -> 0.5, "baby boomers" -> 0.5)) should equal(Some(5)) + zrangeWithScore("baby boomer hackers weighted").get.map(_._2.toInt) should equal(List(1953, 1954, 1956, 1965, 1965)) + } + } + + describe("zcount") { + it ("should return the number of elements between min and max") { + add + + zcount("hackers", 1912, 1920) should equal(Some(2)) + } + } + + describe("zrangebyscore") { + it ("should return the elements between min and max") { + add + + zrangebyscore("hackers", 1940, true, 1969, true, None).get should equal( + List("alan kay", "richard stallman", "yukihiro matsumoto", "linus torvalds")) + + zrangebyscore("hackers", 1940, true, 1969, true, None, DESC).get should equal( + List("linus torvalds", "yukihiro matsumoto", "richard stallman","alan kay")) + } + + it("should return the elements between min and max and allow offset and limit") { + add + + zrangebyscore("hackers", 1940, true, 1969, true, Some(0, 2)).get should equal( + List("alan kay", "richard stallman")) + + zrangebyscore("hackers", 1940, true, 1969, true, Some(0, 2), DESC).get should equal( + List("linus torvalds", "yukihiro matsumoto")) + + zrangebyscore("hackers", 1940, true, 1969, true, Some(3, 1)).get should equal ( + List("linus torvalds")) + + zrangebyscore("hackers", 1940, true, 1969, true, Some(3, 1), DESC).get should equal ( + List("alan kay")) + + zrangebyscore("hackers", 1940, false, 1969, true, Some(0, 2)).get should equal ( + List("richard stallman", "yukihiro matsumoto")) + + zrangebyscore("hackers", 1940, true, 1969, false, Some(0, 2), DESC).get should equal ( + List("yukihiro matsumoto", "richard stallman")) + } + } + + describe("zrangebyscoreWithScore") { + it ("should return the elements between min and max") { + add + + zrangebyscoreWithScore("hackers", 1940, true, 1969, true, None).get should equal( + List(("alan kay", 1940.0), ("richard stallman", 1953.0), ("yukihiro matsumoto", 1965.0), ("linus torvalds", 1969.0))) + + zrangebyscoreWithScore("hackers", 1940, true, 1969, true, None, DESC).get should equal( + List(("linus torvalds", 1969.0), ("yukihiro matsumoto", 1965.0), ("richard stallman", 1953.0),("alan kay", 1940.0))) + + zrangebyscoreWithScore("hackers", 1940, true, 1969, true, Some(3, 1)).get should equal ( + List(("linus torvalds", 1969.0))) + + zrangebyscoreWithScore("hackers", 1940, true, 1969, true, Some(3, 1), DESC).get should equal ( + List(("alan kay", 1940.0))) + } + } +} diff --git a/src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala b/src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala index 088b148..8947bc7 100644 --- a/src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala +++ b/src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala @@ -37,7 +37,23 @@ with BeforeAndAfterAll { } } - describe("Bytes#fiil") { + describe("Bytes#compare") { + it("should return the result of comparison") { + Bytes().compare(Bytes()) shouldBe 0 + Bytes().compare(Bytes(0)) should be < 0 + Bytes(0).compare(Bytes()) should be > 0 + } + it("should compare bits in unsigned") { + Bytes(-1).compare(Bytes(0)) should be > 0 + Bytes(0, 1, 2, 3).compare(Bytes(0, 1, 2, -3)) should be < 0 + } + it("should enable comparison with Bytes") { + Bytes(1, 2, 3) should be < Bytes(1, 2, 4) + Bytes(1, 2, 3) should be > Bytes(1, 2) + } + } + + describe("Bytes#fill") { it("should fill value of the specified number") { Bytes.fill(-1)(3.toByte) shouldBe Bytes.empty Bytes.fill(0)(3.toByte) shouldBe Bytes.empty