Именованные заполнители в форматировании строк
в Python, при форматировании строки, я могу заполнить заполнители по имени, а не по позиции, например:
print "There's an incorrect value '%(value)s' in column # %(column)d" %
{ 'value': x, 'column': y }
интересно, возможно ли это в Java (надеюсь, без внешних библиотек)?
14 ответов:
StrSubstitutor of jakarta commons lang-это легкий способ сделать это при условии, что ваши значения уже правильно отформатированы.
Map<String, String> values = new HashMap<String, String>(); values.put("value", x); values.put("column", y); StrSubstitutor sub = new StrSubstitutor(values, "%(", ")"); String result = sub.replace("There's an incorrect value '%(value)' in column # %(column)");вышеуказанные результаты в:
"есть неверное значение" 1 "в столбце № 2"
при использовании Maven вы можете добавить эту зависимость в свой pom.XML-код:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency>
не совсем, но вы можете использовать MessageFormat ссылаться на одно значение несколько раз:
MessageFormat.format("There's an incorrect value \"{0}\" in column # {1}", x, y);выше может быть сделано со строкой.format () также, но я нахожу messageFormat синтаксис чище, если вам нужно построить сложные выражения, плюс вам не нужно заботиться о типе объекта, который вы помещаете в строку
можно использовать StringTemplate библиотеки, он предлагает то, что вы хотите, и многое другое.
import org.antlr.stringtemplate.*; final StringTemplate hello = new StringTemplate("Hello, $name$"); hello.setAttribute("name", "World"); System.out.println(hello.toString());
на простых случаях вы можете просто использовать жесткую строку заменить, нет необходимости в библиотеке там:
String url = "There's an incorrect value '%(value)' in column # %(column)"; url = url.replace("%(value)", x); // 1 url = url.replace("%(column)", y); // 2предупреждение: Я просто хотел показать самый простой код. Конечно, не используйте это для серьезного производственного кода, где вопросы безопасности, как указано в комментариях: экранирование, обработка ошибок и безопасность являются проблемой здесь. Но в худшем случае вы теперь знаете, почему использование "хорошего" lib требуется : -)
Спасибо за вашу помощь! Используя все ваши подсказки, я написал процедуру, чтобы сделать именно то, что я хочу-python-подобное форматирование строк с помощью словаря. Поскольку я новичок Java, любые намеки приветствуются.
public static String dictFormat(String format, Hashtable<String, Object> values) { StringBuilder convFormat = new StringBuilder(format); Enumeration<String> keys = values.keys(); ArrayList valueList = new ArrayList(); int currentPos = 1; while (keys.hasMoreElements()) { String key = keys.nextElement(), formatKey = "%(" + key + ")", formatPos = "%" + Integer.toString(currentPos) + "$"; int index = -1; while ((index = convFormat.indexOf(formatKey, index)) != -1) { convFormat.replace(index, index + formatKey.length(), formatPos); index += formatPos.length(); } valueList.add(values.get(key)); ++currentPos; } return String.format(convFormat.toString(), valueList.toArray()); }
Это старый поток, но только для записи, вы также можете использовать Java 8 стиль, как это:
public static String replaceParams(Map<String, String> hashMap, String template) { return hashMap.entrySet().stream().reduce(template, (s, e) -> s.replace("%(" + e.getKey() + ")", e.getValue()), (s, s2) -> s); }использование:
public static void main(String[] args) { final HashMap<String, String> hashMap = new HashMap<String, String>() { { put("foo", "foo1"); put("bar", "bar1"); put("car", "BMW"); put("truck", "MAN"); } }; String res = replaceParams(hashMap, "This is '%(foo)' and '%(foo)', but also '%(bar)' '%(bar)' indeed."); System.out.println(res); System.out.println(replaceParams(hashMap, "This is '%(car)' and '%(foo)', but also '%(bar)' '%(bar)' indeed.")); System.out.println(replaceParams(hashMap, "This is '%(car)' and '%(truck)', but also '%(foo)' '%(bar)' + '%(truck)' indeed.")); }вывод будет:
This is 'foo1' and 'foo1', but also 'bar1' 'bar1' indeed. This is 'BMW' and 'foo1', but also 'bar1' 'bar1' indeed. This is 'BMW' and 'MAN', but also 'foo1' 'bar1' + 'MAN' indeed.
public static String format(String format, Map<String, Object> values) { StringBuilder formatter = new StringBuilder(format); List<Object> valueList = new ArrayList<Object>(); Matcher matcher = Pattern.compile("\$\{(\w+)}").matcher(format); while (matcher.find()) { String key = matcher.group(1); String formatKey = String.format("${%s}", key); int index = formatter.indexOf(formatKey); if (index != -1) { formatter.replace(index, index + formatKey.length(), "%s"); valueList.add(values.get(key)); } } return String.format(formatter.toString(), valueList.toArray()); }пример:
String format = "My name is . ."; Map<String, Object> values = new HashMap<String, Object>(); values.put("0", "James"); values.put("1", "Bond"); System.out.println(format(format, values)); // My name is Bond. James Bond.
Я автор небольшая библиотека что делает именно то, что вы хотите:
Student student = new Student("Andrei", 30, "Male"); String studStr = template("#{id}\tName: #{st.getName}, Age: #{st.getAge}, Gender: #{st.getGender}") .arg("id", 10) .arg("st", student) .format(); System.out.println(studStr);или вы можете цепочку аргументов:
String result = template("#{x} + #{y} = #{z}") .args("x", 5, "y", 10, "z", 15) .format(); System.out.println(result); // Output: "5 + 10 = 15"
вы могли бы иметь что-то вроде этого на строку вспомогательного класса
/** * An interpreter for strings with named placeholders. * * For example given the string "hello %(myName)" and the map <code> * <p>Map<String, Object> map = new HashMap<String, Object>();</p> * <p>map.put("myName", "world");</p> * </code> * * the call {@code format("hello %(myName)", map)} returns "hello world" * * It replaces every occurrence of a named placeholder with its given value * in the map. If there is a named place holder which is not found in the * map then the string will retain that placeholder. Likewise, if there is * an entry in the map that does not have its respective placeholder, it is * ignored. * * @param str * string to format * @param values * to replace * @return formatted string */ public static String format(String str, Map<String, Object> values) { StringBuilder builder = new StringBuilder(str); for (Entry<String, Object> entry : values.entrySet()) { int start; String pattern = "%(" + entry.getKey() + ")"; String value = entry.getValue().toString(); // Replace every occurence of %(key) with value while ((start = builder.indexOf(pattern)) != -1) { builder.replace(start, start + pattern.length(), value); } } return builder.toString(); }
мой ответ:
a) используйте StringBuilder, когда это возможно
b) сохранить (в любой форме: integer является лучшим, speciall char, как доллар макрос и т.д.) положение "заполнитель", а затем использовать
StringBuilder.insert()(несколько версий аргументов).использование внешних библиотек кажется излишним, и я считаю, что значительно ухудшается производительность, когда StringBuilder преобразуется в String внутренне.
на основе ответ Я создал
MapBuilderкласс:public class MapBuilder { public static Map<String, Object> build(Object... data) { Map<String, Object> result = new LinkedHashMap<>(); if (data.length % 2 != 0) { throw new IllegalArgumentException("Odd number of arguments"); } String key = null; Integer step = -1; for (Object value : data) { step++; switch (step % 2) { case 0: if (value == null) { throw new IllegalArgumentException("Null key value"); } key = (String) value; continue; case 1: result.put(key, value); break; } } return result; } }затем я создал класс
StringFormatдля форматирования строк:public final class StringFormat { public static String format(String format, Object... args) { Map<String, Object> values = MapBuilder.build(args); for (Map.Entry<String, Object> entry : values.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); format = format.replace("$" + key, value.toString()); } return format; } }что вы могли бы использовать так:
String bookingDate = StringFormat.format("From $startDate to $endDate"), "$startDate", formattedStartDate, "$endDate", formattedEndDate );
Apache Commons Lang's replaceEach метод может пригодиться в зависимости от ваших конкретных потребностей. Вы можете легко использовать его для замены заполнителей по имени с помощью этого единственного вызова метода:
StringUtils.replaceEach("There's an incorrect value '%(value)' in column # %(column)", new String[] { "%(value)", "%(column)" }, new String[] { x, y });учитывая некоторый входной текст, это заменит все вхождения заполнителей в первом строковом массиве соответствующими значениями во втором.
попробовать Freemarker, шаблонов, библиотек.
Я также создал класс util / helper (используя jdk 8), который может форматировать строку и заменяет вхождения переменных.
Для этого я использовал метод Matchers "appendReplacement", который выполняет все подстановки и циклы только над затронутыми частями строки формата.
Вспомогательный класс В настоящее время не очень хорошо документирован javadoc. Я изменю это в будущем ;) В любом случае я прокомментировал самые важные строки (я надеяться.)
public class FormatHelper { //Prefix and suffix for the enclosing variable name in the format string. //Replace the default values with any you need. public static final String DEFAULT_PREFIX = "${"; public static final String DEFAULT_SUFFIX = "}"; //Define dynamic function what happens if a key is not found. //Replace the defualt exception with any "unchecked" exception type you need or any other behavior. public static final BiFunction<String, String, String> DEFAULT_NO_KEY_FUNCTION = (fullMatch, variableName) -> { throw new RuntimeException(String.format("Key: %s for variable %s not found.", variableName, fullMatch)); }; private final Pattern variablePattern; private final Map<String, String> values; private final BiFunction<String, String, String> noKeyFunction; private final String prefix; private final String suffix; public FormatHelper(Map<String, String> values) { this(DEFAULT_NO_KEY_FUNCTION, values); } public FormatHelper( BiFunction<String, String, String> noKeyFunction, Map<String, String> values) { this(DEFAULT_PREFIX, DEFAULT_SUFFIX, noKeyFunction, values); } public FormatHelper(String prefix, String suffix, Map<String, String> values) { this(prefix, suffix, DEFAULT_NO_KEY_FUNCTION, values); } public FormatHelper( String prefix, String suffix, BiFunction<String, String, String> noKeyFunction, Map<String, String> values) { this.prefix = prefix; this.suffix = suffix; this.values = values; this.noKeyFunction = noKeyFunction; //Create the Pattern and quote the prefix and suffix so that the regex don't interpret special chars. //The variable name is a "\w+" in an extra capture group. variablePattern = Pattern.compile(Pattern.quote(prefix) + "(\w+)" + Pattern.quote(suffix)); } public static String format(CharSequence format, Map<String, String> values) { return new FormatHelper(values).format(format); } public static String format( CharSequence format, BiFunction<String, String, String> noKeyFunction, Map<String, String> values) { return new FormatHelper(noKeyFunction, values).format(format); } public static String format( String prefix, String suffix, CharSequence format, Map<String, String> values) { return new FormatHelper(prefix, suffix, values).format(format); } public static String format( String prefix, String suffix, BiFunction<String, String, String> noKeyFunction, CharSequence format, Map<String, String> values) { return new FormatHelper(prefix, suffix, noKeyFunction, values).format(format); } public String format(CharSequence format) { //Create matcher based on the init pattern for variable names. Matcher matcher = variablePattern.matcher(format); //This buffer will hold all parts of the formatted finished string. StringBuffer formatBuffer = new StringBuffer(); //loop while the matcher finds another variable (prefix -> name <- suffix) match while (matcher.find()) { //The root capture group with the full match e.g ${variableName} String fullMatch = matcher.group(); //The capture group for the variable name resulting from "(\w+)" e.g. variableName String variableName = matcher.group(1); //Get the value in our Map so the Key is the used variable name in our "format" string. The associated value will replace the variable. //If key is missing (absent) call the noKeyFunction with parameters "fullMatch" and "variableName" else return the value. String value = values.computeIfAbsent(variableName, key -> noKeyFunction.apply(fullMatch, key)); //Escape the Map value because the "appendReplacement" method interprets the $ and \ as special chars. String escapedValue = Matcher.quoteReplacement(value); //The "appendReplacement" method replaces the current "full" match (e.g. ${variableName}) with the value from the "values" Map. //The replaced part of the "format" string is appended to the StringBuffer "formatBuffer". matcher.appendReplacement(formatBuffer, escapedValue); } //The "appendTail" method appends the last part of the "format" String which has no regex match. //That means if e.g. our "format" string has no matches the whole untouched "format" string is appended to the StringBuffer "formatBuffer". //Further more the method return the buffer. return matcher.appendTail(formatBuffer) .toString(); } public String getPrefix() { return prefix; } public String getSuffix() { return suffix; } public Map<String, String> getValues() { return values; } }вы можете создать экземпляр класса для конкретной карты со значениями (или суффикс префикс или noKeyFunction) как:
Map<String, String> values = new HashMap<>(); values.put("firstName", "Peter"); values.put("lastName", "Parker"); FormatHelper formatHelper = new FormatHelper(values); formatHelper.format("${firstName} ${lastName} is Spiderman!"); // Result: "Peter Parker is Spiderman!" // Next format: formatHelper.format("Does ${firstName} ${lastName} works as photographer?"); //Result: "Does Peter Parker works as photographer?"Кроме того, вы можете определить, что произойдет, если ключ в карте значений отсутствует (работает в обоих направлениях, например, неправильное имя переменной в строке формата или отсутствующий ключ в карте). Поведение по умолчанию-это выброшенное непроверенное исключение (непроверенное, потому что я использую функция jdk8 по умолчанию, которая не может обрабатывать проверенные исключения) как:
Map<String, String> map = new HashMap<>(); map.put("firstName", "Peter"); map.put("lastName", "Parker"); FormatHelper formatHelper = new FormatHelper(map); formatHelper.format("${missingName} ${lastName} is Spiderman!"); //Result: RuntimeException: Key: missingName for variable ${missingName} not found.Вы можете определить пользовательское поведение в конструкторе вызов:
Map<String, String> values = new HashMap<>(); values.put("firstName", "Peter"); values.put("lastName", "Parker"); FormatHelper formatHelper = new FormatHelper(fullMatch, variableName) -> variableName.equals("missingName") ? "John": "SOMETHING_WRONG", values); formatHelper.format("${missingName} ${lastName} is Spiderman!"); // Result: "John Parker is Spiderman!"или делегировать его обратно к поведению ключа по умолчанию нет:
... FormatHelper formatHelper = new FormatHelper((fullMatch, variableName) -> variableName.equals("missingName") ? "John" : FormatHelper.DEFAULT_NO_KEY_FUNCTION.apply(fullMatch, variableName), map); ...Для лучшей обработки существуют также статические представления методов, такие как:
Map<String, String> values = new HashMap<>(); values.put("firstName", "Peter"); values.put("lastName", "Parker"); FormatHelper.format("${firstName} ${lastName} is Spiderman!", map); // Result: "Peter Parker is Spiderman!"
Comments