Skip to content

Commit

Permalink
Node: Add LCS command. (valkey-io#2049)
Browse files Browse the repository at this point in the history
* Add `LCS` command.

Signed-off-by: Yury-Fridlyand <[email protected]>
  • Loading branch information
Yury-Fridlyand authored Jul 31, 2024
1 parent 33e30e9 commit 1a898cf
Show file tree
Hide file tree
Showing 8 changed files with 397 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#### Changes
* Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049))
* Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046))
* Node: Added BLMOVE command ([#2027](https://github.com/valkey-io/valkey-glide/pull/2027))
* Node: Exported client configuration types ([#2023](https://github.com/valkey-io/valkey-glide/pull/2023))
Expand Down
4 changes: 2 additions & 2 deletions node/npm/glide/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ function initialize() {
ListDirection,
ExpireOptions,
FlushMode,
GeoUnit,
InfoOptions,
InsertPosition,
SetOptions,
Expand All @@ -138,6 +137,7 @@ function initialize() {
ConfigurationError,
ExecAbortError,
RedisError,
ReturnType,
RequestError,
TimeoutError,
ConnectionError,
Expand Down Expand Up @@ -199,7 +199,6 @@ function initialize() {
ListDirection,
ExpireOptions,
FlushMode,
GeoUnit,
InfoOptions,
InsertPosition,
SetOptions,
Expand All @@ -221,6 +220,7 @@ function initialize() {
ConfigurationError,
ExecAbortError,
RedisError,
ReturnType,
RequestError,
TimeoutError,
ConnectionError,
Expand Down
113 changes: 108 additions & 5 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
GeoUnit,
GeospatialData,
InsertPosition,
KeyWeight,
KeyWeight, // eslint-disable-line @typescript-eslint/no-unused-vars
LPosOptions,
ListDirection,
MemberOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -84,6 +84,7 @@ import {
createIncr,
createIncrBy,
createIncrByFloat,
createLCS,
createLIndex,
createLInsert,
createLLen,
Expand Down Expand Up @@ -3940,11 +3941,8 @@ export class BaseClient {
* where each sub-array represents a single item in the following order:
*
* - The member (location) name.
*
* - The distance from the center as a floating point `number`, in the same unit specified for `searchBy`, if `withDist` is set to `true`.
*
* - The geohash of the location as a integer `number`, if `withHash` is set to `true`.
*
* - The coordinates as a two item `array` of floating point `number`s, if `withCoord` is set to `true`.
*
* @example
Expand Down Expand Up @@ -4173,7 +4171,7 @@ export class BaseClient {
* See https://valkey.io/commands/geohash/ for more details.
*
* @param key - The key of the sorted set.
* @param members - The array of members whose <code>GeoHash</code> strings are to be retrieved.
* @param members - The array of members whose `GeoHash` strings are to be retrieved.
* @returns An array of `GeoHash` strings representing the positions of the specified members stored at `key`.
* If a member does not exist in the sorted set, a `null` value is returned for that member.
*
Expand All @@ -4191,6 +4189,111 @@ export class BaseClient {
);
}

/**
* Returns all the longest common subsequences combined between strings stored at `key1` and `key2`.
*
* since Valkey version 7.0.0.
*
* @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot.
*
* See https://valkey.io/commands/lcs/ for more details.
*
* @param key1 - The key that stores the first string.
* @param key2 - The key that stores the second string.
* @returns A `String` containing all the longest common subsequence combined between the 2 strings.
* An empty `String` is returned if the keys do not exist or have no common subsequences.
*
* @example
* ```typescript
* await client.mset({"testKey1": "abcd", "testKey2": "axcd"});
* const result = await client.lcs("testKey1", "testKey2");
* console.log(result); // Output: 'cd'
* ```
*/
public async lcs(key1: string, key2: string): Promise<string> {
return this.createWritePromise(createLCS(key1, key2));
}

/**
* Returns the total length of all the longest common subsequences between strings stored at `key1` and `key2`.
*
* since Valkey version 7.0.0.
*
* @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot.
*
* See https://valkey.io/commands/lcs/ for more details.
*
* @param key1 - The key that stores the first string.
* @param key2 - The key that stores the second string.
* @returns The total length of all the longest common subsequences between the 2 strings.
*
* @example
* ```typescript
* await client.mset({"testKey1": "abcd", "testKey2": "axcd"});
* const result = await client.lcsLen("testKey1", "testKey2");
* console.log(result); // Output: 2
* ```
*/
public async lcsLen(key1: string, key2: string): Promise<number> {
return this.createWritePromise(createLCS(key1, key2, { len: true }));
}

/**
* Returns the indices and lengths of the longest common subsequences between strings stored at
* `key1` and `key2`.
*
* since Valkey version 7.0.0.
*
* @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot.
*
* See https://valkey.io/commands/lcs/ for more details.
*
* @param key1 - The key that stores the first string.
* @param key2 - The key that stores the second string.
* @param withMatchLen - (Optional) If `true`, include the length of the substring matched for the each match.
* @param minMatchLen - (Optional) The minimum length of matches to include in the result.
* @returns A `Record` containing the indices of the longest common subsequences between the
* 2 strings and the lengths of the longest common subsequences. The resulting map contains two
* keys, "matches" and "len":
* - `"len"` is mapped to the total length of the all longest common subsequences between the 2 strings
* stored as an integer. This value doesn't count towards the `minMatchLen` filter.
* - `"matches"` is mapped to a three dimensional array of integers that stores pairs
* of indices that represent the location of the common subsequences in the strings held
* by `key1` and `key2`.
*
* @example
* ```typescript
* await client.mset({"key1": "ohmytext", "key2": "mynewtext"});
* const result = await client.lcsIdx("key1", "key2");
* console.log(result); // Output:
* {
* "matches" :
* [
* [ // first substring match is "text"
* [4, 7], // in `key1` it is located between indices 4 and 7
* [5, 8], // and in `key2` - in between 5 and 8
* 4 // the match length, returned if `withMatchLen` set to `true`
* ],
* [ // second substring match is "my"
* [2, 3], // in `key1` it is located between indices 2 and 3
* [0, 1], // and in `key2` - in between 0 and 1
* 2 // the match length, returned if `withMatchLen` set to `true`
* ]
* ],
* "len" : 6 // total length of the all matches found
* }
* ```
*/
public async lcsIdx(
key1: string,
key2: string,
options?: { withMatchLen?: boolean; minMatchLen?: number },
): Promise<Record<string, (number | [number, number])[][] | number>> {
return this.createWritePromise(
createLCS(key1, key2, { idx: options ?? {} }),
);
}

/**
* @internal
*/
Expand Down
24 changes: 24 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2776,3 +2776,27 @@ export function createZRandMember(

return createCommand(RequestType.ZRandMember, args);
}

/** @internal */
export function createLCS(
key1: string,
key2: string,
options?: {
len?: boolean;
idx?: { withMatchLen?: boolean; minMatchLen?: number };
},
): command_request.Command {
const args = [key1, key2];

if (options) {
if (options.len) args.push("LEN");
else if (options.idx) {
args.push("IDX");
if (options.idx.withMatchLen) args.push("WITHMATCHLEN");
if (options.idx.minMatchLen !== undefined)
args.push("MINMATCHLEN", options.idx.minMatchLen.toString());
}
}

return createCommand(RequestType.LCS, args);
}
69 changes: 65 additions & 4 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import {
createIncrBy,
createIncrByFloat,
createInfo,
createLCS,
createLIndex,
createLInsert,
createLLen,
Expand Down Expand Up @@ -2364,11 +2365,8 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
* where each sub-array represents a single item in the following order:
*
* - The member (location) name.
*
* - The distance from the center as a floating point `number`, in the same unit specified for `searchBy`.
*
* - The geohash of the location as a integer `number`.
*
* - The coordinates as a two item `array` of floating point `number`s.
*/
public geosearch(
Expand Down Expand Up @@ -2496,14 +2494,77 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
* See https://valkey.io/commands/geohash/ for more details.
*
* @param key - The key of the sorted set.
* @param members - The array of members whose <code>GeoHash</code> strings are to be retrieved.
* @param members - The array of members whose `GeoHash` strings are to be retrieved.
*
* Command Response - An array of `GeoHash` strings representing the positions of the specified members stored at `key`.
* If a member does not exist in the sorted set, a `null` value is returned for that member.
*/
public geohash(key: string, members: string[]): T {
return this.addAndReturn(createGeoHash(key, members));
}

/**
* Returns all the longest common subsequences combined between strings stored at `key1` and `key2`.
*
* since Valkey version 7.0.0.
*
* See https://valkey.io/commands/lcs/ for more details.
*
* @param key1 - The key that stores the first string.
* @param key2 - The key that stores the second string.
*
* Command Response - A `String` containing all the longest common subsequence combined between the 2 strings.
* An empty `String` is returned if the keys do not exist or have no common subsequences.
*/
public lcs(key1: string, key2: string): T {
return this.addAndReturn(createLCS(key1, key2));
}

/**
* Returns the total length of all the longest common subsequences between strings stored at `key1` and `key2`.
*
* since Valkey version 7.0.0.
*
* See https://valkey.io/commands/lcs/ for more details.
*
* @param key1 - The key that stores the first string.
* @param key2 - The key that stores the second string.
*
* Command Response - The total length of all the longest common subsequences between the 2 strings.
*/
public lcsLen(key1: string, key2: string): T {
return this.addAndReturn(createLCS(key1, key2, { len: true }));
}

/**
* Returns the indices and lengths of the longest common subsequences between strings stored at
* `key1` and `key2`.
*
* since Valkey version 7.0.0.
*
* See https://valkey.io/commands/lcs/ for more details.
*
* @param key1 - The key that stores the first string.
* @param key2 - The key that stores the second string.
* @param withMatchLen - (Optional) If `true`, include the length of the substring matched for the each match.
* @param minMatchLen - (Optional) The minimum length of matches to include in the result.
*
* Command Response - A `Record` containing the indices of the longest common subsequences between the
* 2 strings and the lengths of the longest common subsequences. The resulting map contains two
* keys, "matches" and "len":
* - `"len"` is mapped to the total length of the all longest common subsequences between the 2 strings
* stored as an integer. This value doesn't count towards the `minMatchLen` filter.
* - `"matches"` is mapped to a three dimensional array of integers that stores pairs
* of indices that represent the location of the common subsequences in the strings held
* by `key1` and `key2`.
*/
public lcsIdx(
key1: string,
key2: string,
options?: { withMatchLen?: boolean; minMatchLen?: number },
): T {
return this.addAndReturn(createLCS(key1, key2, { idx: options ?? {} }));
}
}

/**
Expand Down
11 changes: 4 additions & 7 deletions node/tests/RedisClusterClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,17 +338,14 @@ describe("GlideClusterClient", () => {
client.zintercard(["abc", "zxy", "lkn"]),
client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX),
client.bzmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX, 0.1),
client.lcs("abc", "xyz"),
client.lcsLen("abc", "xyz"),
client.lcsIdx("abc", "xyz"),
);
}

for (const promise of promises) {
try {
await promise;
} catch (e) {
expect((e as Error).message.toLowerCase()).toContain(
"crossslot",
);
}
await expect(promise).rejects.toThrowError(/crossslot/i);
}

client.close();
Expand Down
Loading

0 comments on commit 1a898cf

Please sign in to comment.