Skip to content

Commit

Permalink
feat: zrangebylex and zrevrangebylex (#1269)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoreni authored Apr 11, 2023
1 parent fd318cd commit 08d6c98
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,14 @@ export * from './zinterstore'
export * from './zpopmax'
export * from './zpopmin'
export * from './zrange'
export * from './zrangebylex'
export * from './zrangebyscore'
export * from './zrank'
export * from './zrem'
export * from './zremrangebyrank'
export * from './zremrangebyscore'
export * from './zrevrange'
export * from './zrevrangebylex'
export * from './zrevrangebyscore'
export * from './zrevrank'
export * from './zscan'
Expand Down
84 changes: 64 additions & 20 deletions src/commands/zrange-command.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,34 @@ export function parseLimit(input) {
}
}

function parseLexLimit(input) {
if (input === '-') {
return { value: '-', isExclusive: false }
}

if (input === '+') {
return { value: '+', isExclusive: false }
}

let str = input
let exclusive = false

if (str[0] === '(') {
str = str.substr(1, str.length)
exclusive = true
} else if (str[0] === '[') {
str = str.substr(1, str.length)
} else {
// [ or ( are required
throw new Error('ERR synax error')
}

return {
value: str,
isExclusive: exclusive,
}
}

export function filterPredicate(min, max) {
return it => {
if (it.score < min.value || (min.isStrict && it.score === min.value)) {
Expand All @@ -51,6 +79,26 @@ export function filterPredicate(min, max) {
}
}

export function filterLexPredicate(min, max) {
return it => {
if (
(min.value !== '-' && it.value < min.value) ||
(min.isExclusive && it.value === min.value)
) {
return false
}

if (
(max.value !== '+' && it.value > max.value) ||
(max.isExclusive && it.value === max.value)
) {
return false
}

return true
}
}

function streq(a, b) {
return a.toString().toLowerCase() === b.toString().toLowerCase()
}
Expand Down Expand Up @@ -157,15 +205,13 @@ export function zrangeBaseCommand(
end = parseLimit(args[maxIdx])

break
// FIXME: handle RANGE_LEX
// case ZRANGE_LEX:
// /* Z[REV]RANGEBYLEX, ZRANGESTORE [REV]RANGEBYLEX */
// if (zslParseLexRange(c->argv[minidx], c->argv[maxidx], &lexrange) != C_OK) {
// addReplyError(c, "min or max not valid string range item");
// return;
// }
// break;
// }

case RANGE_LEX:
/* Z[REV]RANGEBYLEX, ZRANGESTORE [REV]RANGEBYLEX */
start = parseLexLimit(args[minIdx])
end = parseLexLimit(args[maxIdx])

break
default:
throw new Error('ERR syntax error')
}
Expand All @@ -177,22 +223,20 @@ export function zrangeBaseCommand(
}

let ordered
const inputArray = Array.from(map.values())
if (range === RANGE_SCORE) {
const filteredArray = Array.from(map.values()).filter(
filterPredicate(start, end)
)
// TODO If items have different scores, result is unspecified

const filteredArray = inputArray.filter(filterPredicate(start, end))
ordered = orderBy(filteredArray, ['score', 'value'], sort)
}
// FIXME: handle RANGE_LEX
else {
ordered = slice(
orderBy(Array.from(map.values()), ['score', 'value'], sort),
start,
end
)
} else if (range === RANGE_LEX) {
const filteredArray = inputArray.filter(filterLexPredicate(start, end))
ordered = orderBy(filteredArray, ['score', 'value'], sort)
} else {
ordered = slice(orderBy(inputArray, ['score', 'value'], sort), start, end)
}

// TODO: handle STORE
if (limit !== null) {
ordered = offsetAndLimit(ordered, offset, limit)
}
Expand Down
14 changes: 14 additions & 0 deletions src/commands/zrangebylex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { convertStringToBuffer } from '../commands-utils/convertStringToBuffer'
import {
RANGE_LEX,
zrangeBaseCommand,
} from './zrange-command.common'

export function zrangebylex(...args) {
return zrangeBaseCommand.call(this, args, 0, false, RANGE_LEX);
}

export function zrangebylexBuffer(...args) {
const val = zrangebylex.apply(this, args)
return convertStringToBuffer(val)
}
15 changes: 15 additions & 0 deletions src/commands/zrevrangebylex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { convertStringToBuffer } from '../commands-utils/convertStringToBuffer'
import {
DIRECTION_REVERSE,
RANGE_LEX,
zrangeBaseCommand,
} from './zrange-command.common'

export function zrevrangebylex(...args) {
return zrangeBaseCommand.call(this, args, 0, false, RANGE_LEX, DIRECTION_REVERSE);
}

export function zrevrangebylexBuffer(...args) {
const val = zrevrangebylex.apply(this, args)
return convertStringToBuffer(val)
}
42 changes: 42 additions & 0 deletions test/integration/commands/zrange.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,46 @@ describe('zrange', () => {
.zrange('foo', '3', '1', 'BYSCORE', 'REV', 'LIMIT', 0, 1, 'WITHSCORES')
.then(res => expect(res).toEqual(['third', '3']))
})

const lexData = {
foo: new Map([
['a', { score: 100, value: 'a' }],
['b', { score: 100, value: 'b' }],
['c', { score: 100, value: 'c' }],
['d', { score: 100, value: 'd' }],
['e', { score: 100, value: 'e' }],
]),
}

it('should handle BYLEX', () => {
const redis = new Redis({ data: lexData })

return redis
.zrange('foo', '[b', '(d', 'BYLEX')
.then(res => expect(res).toEqual(['b', 'c']))
})

it('should handle BYLEX with LIMIT', () => {
const redis = new Redis({ data: lexData })

return redis
.zrange('foo', '[b', '(d', 'BYLEX', 'LIMIT', 1, 2)
.then(res => expect(res).toEqual(['c']))
})

it('should handle BYLEX REV', () => {
const redis = new Redis({ data: lexData })

return redis
.zrange('foo', '[c', '[a', 'BYLEX', 'REV')
.then(res => expect(res).toEqual(['c', 'b', 'a']))
})

it('should handle BYLEX REV with LIMIT', () => {
const redis = new Redis({ data: lexData })

return redis
.zrange('foo', '[c', '[a', 'BYLEX', 'REV', 'LIMIT', 0, 1)
.then(res => expect(res).toEqual(['c']))
})
})
111 changes: 111 additions & 0 deletions test/integration/commands/zrangebylex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import Redis from 'ioredis'

