Therefore, the string replacements must be done in a single pass of the format string. I recommend doing it using a regular expression.
Furthermore, withyour method provides no escape mechanism, in case you need to specify a literal Matcher.replaceAll(Function<MatchResult,String> replacer)
%(blah)
in the format string. In Java, it would be customary to use backslash as an escape character.
Suggested solution
This solution uses Matcher.replaceAll(Function<MatchResult,String> replacer)
, which was introduced in Java 9, to provide each substitution text via a callback.
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NamedFormatter {
private static final Pattern RE = Pattern.compile(
"\\\\(.)" + // Treat any character after a backslash literally
"|" +
"(%\\(([^)]+)\\))" // Look for %(keys) to replace
);
private NamedFormatter() {}
/**
* Expands format strings containing <code>%(keys)</code>.
*
* <p>Examples:</p>
*
* <ul>
* <li><code>NamedFormatter.format("Hello, %(name)!", Map.of("name", "200_success"))</code> → <code>"Hello, 200_success!"</code></li>
* <li><code>NamedFormatter.format("Hello, \%(name)!", Map.of("name", "200_success"))</code> → <code>"Hello, %(name)!"</code></li>
* <li><code>NamedFormatter.format("Hello, %(name)!", Map.of("foo", "bar"))</code> → <code>"Hello, %(name)!"</code></li>
* </ul>
*
* @param fmt The format string. Any character in the format string that
* follows a backslash is treated literally. Any
* <code>%(key)</code> is replaced by its corresponding value
* in the <code>values</code> map. If the key does not exist
* in the <code>values</code> map, then it is left unsubstituted.
*
* @param values Key-value pairs to be used in the substitutions.
*
* @return The formatted string.
*/
public static String format(String fmt, Map<String, Object> values) {
return RE.matcher(fmt).replaceAll(match ->
match.group(1) != null ?
match.group(1) :
values.getOrDefault(match.group(3), match.group(2)).toString()
);
}
}
Therefore, the string replacements must be done in a single pass of the format string. I recommend doing it using a regular expression, with Matcher.replaceAll(Function<MatchResult,String> replacer)
to provide each substitution text via a callback.
Therefore, the string replacements must be done in a single pass of the format string. I recommend doing it using a regular expression.
Furthermore, your method provides no escape mechanism, in case you need to specify a literal %(blah)
in the format string. In Java, it would be customary to use backslash as an escape character.
Suggested solution
This solution uses Matcher.replaceAll(Function<MatchResult,String> replacer)
, which was introduced in Java 9, to provide each substitution text via a callback.
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NamedFormatter {
private static final Pattern RE = Pattern.compile(
"\\\\(.)" + // Treat any character after a backslash literally
"|" +
"(%\\(([^)]+)\\))" // Look for %(keys) to replace
);
private NamedFormatter() {}
/**
* Expands format strings containing <code>%(keys)</code>.
*
* <p>Examples:</p>
*
* <ul>
* <li><code>NamedFormatter.format("Hello, %(name)!", Map.of("name", "200_success"))</code> → <code>"Hello, 200_success!"</code></li>
* <li><code>NamedFormatter.format("Hello, \%(name)!", Map.of("name", "200_success"))</code> → <code>"Hello, %(name)!"</code></li>
* <li><code>NamedFormatter.format("Hello, %(name)!", Map.of("foo", "bar"))</code> → <code>"Hello, %(name)!"</code></li>
* </ul>
*
* @param fmt The format string. Any character in the format string that
* follows a backslash is treated literally. Any
* <code>%(key)</code> is replaced by its corresponding value
* in the <code>values</code> map. If the key does not exist
* in the <code>values</code> map, then it is left unsubstituted.
*
* @param values Key-value pairs to be used in the substitutions.
*
* @return The formatted string.
*/
public static String format(String fmt, Map<String, Object> values) {
return RE.matcher(fmt).replaceAll(match ->
match.group(1) != null ?
match.group(1) :
values.getOrDefault(match.group(3), match.group(2)).toString()
);
}
}
Performing string substitutions using multiple passes is almost always the wrong approach, and leads to bugs. If one of the values happens to be a string that looks like a %(key)
, then all sorts of unpredictable things could happen, including various uncontrolled format string attacks!
Therefore, the string replacements must be done in a single pass of the format string. I recommend doing it using a regular expression, with Matcher.replaceAll(Function<MatchResult,String> replacer)
to provide each substitution text via a callback.