Skip to content

Commit

Permalink
Java: Add LCS command (with IDX option) (#1660)
Browse files Browse the repository at this point in the history
* Java: Add `LCS` command (with IDX option) (#386)

* Implemented LCS with IDX

* TODO: add docs and more integTests

* Added docs and remaining tests

* Addressed comments

* Fixed rust formatting

* Addressed comments

* Added WITHMATCHLEN apis

* Expanded on example

* Fixed rust ci failure

* Removed LcsOptions

* Improved examples in docs

* Examples with different match lengths

* Throw NPE if matches is not present

* Resolved conflicts
  • Loading branch information
GumpacG authored Jun 26, 2024
1 parent 1453910 commit b969cd9
Show file tree
Hide file tree
Showing 10 changed files with 767 additions and 2 deletions.
18 changes: 18 additions & 0 deletions glide-core/src/client/value_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub(crate) enum ExpectedReturnType<'a> {
KeyWithMemberAndScore,
FunctionStatsReturnType,
GeoSearchReturnType,
SimpleString,
}

pub(crate) fn convert_to_expected_type(
Expand Down Expand Up @@ -141,6 +142,9 @@ pub(crate) fn convert_to_expected_type(
ExpectedReturnType::BulkString => Ok(Value::BulkString(
from_owned_redis_value::<String>(value)?.into(),
)),
ExpectedReturnType::SimpleString => Ok(Value::SimpleString(
from_owned_redis_value::<String>(value)?,
)),
ExpectedReturnType::JsonToggleReturnType => match value {
Value::Array(array) => {
let converted_array: RedisResult<Vec<_>> = array
Expand Down Expand Up @@ -860,6 +864,10 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option<ExpectedReturnType> {
value_type: &Some(ExpectedReturnType::ArrayOfPairs),
}),
}),
b"LCS" => cmd.position(b"IDX").map(|_| ExpectedReturnType::Map {
key_type: &Some(ExpectedReturnType::SimpleString),
value_type: &None,
}),
b"INCRBYFLOAT" | b"HINCRBYFLOAT" | b"ZINCRBY" => Some(ExpectedReturnType::Double),
b"HEXISTS"
| b"HSETNX"
Expand Down Expand Up @@ -2375,4 +2383,14 @@ mod tests {

assert!(expected_type_for_cmd(redis::cmd("GEOSEARCH").arg("key")).is_none());
}
#[test]
fn convert_lcs_idx() {
assert!(matches!(
expected_type_for_cmd(redis::cmd("LCS").arg("key1").arg("key2").arg("IDX")),
Some(ExpectedReturnType::Map {
key_type: &Some(ExpectedReturnType::SimpleString),
value_type: &None,
})
));
}
}
57 changes: 57 additions & 0 deletions java/client/src/main/java/glide/api/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands;
import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs;
import static glide.ffi.resolvers.SocketListenerResolver.getSocket;
import static glide.utils.ArrayTransformUtils.cast3DArray;
import static glide.utils.ArrayTransformUtils.castArray;
import static glide.utils.ArrayTransformUtils.castArrayofArrays;
import static glide.utils.ArrayTransformUtils.castMapOf2DArray;
Expand Down Expand Up @@ -515,6 +516,21 @@ protected Map<String, Map<String, Object>> handleFunctionStatsResponse(
return response;
}

/** Process a <code>LCS key1 key2 IDX</code> response */
protected Map<String, Object> handleLcsIdxResponse(Map<String, Object> response)
throws RedisException {
Long[][][] convertedMatchesObject =
cast3DArray((Object[]) (response.get(LCS_MATCHES_RESULT_KEY)), Long.class);

if (convertedMatchesObject == null) {
throw new NullPointerException(
"LCS result does not contain the key \"" + LCS_MATCHES_RESULT_KEY + "\"");
}

response.put("matches", convertedMatchesObject);
return response;
}

@Override
public CompletableFuture<Long> del(@NonNull String[] keys) {
return commandManager.submitNewCommand(Del, keys, this::handleLongResponse);
Expand Down Expand Up @@ -2520,6 +2536,47 @@ public CompletableFuture<Long> lcsLen(@NonNull String key1, @NonNull String key2
return commandManager.submitNewCommand(LCS, arguments, this::handleLongResponse);
}

@Override
public CompletableFuture<Map<String, Object>> lcsIdx(@NonNull String key1, @NonNull String key2) {
String[] arguments = new String[] {key1, key2, IDX_COMMAND_STRING};
return commandManager.submitNewCommand(
LCS, arguments, response -> handleLcsIdxResponse(handleMapResponse(response)));
}

@Override
public CompletableFuture<Map<String, Object>> lcsIdx(
@NonNull String key1, @NonNull String key2, long minMatchLen) {
String[] arguments =
new String[] {
key1, key2, IDX_COMMAND_STRING, MINMATCHLEN_COMMAND_STRING, String.valueOf(minMatchLen)
};
return commandManager.submitNewCommand(
LCS, arguments, response -> handleLcsIdxResponse(handleMapResponse(response)));
}

@Override
public CompletableFuture<Map<String, Object>> lcsIdxWithMatchLen(
@NonNull String key1, @NonNull String key2) {
String[] arguments = new String[] {key1, key2, IDX_COMMAND_STRING, WITHMATCHLEN_COMMAND_STRING};
return commandManager.submitNewCommand(LCS, arguments, this::handleMapResponse);
}

@Override
public CompletableFuture<Map<String, Object>> lcsIdxWithMatchLen(
@NonNull String key1, @NonNull String key2, long minMatchLen) {
String[] arguments =
concatenateArrays(
new String[] {
key1,
key2,
IDX_COMMAND_STRING,
MINMATCHLEN_COMMAND_STRING,
String.valueOf(minMatchLen),
WITHMATCHLEN_COMMAND_STRING
});
return commandManager.submitNewCommand(LCS, arguments, this::handleMapResponse);
}

@Override
public CompletableFuture<String> watch(@NonNull String[] keys) {
return commandManager.submitNewCommand(Watch, keys, this::handleStringResponse);
Expand Down
193 changes: 192 additions & 1 deletion java/client/src/main/java/glide/api/commands/StringBaseCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ public interface StringBaseCommands {
/** Redis API keyword used to indicate that the length of the lcs should be returned. */
public static final String LEN_REDIS_API = "LEN";

/** <code>IDX</code> option string to include in the <code>LCS</code> command. */
public static final String IDX_COMMAND_STRING = "IDX";

/** <code>MINMATCHLEN</code> option string to include in the <code>LCS</code> command. */
public static final String MINMATCHLEN_COMMAND_STRING = "MINMATCHLEN";

/** <code>WITHMATCHLEN</code> option string to include in the <code>LCS</code> command. */
public static final String WITHMATCHLEN_COMMAND_STRING = "WITHMATCHLEN";

/** Key for LCS matches result. */
public static final String LCS_MATCHES_RESULT_KEY = "matches";

/**
* Gets the value associated with the given <code>key</code>, or <code>null</code> if no such
* value exists.
Expand Down Expand Up @@ -538,9 +550,188 @@ public interface StringBaseCommands {
* @example
* <pre>{@code
* // testKey1 = abcd, testKey2 = axcd
* Long result = client.lcs("testKey1", "testKey2").get();
* Long result = client.lcsLen("testKey1", "testKey2").get();
* assert result.equals(3L);
* }</pre>
*/
CompletableFuture<Long> lcsLen(String key1, String key2);

/**
* Returns the indices and length of the longest common subsequence between strings stored at
* <code>key1</code> and <code>key2</code>.
*
* @since Redis 7.0 and above.
* @apiNote When in cluster mode, <code>key1</code> and <code>key2</code> must map to the same
* hash slot.
* @see <a href="https://valkey.io/commands/lcs/">valkey.io</a> for details.
* @param key1 The key that stores the first string.
* @param key2 The key that stores the second string.
* @return A <code>Map</code> containing the indices of the longest common subsequence between the
* 2 strings and the length of the longest common subsequence. The resulting map contains two
* keys, "matches" and "len":
* <ul>
* <li>"len" is mapped to the length of the longest common subsequence between the 2 strings
* stored as <code>Long</code>.
* <li>"matches" is mapped to a three dimensional <code>Long</code> array that stores pairs
* of indices that represent the location of the common subsequences in the strings held
* by <code>key1</code> and <code>key2</code>.
* </ul>
*
* @example If <code>key1</code> holds the string <code>"abcd123"</code> and <code>key2</code>
* holds the string <code>"bcdef123"</code> then the sample result would be
* <pre>{@code
* new Long[][][] {
* {
* {4L, 6L},
* {5L, 7L}
* },
* {
* {1L, 3L},
* {0L, 2L}
* }
* }
* }</pre>
* The result indicates that the first substring match is <code>"123"</code> in <code>key1
* </code> at index <code>4</code> to <code>6</code> which matches the substring in <code>key2
* </code> at index <code>5</code> to <code>7</code>. And the second substring match is <code>
* "bcd"</code> in <code>key1</code> at index <code>1</code> to <code>3</code> which matches
* the substring in <code>key2</code> at index <code>0</code> to <code>2</code>.
*/
CompletableFuture<Map<String, Object>> lcsIdx(String key1, String key2);

/**
* Returns the indices and length of the longest common subsequence between strings stored at
* <code>key1</code> and <code>key2</code>.
*
* @since Redis 7.0 and above.
* @apiNote When in cluster mode, <code>key1</code> and <code>key2</code> must map to the same
* hash slot.
* @see <a href="https://valkey.io/commands/lcs/">valkey.io</a> for details.
* @param key1 The key that stores the first string.
* @param key2 The key that stores the second string.
* @param minMatchLen The minimum length of matches to include in the result.
* @return A <code>Map</code> containing the indices of the longest common subsequence between the
* 2 strings and the length of the longest common subsequence. The resulting map contains two
* keys, "matches" and "len":
* <ul>
* <li>"len" is mapped to the length of the longest common subsequence between the 2 strings
* stored as <code>Long</code>.
* <li>"matches" is mapped to a three dimensional <code>Long</code> array that stores pairs
* of indices that represent the location of the common subsequences in the strings held
* by <code>key1</code> and <code>key2</code>.
* </ul>
*
* @example If <code>key1</code> holds the string <code>"abcd123"</code> and <code>key2</code>
* holds the string <code>"bcdef123"</code> then the sample result would be
* <pre>{@code
* new Long[][][] {
* {
* {4L, 6L},
* {5L, 7L}
* },
* {
* {1L, 3L},
* {0L, 2L}
* }
* }
* }</pre>
* The result indicates that the first substring match is <code>"123"</code> in <code>key1
* </code> at index <code>4</code> to <code>6</code> which matches the substring in <code>key2
* </code> at index <code>5</code> to <code>7</code>. And the second substring match is <code>
* "bcd"</code> in <code>key1</code> at index <code>1</code> to <code>3</code> which matches
* the substring in <code>key2</code> at index <code>0</code> to <code>2</code>.
*/
CompletableFuture<Map<String, Object>> lcsIdx(String key1, String key2, long minMatchLen);

/**
* Returns the indices and length of the longest common subsequence between strings stored at
* <code>key1</code> and <code>key2</code>.
*
* @since Redis 7.0 and above.
* @apiNote When in cluster mode, <code>key1</code> and <code>key2</code> must map to the same
* hash slot.
* @see <a href="https://valkey.io/commands/lcs/">valkey.io</a> for details.
* @param key1 The key that stores the first string.
* @param key2 The key that stores the second string.
* @return A <code>Map</code> containing the indices of the longest common subsequence between the
* 2 strings and the length of the longest common subsequence. The resulting map contains two
* keys, "matches" and "len":
* <ul>
* <li>"len" is mapped to the length of the longest common subsequence between the 2 strings
* stored as <code>Long</code>.
* <li>"matches" is mapped to a three dimensional <code>Long</code> array that stores pairs
* of indices that represent the location of the common subsequences in the strings held
* by <code>key1</code> and <code>key2</code>.
* </ul>
*
* @example If <code>key1</code> holds the string <code>"abcd1234"</code> and <code>key2</code>
* holds the string <code>"bcdef1234"</code> then the sample result would be
* <pre>{@code
* new Object[] {
* new Object[] {
* new Long[] {4L, 7L},
* new Long[] {5L, 8L},
* 4L},
* new Object[] {
* new Long[] {1L, 3L},
* new Long[] {0L, 2L},
* 3L}
* }
* }</pre>
* The result indicates that the first substring match is <code>"1234"</code> in <code>key1
* </code> at index <code>4</code> to <code>7</code> which matches the substring in <code>key2
* </code> at index <code>5</code> to <code>8</code> and the last element in the array is the
* length of the substring match which is <code>4</code>. And the second substring match is
* <code>"bcd"</code> in <code>key1</code> at index <code>1</code> to <code>3</code> which
* matches the substring in <code>key2</code> at index <code>0</code> to <code>2</code> and
* the last element in the array is the length of the substring match which is <code>3</code>.
*/
CompletableFuture<Map<String, Object>> lcsIdxWithMatchLen(String key1, String key2);

/**
* Returns the indices and length of the longest common subsequence between strings stored at
* <code>key1</code> and <code>key2</code>.
*
* @since Redis 7.0 and above.
* @apiNote When in cluster mode, <code>key1</code> and <code>key2</code> must map to the same
* hash slot.
* @see <a href="https://valkey.io/commands/lcs/">valkey.io</a> for details.
* @param key1 The key that stores the first string.
* @param key2 The key that stores the second string.
* @param minMatchLen The minimum length of matches to include in the result.
* @return A <code>Map</code> containing the indices of the longest common subsequence between the
* 2 strings and the length of the longest common subsequence. The resulting map contains two
* keys, "matches" and "len":
* <ul>
* <li>"len" is mapped to the length of the longest common subsequence between the 2 strings
* stored as <code>Long</code>.
* <li>"matches" is mapped to a three dimensional <code>Long</code> array that stores pairs
* of indices that represent the location of the common subsequences in the strings held
* by <code>key1</code> and <code>key2</code>.
* </ul>
*
* @example If <code>key1</code> holds the string <code>"abcd1234"</code> and <code>key2</code>
* holds the string <code>"bcdef1234"</code> then the sample result would be
* <pre>{@code
* new Object[] {
* new Object[] {
* new Long[] {4L, 7L},
* new Long[] {5L, 8L},
* 4L},
* new Object[] {
* new Long[] {1L, 3L},
* new Long[] {0L, 2L},
* 3L}
* }
* }</pre>
* The result indicates that the first substring match is <code>"1234"</code> in <code>key1
* </code> at index <code>4</code> to <code>7</code> which matches the substring in <code>key2
* </code> at index <code>5</code> to <code>8</code> and the last element in the array is the
* length of the substring match which is <code>4</code>. And the second substring match is
* <code>"bcd"</code> in <code>key1</code> at index <code>1</code> to <code>3</code> which
* matches the substring in <code>key2</code> at index <code>0</code> to <code>2</code> and
* the last element in the array is the length of the substring match which is <code>3</code>.
*/
CompletableFuture<Map<String, Object>> lcsIdxWithMatchLen(
String key1, String key2, long minMatchLen);
}
Loading

0 comments on commit b969cd9

Please sign in to comment.