Home > Blockchain >  Java-Stream & Optional - Find a value that matches to a stream-element or provide a Default value
Java-Stream & Optional - Find a value that matches to a stream-element or provide a Default value

Time:09-11

I have a Dictionary object which consists of several entries:

record Dictionary(String key, String value, String other) {};

I would like to replace words in the given String my a which are present as a "key" in one of the dictionaries with the corresponding value. I can achieve it like this, but I guess, there must be a better way to do this.

An example:

> Input: One <sup>a</sup> Two <sup>b</sup> Three <sup>D</sup> Four
> Output: One [a-value] Two [b-value] Three [D] Four

The code to be improved:

public class ReplaceStringWithDictionaryEntries {
    public static void main(String[] args) {
        List<Dictionary> dictionary = List.of(new Dictionary("a", "a-value", "a-other"),
                new Dictionary("b", "b-value", "b-other"));
        String theText = "One <sup>a</sup> Two <sup>b</sup> Three <sup>D</sup> Four";
        Matcher matcher = Pattern.compile("<sup>([A-Za-z] )</sup>").matcher(theText);
        StringBuilder sb = new StringBuilder();
        int matchLast = 0;
        while (matcher.find()) {
            sb.append(theText, matchLast, matcher.start());
            Optional<Dictionary> dict = dictionary.stream().filter(f -> f.key().equals(matcher.group(1))).findFirst();
            if (dict.isPresent()) {
                sb.append("[").append(dict.get().value()).append("]");
            } else {
                sb.append("[").append(matcher.group(1)).append("]");
            }

            matchLast = matcher.end();
        }
        if (matchLast != 0) {
            sb.append(theText.substring(matchLast));
        }
        System.out.println("Result: "   sb.toString());
    }
}

Output:

Result: One [a-value] Two [b-value] Three [D] Four

Do you have a more elegant way to do this?

CodePudding user response:

Create a map from your list using key as key and value as value, use the Matcher#appendReplacement method to replace matches using the above map and calling Map.getOrDefault, use the group(1) value as default value. Use String#join to put the replacements in square braces

public static void main(String[] args) {
    List<Dictionary> dictionary = List.of(
            new Dictionary("a", "a-value", "a-other"),
            new Dictionary("b", "b-value", "b-other"));

    Map<String,String> myMap = dictionary.stream()
                                         .collect(Collectors.toMap(Dictionary::key, Dictionary::value));

    String theText  = "One <sup>a</sup> Two <sup>b</sup> Three <sup>D</sup> Four";
    Matcher matcher = Pattern.compile("<sup>([A-Za-z] )</sup>").matcher(theText);

    StringBuilder sb = new StringBuilder();
    while (matcher.find()) {
        matcher.appendReplacement(sb, 
                  String.join("", "[", myMap.getOrDefault(matcher.group(1), matcher.group(1)), "]"));
    }
    matcher.appendTail(sb);
    System.out.println(sb.toString());
}

record Dictionary( String key, String value, String other) {};

CodePudding user response:

Since Java 9, Matcher#replaceAll can accept a callback function to return the replacement for each matched value.

String result = Pattern.compile("<sup>([A-Za-z] )</sup>").matcher(theText)
    .replaceAll(mr -> "["   dictionary.stream().filter(f -> f.key().equals(mr.group(1)))
                  .findFirst().map(Dictionary::value)
                  .orElse(mr.group(1))   "]");

CodePudding user response:

Map vs List

As @Chaosfire has pointed out in the comment, a Map is more suitable collection for the task than a List, because it eliminates the need of iterating over collection to access a particular element

Map<String, Dictionary> dictByKey = Map.of(
    "a", new Dictionary("a", "a-value", "a-other"),
    "b", new Dictionary("b", "b-value", "b-other")
);

And I would also recommend wrapping the Map with a class in order to provide continent access to the string-values of the dictionary, otherwise we are forced to check whether a dictionary returned from the map is not null and only then make a call to obtain the required value, which is inconvenient. The utility class can facilitate getting the target value in a single method call.

To avoid complicating the answer, I would implement such a utility class, and for simplicity I'll go with a Map<String,String> (which basically would act as such intended to act - providing the value withing a single call).

public static final Map<String, String> dictByKey = Map.of(
    "a", "a-value",
    "b", "b-value"
);

Pattern.splitAsStream()

We can replace while-loop with a stream created via splitAsStream() .

In order to distinguish between string-values enclosed with tags <sup>text</sup> we can make use of the special constructs which are called Lookbehind (?<=</sup>) and Lookahead (?=<sup>).

  • (?<=foo) - matches a position that immediately precedes the foo.
  • (?=foo) - matches a position that immediately follows after the foo;

For more information, have a look at this tutorial

The pattern "(?=<sup>)|(?<=</sup>)" would match a position in the given string right before the opening tag and immediately after the closing tag. So when we apply this pattern splitting the string with splitAsStream(), it would produce a stream containing elements like "<sup>a</sup>" enclosed with tags, and plain string like "One", "Two", "Three".

Note that in order to reuse the pattern without recompiling, it can be declared on a class level:

public static final Pattern pattern = Pattern.compile("(?=<sup>)|(?<=</sup>)");

The final solution would result in lean and simple stream:

public static void foo(String text) {

    String result = pattern.splitAsStream(text)
        .map(str -> getValue(str))      // or MyClass::getValue
        .collect(Collectors.joining());

    System.out.println(result);
}

Instead of tackling conditional logic inside a lambda, it's often better to extract it into a separate method:

public static String getValue(String str) {
    if (str.matches("<sup>\\p{Alpha} </sup>")) {
        String key = str.replaceAll("<sup>|</sup>", "");
        return "["   dictByKey.getOrDefault(key, key)   "]";
    }
    return str;
}

main()

public static void main(String[] args) {
    foo("One <sup>a</sup> Two <sup>b</sup> Three <sup>D</sup> Four");
}

Output:

Result: One [a-value] Two [b-value] Three [D] Four

A link to Online Demo

  • Related