Marcadores de posición con nombre en formato de cadena

En Python, al formatear una cadena, puedo llenar marcadores de posición por nombre en lugar de por posición, así:

print "There's an incorrect value '%(value)s' in column # %(column)d" % \ { 'value': x, 'column': y } 

Me pregunto si eso es posible en Java (con suerte, sin bibliotecas externas)?

StrSubstitutor de jakarta commons lang es una forma ligera de hacerlo, siempre que tus valores ya estén formateados correctamente.

http://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/text/StrSubstitutor.html

 Map values = new HashMap(); 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)"); 

Los resultados anteriores en:

“Hay un valor incorrecto ‘1’ en la columna n.º 2”

Al usar Maven, puede agregar esta dependencia a su pom.xml:

  org.apache.commons commons-lang3 3.4  

no del todo, pero puede usar MessageFormat para hacer referencia a un valor varias veces:

 MessageFormat.format("There's an incorrect value \"{0}\" in column # {1}", x, y); 

Lo anterior también se puede hacer con String.format (), pero creo que la syntax de messageFormat es más limpia si necesita crear expresiones complejas, además de que no necesita preocuparse por el tipo de objeto que está poniendo en la cadena.

Puede usar la biblioteca StringTemplate , ofrece lo que desea y mucho más.

 import org.antlr.stringtemplate.*; final StringTemplate hello = new StringTemplate("Hello, $name$"); hello.setAttribute("name", "World"); System.out.println(hello.toString()); 

Para casos simples , simplemente puede usar un reemplazo de cadenas codificadas, no es necesario que haya una biblioteca allí:

  String url = "There's an incorrect value '%(value)' in column # %(column)"; url = url.replace("%(value)", x); // 1 url = url.replace("%(column)", y); // 2 

ADVERTENCIA : solo quería mostrar el código más simple posible. Por supuesto, NO use esto para el código de producción serio donde la seguridad importa, como se indica en los comentarios: el escape, el manejo de errores y la seguridad son un problema aquí. Pero en el peor de los casos, ahora sabes por qué se necesita usar una ‘buena’ lib 🙂

¡Gracias por toda tu ayuda! Utilizando todas tus pistas, escribí la rutina para hacer exactamente lo que quiero: formateo de cadena tipo python usando el diccionario. Como soy novato en Java, se aprecian todas las sugerencias.

 public static String dictFormat(String format, Hashtable values) { StringBuilder convFormat = new StringBuilder(format); Enumeration 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()); } 

Este es un hilo viejo, pero solo para el registro, también puedes usar el estilo Java 8, como este:

 public static String replaceParams(Map hashMap, String template) { return hashMap.entrySet().stream().reduce(template, (s, e) -> s.replace("%(" + e.getKey() + ")", e.getValue()), (s, s2) -> s); } 

