A simple Dictionary consists of entries DictionaryEntry( key, value, otherStuff).
I would like to replace words in a String my a dictionary entry or the "key". I can write 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());
}
}
record Dictionary( String key, String value, String other) {};
The output is: 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 thefoo
.(?=foo)
- matches a position that immediately follows after thefoo
;
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