From 49a3b11ffa187012e3fc18cdf181a07cbf9ccdbc Mon Sep 17 00:00:00 2001 From: Max R Date: Fri, 29 Sep 2023 06:53:58 -0400 Subject: [PATCH] feat: add zdiff (#1312) --- compat.md | 2 +- src/commands/index.js | 1 + src/commands/zdiff.js | 49 ++++++++++++ test/integration/commands/zdiff.js | 118 +++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/commands/zdiff.js create mode 100644 test/integration/commands/zdiff.js diff --git a/compat.md b/compat.md index d5f18556..6846a859 100644 --- a/compat.md +++ b/compat.md @@ -202,7 +202,7 @@ | [zadd] | :white_check_mark: | :white_check_mark: | | [zcard] | :white_check_mark: | :white_check_mark: | | [zcount] | :white_check_mark: | :white_check_mark: | -| [zdiff] | :white_check_mark: | :x: | +| [zdiff] | :white_check_mark: | :white_check_mark: | | [zdiffstore] | :white_check_mark: | :x: | | [zincrby] | :white_check_mark: | :white_check_mark: | | [zinter] | :white_check_mark: | :x: | diff --git a/src/commands/index.js b/src/commands/index.js index 8297e9a3..f39b2a24 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -130,6 +130,7 @@ export * from './xrevrange' export * from './zadd' export * from './zcard' export * from './zcount' +export * from './zdiff' export * from './zincrby' export * from './zinterstore' export * from './zpopmax' diff --git a/src/commands/zdiff.js b/src/commands/zdiff.js new file mode 100644 index 00000000..db7bc462 --- /dev/null +++ b/src/commands/zdiff.js @@ -0,0 +1,49 @@ +import { convertStringToBuffer } from '../commands-utils/convertStringToBuffer' +import { zrange } from './zrange' +import { zscore } from './zscore' + +export function zdiff(numkeys, ...vals) { + if (vals.length === 0) { + throw new Error("ERR wrong number of arguments for 'zdiff' command") + } + + const [keys, withScores] = + vals[vals.length - 1] === 'WITHSCORES' + ? [vals.slice(0, -1), true] + : [vals, false] + + if ( + (Number.isInteger(numkeys) ? numkeys : parseInt(numkeys, 10)) !== + keys.length + ) { + throw new Error( + 'ERR numkeys must match the number of keys. ' + + `numkeys===${numkeys} keys but got ${keys.length} keys` + ) + } + + const firstKeyMembers = zrange.call(this, keys[0], 0, -1) + const otherMembers = new Set( + keys + .slice(1) + .map(key => zrange.call(this, key, 0, -1)) + .flat() + ) + + const diffedMembers = firstKeyMembers.filter( + member => !otherMembers.has(member) + ) + + if (!withScores) { + return diffedMembers + } + + return diffedMembers + .map(member => [member, zscore.call(this, keys[0], member)]) + .flat() +} + +export function zdiffBuffer(numkeys, ...vals) { + const val = zdiff.apply(this, numkeys, vals) + return convertStringToBuffer(val) +} diff --git a/test/integration/commands/zdiff.js b/test/integration/commands/zdiff.js new file mode 100644 index 00000000..4f432b19 --- /dev/null +++ b/test/integration/commands/zdiff.js @@ -0,0 +1,118 @@ +import Redis from 'ioredis' + +// eslint-disable-next-line import/no-relative-parent-imports +import { runTwinSuite } from '../../../test-utils' + +runTwinSuite('zdiff', command => { + describe(command, () => { + const redis = new Redis() + + afterAll(() => { + redis.disconnect() + }) + + it('throws if not enough keys', () => { + redis + .zdiff(1, 'key1') + .catch(err => + expect(err.toString()).toContain( + "ERR wrong number of arguments for 'zdiff' command" + ) + ) + }) + + it('throws if not enough keys with scores', () => { + redis + .zdiff(1, 'key1', 'WITHSCORES') + .catch(err => + expect(err.toString()).toContain( + "ERR wrong number of arguments for 'zdiff' command" + ) + ) + }) + + it('throws if numkeys is wrong', () => { + redis + .zdiff(1, 'key1', 'key2') + .catch(err => + expect(err.toString()).toContain( + 'ERR numkeys must match the number of keys' + ) + ) + redis + .zdiff(2, 'key1', 'key2') + .catch(err => + expect(err.toString()).toContain( + 'ERR numkeys must match the number of keys' + ) + ) + }) + + it('throws if numkeys is wrong with scores', () => { + redis + .zdiff(1, 'key1', 'key2', 'WITHSCORES') + .catch(err => + expect(err.toString()).toContain( + 'ERR numkeys must match the number of keys' + ) + ) + redis + .zdiff(2, 'key1', 'key2', 'WITHSCORES') + .catch(err => + expect(err.toString()).toContain( + 'ERR numkeys must match the number of keys' + ) + ) + }) + + it('should return diff between two keys', async () => { + await redis.zadd('key1', 1, 'a', 2, 'b', 3, 'c', 4, 'd') + await redis.zadd('key2', 3, 'a', 4, 'd') + + const members = await redis.zdiff(2, 'key1', 'key2') + expect(members).toEqual(['b', 'c']) + }) + + it('should return diff between two keys with scores', async () => { + await redis.zadd('key1', 1, 'a', 2, 'b', 3, 'c', 4, 'd') + await redis.zadd('key2', 3, 'a', 4, 'd') + + const members = await redis.zdiff(2, 'key1', 'key2', 'WITHSCORES') + expect(members).toEqual(['b', '2', 'c', '3']) + }) + + it('should return diff between two keys with no overlap', async () => { + await redis.zadd('key1', 1, 'a', 2, 'b', 3, 'c', 4, 'd') + await redis.zadd('key2', 3, 'e', 4, 'f') + + const members = await redis.zdiff(2, 'key1', 'key2') + expect(members).toEqual(['a', 'b', 'c', 'd']) + }) + + it('should return diff between two keys with no overlap with scores', async () => { + await redis.zadd('key1', 1, 'a', 2, 'b', 3, 'c', 4, 'd') + await redis.zadd('key2', 3, 'e', 4, 'f') + + const members = await redis.zdiff(2, 'key1', 'key2', 'WITHSCORES') + expect(members).toEqual(['a', '1', 'b', '2', 'c', '3', 'd', '4']) + }) + + it('should return diff between three keys', async () => { + await redis.zadd('key1', 1, 'a', 2, 'b', 3, 'c', 4, 'd') + await redis.zadd('key2', 3, 'a', 4, 'd') + await redis.zadd('key2', 5, 'a', 6, 'c') + + const members = await redis.zdiff(3, 'key1', 'key2', 'key3') + expect(members).toEqual(['b']) + }) + + it('should return diff between three keys with scores', async () => { + await redis.zadd('key1', 1, 'a', 2, 'b', 3, 'c', 4, 'd') + await redis.zadd('key2', 3, 'a', 4, 'd') + await redis.zadd('key2', 5, 'a', 6, 'c') + + const members = await redis.zdiff(3, 'key1', 'key2', 'key3', 'WITHSCORES') + expect(members).toEqual(['b', '2']) + }) + }) +})