Uso:

 public static void main(String[] args) { final HashMap hashMap = new HashMap() { { 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.")); } 

El resultado será:

 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. 

Soy el autor de una pequeña biblioteca que hace exactamente lo que desea:

 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); 

O puedes encadenar los argumentos:

 String result = template("#{x} + #{y} = #{z}") .args("x", 5, "y", 10, "z", 15) .format(); System.out.println(result); // Output: "5 + 10 = 15" 

Podrías tener algo como esto en una clase de ayudante de cuerdas

 /** * An interpreter for strings with named placeholders. * * For example given the string "hello %(myName)" and the map  * 

Map map = new HashMap();

*

map.put("myName", "world");

*
* * 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 values) { StringBuilder builder = new StringBuilder(str); for (Entry 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(); }
 public static String format(String format, Map values) { StringBuilder formatter = new StringBuilder(format); List valueList = new ArrayList(); 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()); } 

Ejemplo:

 String format = "My name is ${1}. ${0} ${1}."; Map values = new HashMap(); values.put("0", "James"); values.put("1", "Bond"); System.out.println(format(format, values)); // My name is Bond. James Bond. 

Mi respuesta es:

a) use StringBuilder cuando sea posible

b) keep (en cualquier forma: integer es la mejor posición, speciall char como macro de dólar, etc.) de “placeholder” y luego usa StringBuilder.insert() (pocas versiones de argumentos).

El uso de bibliotecas externas parece exagerado y creo que degradan el rendimiento de manera significativa cuando StringBuilder se convierte internamente en Cadena.

Basado en la respuesta , MapBuilder clase MapBuilder :

 public class MapBuilder { public static Map build(Object... data) { Map 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; } } 

luego creé la clase StringFormat para el formato de cadena:

 public final class StringFormat { public static String format(String format, Object... args) { Map values = MapBuilder.build(args); for (Map.Entry entry : values.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); format = format.replace("$" + key, value.toString()); } return format; } } 

que podrías usar así:

 String bookingDate = StringFormat.format("From $startDate to $endDate"), "$startDate", formattedStartDate, "$endDate", formattedEndDate ); 

Apache Commons Lang’s replaceEach método puede ser útil dependiendo de sus necesidades específicas. Puedes usarlo fácilmente para reemplazar los marcadores de posición por nombre con esta llamada a un solo método:

 StringUtils.replaceEach("There's an incorrect value '%(value)' in column # %(column)", new String[] { "%(value)", "%(column)" }, new String[] { x, y }); 

Dado un texto de entrada, esto reemplazará todas las apariciones de los marcadores de posición en la primera matriz de cadenas con los valores correspondientes en la segunda.

Pruebe Freemarker , biblioteca de plantillas.

texto alternativo http://sofes.miximages.com/string-formatting/overview.png

Creé también una clase util / helper (usando jdk 8) que puede formatear una cadena y reemplaza las ocurrencias de variables.

Para este propósito utilicé el método “appendReplacement” de Matchers que hace toda la sustitución y loops solo sobre las partes afectadas de una cadena de formato.

La clase de ayuda no está actualmente bien documentada por javadoc. Voy a cambiar esto en el futuro;) De todos modos comenté las líneas más importantes (espero).

  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 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 values; private final BiFunction noKeyFunction; private final String prefix; private final String suffix; public FormatHelper(Map values) { this(DEFAULT_NO_KEY_FUNCTION, values); } public FormatHelper( BiFunction noKeyFunction, Map values) { this(DEFAULT_PREFIX, DEFAULT_SUFFIX, noKeyFunction, values); } public FormatHelper(String prefix, String suffix, Map values) { this(prefix, suffix, DEFAULT_NO_KEY_FUNCTION, values); } public FormatHelper( String prefix, String suffix, BiFunction noKeyFunction, Map 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 values) { return new FormatHelper(values).format(format); } public static String format( CharSequence format, BiFunction noKeyFunction, Map values) { return new FormatHelper(noKeyFunction, values).format(format); } public static String format( String prefix, String suffix, CharSequence format, Map values) { return new FormatHelper(prefix, suffix, values).format(format); } public static String format( String prefix, String suffix, BiFunction noKeyFunction, CharSequence format, Map 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 eg ${variableName} String fullMatch = matcher.group(); //The capture group for the variable name resulting from "(\w+)" eg 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 (eg ${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 eg 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 getValues() { return values; } } 

Puede crear una instancia de clase para un Mapa específico con valores (o prefijo de sufijo o noKeyFunction) como:

  Map 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?" 

Además, puede definir qué sucede si falta una clave en los valores Mapa (funciona de ambas maneras, por ejemplo, nombre de variable incorrecto en cadena de formato o clave faltante en Mapa). El comportamiento predeterminado es una excepción lanzada sin marcar (sin marcar porque utilizo la función jdk8 predeterminada que no puede controlar las excepciones comprobadas) como:

  Map 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. 

Puede definir un comportamiento personalizado en la llamada del constructor como:

 Map 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!" 

o delegarlo nuevamente al comportamiento predeterminado sin clave:

 ... FormatHelper formatHelper = new FormatHelper((fullMatch, variableName) -> variableName.equals("missingName") ? "John" : FormatHelper.DEFAULT_NO_KEY_FUNCTION.apply(fullMatch, variableName), map); ... 

Para un mejor manejo también hay representaciones de métodos estáticos como:

 Map values = new HashMap<>(); values.put("firstName", "Peter"); values.put("lastName", "Parker"); FormatHelper.format("${firstName} ${lastName} is Spiderman!", map); // Result: "Peter Parker is Spiderman!" 
    Intereting Posts