From a9e243e46ca310521f592bbccd448690a3bb0c8d Mon Sep 17 00:00:00 2001 From: Kyle Mahan Date: Mon, 4 Jun 2018 10:56:07 -0700 Subject: [PATCH] Feat: Add zrevrangebyscore command (#442) * Add support for zrevrangebyscore * Support optional parameter WITHSCORES for zrangebyscore and zrevrangebyscore --- compat.md | 2 +- src/commands/index.js | 1 + src/commands/zrange-command.common.js | 32 +++++++++++ src/commands/zrangebyscore.js | 43 +++------------ src/commands/zrevrangebyscore.js | 28 ++++++++++ test/commands/zrangebyscore.js | 9 +++- test/commands/zrevrangebyscore.js | 76 +++++++++++++++++++++++++++ 7 files changed, 154 insertions(+), 37 deletions(-) create mode 100644 src/commands/zrevrangebyscore.js create mode 100644 test/commands/zrevrangebyscore.js diff --git a/compat.md b/compat.md index 00052cea2..b1283b4d2 100644 --- a/compat.md +++ b/compat.md @@ -168,7 +168,7 @@ | [zremrangebyscore](http://redis.io/commands/ZREMRANGEBYSCORE) | :white_check_mark: | :x: | | [zrevrange](http://redis.io/commands/ZREVRANGE) | :white_check_mark: | :white_check_mark: | | [zrevrangebylex](http://redis.io/commands/ZREVRANGEBYLEX) | :white_check_mark: | :x: | -| [zrevrangebyscore](http://redis.io/commands/ZREVRANGEBYSCORE) | :white_check_mark: | :x: | +| [zrevrangebyscore](http://redis.io/commands/ZREVRANGEBYSCORE) | :white_check_mark: | :white_check_mark: | | [zrevrank](http://redis.io/commands/ZREVRANK) | :white_check_mark: | :x: | | [zscan](http://redis.io/commands/ZSCAN) | :white_check_mark: | :x: | | [zscore](http://redis.io/commands/ZSCORE) | :white_check_mark: | :x: | diff --git a/src/commands/index.js b/src/commands/index.js index 13e351d4e..811d12a9e 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -97,4 +97,5 @@ export * from './zrange'; export * from './zrangebyscore'; export * from './zremrangebyrank'; export * from './zrevrange'; +export * from './zrevrangebyscore'; export * from './zscan'; diff --git a/src/commands/zrange-command.common.js b/src/commands/zrange-command.common.js index 8b26b9bfb..1bff9e16e 100644 --- a/src/commands/zrange-command.common.js +++ b/src/commands/zrange-command.common.js @@ -17,3 +17,35 @@ export function slice(arr, s, e) { } return arr.slice(start, end + 1); } + +export function parseLimit(input) { + let str = `${input}`; + let strict = false; + if (str[0] === '(') { + str = str.substr(1, str.length); + strict = true; + } else if (str === '-inf') { + return { value: -Infinity, isStrict: true }; + } else if (str === '+inf') { + return { value: +Infinity, isStrict: true }; + } + + return { + value: parseInt(str, 10), + isStrict: strict, + }; +} + +export function filterPredicate(min, max) { + return it => { + if (it.score < min.value || (min.isStrict && it.score === min.value)) { + return false; + } + + if (it.score > max.value || (max.isStrict && it.score === max.value)) { + return false; + } + + return true; + }; +} diff --git a/src/commands/zrangebyscore.js b/src/commands/zrangebyscore.js index 288159767..6c395f24b 100644 --- a/src/commands/zrangebyscore.js +++ b/src/commands/zrangebyscore.js @@ -1,40 +1,9 @@ import Map from 'es6-map'; import arrayFrom from 'array-from'; -import { orderBy, filter } from 'lodash'; +import { orderBy, filter, flatMap } from 'lodash'; +import { parseLimit, filterPredicate } from './zrange-command.common'; -function parseLimit(input) { - let str = `${input}`; - let strict = false; - if (str[0] === '(') { - str = str.substr(1, str.length); - strict = true; - } else if (str === '-inf') { - return { value: -Infinity, isStrict: true }; - } else if (str === '+inf') { - return { value: +Infinity, isStrict: true }; - } - - return { - value: parseInt(str, 10), - isStrict: strict, - }; -} - -function filterPredicate(min, max) { - return it => { - if (it.score < min.value || (min.isStrict && it.score === min.value)) { - return false; - } - - if (it.score > max.value || (max.isStrict && it.score === max.value)) { - return false; - } - - return true; - }; -} - -export function zrangebyscore(key, inputMin, inputMax) { +export function zrangebyscore(key, inputMin, inputMax, withScores) { const map = this.data.get(key); if (!map) { return []; @@ -51,5 +20,9 @@ export function zrangebyscore(key, inputMin, inputMax) { filterPredicate(min, max) ); - return orderBy(filteredArray, 'score').map(it => it.value); + const ordered = orderBy(filteredArray, 'score'); + if (withScores === 'WITHSCORES') { + return flatMap(ordered, it => [it.value, it.score]); + } + return ordered.map(it => it.value); } diff --git a/src/commands/zrevrangebyscore.js b/src/commands/zrevrangebyscore.js new file mode 100644 index 000000000..d0f699ddc --- /dev/null +++ b/src/commands/zrevrangebyscore.js @@ -0,0 +1,28 @@ +import Map from 'es6-map'; +import arrayFrom from 'array-from'; +import { orderBy, filter, flatMap } from 'lodash'; +import { parseLimit, filterPredicate } from './zrange-command.common'; + +export function zrevrangebyscore(key, inputMax, inputMin, withScores) { + const map = this.data.get(key); + if (!map) { + return []; + } + + if (this.data.has(key) && !(this.data.get(key) instanceof Map)) { + return []; + } + + const min = parseLimit(inputMin); + const max = parseLimit(inputMax); + const filteredArray = filter( + arrayFrom(map.values()), + filterPredicate(min, max) + ); + + const ordered = orderBy(filteredArray, 'score', 'desc'); + if (withScores === 'WITHSCORES') { + return flatMap(ordered, it => [it.value, it.score]); + } + return ordered.map(it => it.value); +} diff --git a/test/commands/zrangebyscore.js b/test/commands/zrangebyscore.js index 3572b43ca..eb8bdfc27 100644 --- a/test/commands/zrangebyscore.js +++ b/test/commands/zrangebyscore.js @@ -21,7 +21,7 @@ describe('zrangebyscore', () => { .then(res => expect(res).toEqual(['first', 'second', 'third'])); }); - it('should can using strict compare', () => { + it('should return using strict compare', () => { const redis = new MockRedis({ data }); return redis @@ -66,4 +66,11 @@ describe('zrangebyscore', () => { .zrangebyscore('foo', 1, 2) .then(res => expect(res).toEqual([])); }); + + it('should include scores if WITHSCORES is specified', () => { + const redis = new MockRedis({ data }); + return redis + .zrangebyscore('foo', 1, 3, 'WITHSCORES') + .then(res => expect(res).toEqual(['first', 1, 'second', 2, 'third', 3])); + }); }); diff --git a/test/commands/zrevrangebyscore.js b/test/commands/zrevrangebyscore.js new file mode 100644 index 000000000..2b267dd80 --- /dev/null +++ b/test/commands/zrevrangebyscore.js @@ -0,0 +1,76 @@ +import Map from 'es6-map'; +import expect from 'expect'; + +import MockRedis from '../../src'; + +describe('zrevrangebyscore', () => { + const data = { + foo: new Map([ + ['first', { score: 1, value: 'first' }], + ['second', { score: 2, value: 'second' }], + ['third', { score: 3, value: 'third' }], + ['fourth', { score: 4, value: 'fourth' }], + ['fifth', { score: 5, value: 'fifth' }], + ]), + }; + it('should return using not strict compare', () => { + const redis = new MockRedis({ data }); + + return redis + .zrevrangebyscore('foo', 3, 1) + .then(res => expect(res).toEqual(['third', 'second', 'first'])); + }); + + it('should return using strict compare', () => { + const redis = new MockRedis({ data }); + + return redis + .zrevrangebyscore('foo', 5, '(3') + .then(res => expect(res).toEqual(['fifth', 'fourth'])); + }); + + it('should accept infinity string', () => { + const redis = new MockRedis({ data }); + + return redis + .zrevrangebyscore('foo', '+inf', '-inf') + .then(res => + expect(res).toEqual(['fifth', 'fourth', 'third', 'second', 'first']) + ); + }); + + it('should return empty array if out-of-range', () => { + const redis = new MockRedis({ data }); + + return redis + .zrevrangebyscore('foo', 100, 10) + .then(res => expect(res).toEqual([])); + }); + + it('should return empty array if key not found', () => { + const redis = new MockRedis({ data }); + + return redis + .zrevrangebyscore('boo', 100, 10) + .then(res => expect(res).toEqual([])); + }); + + it('should return empty array if the key contains something other than a list', () => { + const redis = new MockRedis({ + data: { + foo: 'not a list', + }, + }); + + return redis + .zrevrangebyscore('foo', 2, 1) + .then(res => expect(res).toEqual([])); + }); + + it('should include scores if WITHSCORES is specified', () => { + const redis = new MockRedis({ data }); + return redis + .zrevrangebyscore('foo', 3, 1, 'WITHSCORES') + .then(res => expect(res).toEqual(['third', 3, 'second', 2, 'first', 1])); + }); +});