describe('zrangebylex', () => {
const data = {
foo: new Map([
['a', { score: 2, value: 'a' }],
['b', { score: 2, value: 'b' }],
['c', { score: 2, value: 'c' }],
['d', { score: 2, value: 'd' }],
['e', { score: 2, value: 'e' }],
]),
}
it('should return using inclusive compare', () => {
const redis = new Redis({ data })

return redis
.zrangebylex('foo', '[b', '[d')
.then(res => expect(res).toEqual(['b', 'c', 'd']))
})

it('should return using exclusive compare', () => {
const redis = new Redis({ data })

return redis
.zrangebylex('foo', '(b', '(d')
.then(res => expect(res).toEqual(['c']))
})

it('should accept - string', () => {
const redis = new Redis({ data })

return redis
.zrangebylex('foo', '-', '(c')
.then(res => expect(res).toEqual(['a', 'b']))
})
it('should accept + string', () => {
const redis = new Redis({ data })

return redis
.zrangebylex('foo', '(c', '+')
.then(res => expect(res).toEqual(['d', 'e']))
})

it('should accept -+ strings', () => {
const redis = new Redis({ data })

return redis
.zrangebylex('foo', '-', '+')
.then(res =>
expect(res).toEqual(['a', 'b', 'c', 'd', 'e'])
)
})
it('should return empty array if out-of-range', () => {
const redis = new Redis({ data })

return redis
.zrangebylex('foo', '(f', '[z')
.then(res => expect(res).toEqual([]))
})

it('should return empty array if key not found', () => {
const redis = new Redis({ data })

return redis
.zrangebylex('boo', '-', '+')
.then(res => expect(res).toEqual([]))
})

it('should return empty array if the key contains something other than a list', () => {
const redis = new Redis({
data: {
foo: 'not a list',
},
})

return redis
.zrangebylex('foo', '-', '+')
.then(res => expect(res).toEqual([]))
})

it('should handle offset and limit (0,1)', () => {
const redis = new Redis({ data })
return redis
.zrangebylex('foo', '[a', '[c', 'LIMIT', 0, 1)
.then(res => expect(res).toEqual(['a']))
})
it('should handle offset and limit (1,2)', () => {
const redis = new Redis({ data })
return redis
.zrangebylex('foo', '[a', '[c', 'LIMIT', 1, 2)
.then(res => expect(res).toEqual(['b', 'c']))
})
it('should handle LIMIT of -1', () => {
const redis = new Redis({ data })
return redis
.zrangebylex('foo', '-', '+', 'LIMIT', 1, -1)
.then(res => expect(res).toEqual(['b', 'c', 'd']))
})
it('should handle LIMIT of -2', () => {
const redis = new Redis({ data })
return redis
.zrangebylex('foo', '-', '+', 'LIMIT', 1, -2)
.then(res => expect(res).toEqual(['b', 'c']))
})
it('should handle LIMIT of 0', () => {
const redis = new Redis({ data })
return redis
.zrangebylex('foo', '-', '+', 'LIMIT', 1, 0)
.then(res => expect(res).toEqual([]))
})
})
Loading

0 comments on commit 08d6c98

Please sign in to comment.