0

This java lambda replaces a String that begins with $( and ends with ) with a predefined value from a Map :

SortedMap<String, String> map = new TreeMap<String, String>();
map.put("test", "REPLACE");
String update = Arrays.stream("$(test) (test)      (test2)".split("\\(\\$|\\)"))
.map(token -> map.getOrDefault(token, token))
.collect(Collectors.joining(""));

System.out.println(update);

This prints REPLACE (test (test2

This is almost working as expected but the closing parenthesis is removed from (test & (test2 . Do I need to update the regex or can the lambda be modified to produce :

REPLACE (test) (test2)

blue-sky
  • 51,962
  • 152
  • 427
  • 752

2 Answers2

1

The problem is that you match $( and ) that may mismatch. You only need to match ) after $( and the $( that is followed with ).

Solution 1: Using Matcher#appendReplacementCallback

You can use a simple regex to match $(...) strings and capture the text inside them to get the token from the map, and perform the replacements "on the go" while matching:

SortedMap<String, String> map = new TreeMap<String, String>();
map.put("test", "REPLACE");
String update = "$(test) (test)      (test2)";
Matcher m = Pattern.compile("\\$\\(([^)]*)\\)").matcher(update);
StringBuffer sb = new StringBuffer();
while (m.find()) {
   m.appendReplacement(sb, map.getOrDefault(m.group(1), m.group(1)));
}
m.appendTail(sb);
System.out.println(sb);

See the IDEONE demo

Solution 2: Using Lambda with Split and Lookarounds

Disclaimer: This approach is only good for not-so-long strings.

You can also use lookarounds. With the lookahead, there is no problem, it is infinite width. With lookbehind, we can rely on the constrained width lookbehind (using a limiting quantifier instead of + or *):

SortedMap<String, String> map = new TreeMap<String, String>();
map.put("test", "REPLACE");
String update = Arrays.stream("$(test) (test)      (test2)"
    .split("\\$\\((?=[^)]*\\))|(?<=\\$\\([^(]{0,1000})\\)"))
    .map(token -> map.getOrDefault(token, token))
    .collect(Collectors.joining(""));
    
System.out.println(update); // => REPLACE (test)      (test2)

See the IDEONE demo

The regex now reads:

  • \$\((?=[^)]*\)) - match a $( that is followed with 0+ characters other than ) and then a )
  • | - or
  • (?<=\$\([^(]{0,1000})\) - match a ) that is preceded with 0-1000 (that should be enough) characters other than ( that are preceded with $(.
Community
  • 1
  • 1
Wiktor Stribiżew
  • 607,720
  • 39
  • 448
  • 563
1

You can do this with a stream reduce on the map keySet

public static String replaceKeywords(
        final String template, 
        final Map<String, String> map
) {
    return map.keySet().stream().reduce(template, 
            (acc, key) -> acc.replaceAll("\\$\\(" + key + "\\)", map.get(key)));
}

Usage:

Map<String, String> map = new TreeMap<String, String>();
map.put("test", "FOO");
map.put("test2", "BAR");

System.out.println(replaceKeywords("$(test) $(test2)      (test2)", map));

output:

FOO BAR      (test2)
flakes
  • 21,558
  • 8
  • 41
  • 88
  • Is this a correct explanation of your code (based on https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html) ? template is the initial value of the reduction and the default result if there are no elements in the stream. The accumulator function takes two parameters : acc & key acc contains the partial result of the reduction and key is the next element to be processed. In general the value of the accumulator will contain the identity element so acc will contain the value of template. – blue-sky Apr 08 '16 at 08:04
  • @blue-sky you got it – flakes Apr 08 '16 at 11:18