Skip to content

Commit

Permalink
Feat: Add zrevrangebyscore command (stipsan#442)
Browse files Browse the repository at this point in the history
* Add support for zrevrangebyscore
* Support optional parameter WITHSCORES for zrangebyscore and zrevrangebyscore
  • Loading branch information
kylewm authored and stipsan committed Jun 4, 2018
1 parent a696f5c commit a9e243e
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 37 deletions.
2 changes: 1 addition & 1 deletion compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
1 change: 1 addition & 0 deletions src/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,5 @@ export * from './zrange';
export * from './zrangebyscore';
export * from './zremrangebyrank';
export * from './zrevrange';
export * from './zrevrangebyscore';
export * from './zscan';
32 changes: 32 additions & 0 deletions src/commands/zrange-command.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}
43 changes: 8 additions & 35 deletions src/commands/zrangebyscore.js
Original file line number Diff line number Diff line change
@@ -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 [];
Expand All @@ -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);
}
28 changes: 28 additions & 0 deletions src/commands/zrevrangebyscore.js
Original file line number Diff line number Diff line change
@@ -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);
}
9 changes: 8 additions & 1 deletion test/commands/zrangebyscore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]));
});
});
76 changes: 76 additions & 0 deletions test/commands/zrevrangebyscore.js
Original file line number Diff line number Diff line change
@@ -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]));
});
});

0 comments on commit a9e243e

Please sign in to comment.