I have a String
and want to replace some words within. I have a HashMap
where the key is the placeholder to be replaced and the value the word to replace it. Here is my old-school code:
private String replace(String text, Map<String, String> map) {
for (Entry<String, String> entry : map.entrySet()) {
text = text.replaceAll(entry.getKey(), entry.getValue());
}
return text;
}
Is there a way to write this code as a lambda expression?
I tried entrySet().stream().map(...).reduce(...).apply(...);
but couldn't make it work.
Thanks in advance.
I don’t think that you should try to find a simpler or shorter solution, but rather think about the semantics and efficiency of your approach.
You are iterating over a map which likely has no specified iteration order (like HashMap
) and performing one replacement after another, using the result of a replacement as input to the next, potentially missing matches due to previously applied replaces or replacing contents within the replacements.
Even if we assume that you are passing in a map whose keys and values have no interference, this approach is very inefficient. Note further that replaceAll
will interpret the arguments as regular expressions.
If we assume that no regular expressions are intended, we can clear ambiguities between keys by sorting them by length, so that longer keys are attempted first. Then, a solution doing a single replacement operation may look like:
private static String replace(String text, Map<String, String> map) {
if(map.isEmpty()) return text;
String pattern = map.keySet().stream()
.sorted(Comparator.comparingInt(String::length).reversed())
.map(Pattern::quote)
.collect(Collectors.joining("|"));
Matcher m = Pattern.compile(pattern).matcher(text);
if(!m.find()) return text;
StringBuffer sb = new StringBuffer();
do m.appendReplacement(sb, Matcher.quoteReplacement(map.get(m.group())));
while(m.find());
return m.appendTail(sb).toString();
}
starting with Java 9, you can use StringBuilder
instead of StringBuffer
here
If you test it with
Map<String, String> map = new HashMap<>();
map.put("f", "F");
map.put("foo", "bar");
map.put("b", "B");
System.out.println(replace("foo, bar, baz", map));
you’ll get
bar, Bar, Baz
demonstrating that replacing foo
gets precedence over replacing f
and the b
within its replacement bar
is not replaced.
A different thing would be if you want to replace matches within replacements again. In that case, you would need either, a mechanism to control the order or implement a repeated replacement that will only return when there are no matches anymore. Of course, the latter requires care to provide replacements which always will converge to a result eventually.
E.g.
private static String replaceRepeatedly(String text, Map<String, String> map) {
if(map.isEmpty()) return text;
String pattern = map.keySet().stream()
.sorted(Comparator.comparingInt(String::length).reversed())
.map(Pattern::quote)
.collect(Collectors.joining("|"));
Matcher m = Pattern.compile(pattern).matcher(text);
if(!m.find()) return text;
StringBuffer sb;
do {
sb = new StringBuffer();
do m.appendReplacement(sb, Matcher.quoteReplacement(map.get(m.group())));
while(m.find());
m.appendTail(sb);
} while(m.reset(sb).find());
return sb.toString();
}
Map<String, String> map = new HashMap<>();
map.put("a", "e1");
map.put("e", "o2");
map.put("o", "x3");
System.out.println(replaceRepeatedly("foo, bar, baz", map));
fx3x3, bx321r, bx321z
Few improvements to @RavindraRanwala 's code.
String replacement = Stream.of(text.split("\\b"))
.map(token -> map.getOrDefault(token, token))
.collect(Collectors.joining(""));
1) Use Map.getOrDefault
from Java 8
2) Split by "\b" to support any delimiter of the words, not only space character
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With