2009-07-16 25 views
9

Dada una cadena de este modo:Cómo reemplazar fichas en una cadena sin StringTokenizer

Hello {FIRST_NAME}, this is a personalized message for you. 

Dónde FIRST_NAME es un token arbitraria (una llave en un mapa pasado al método), para escribir una rutina que se convertiría esa cadena en:

Hello Jim, this is a personalized message for you. 

dado un mapa con una entrada FIRST_NAME -> Jim.

Parece que StringTokenizer es el enfoque más directo, pero los Javadocs realmente dicen que debería preferir usar el enfoque de expresiones regulares. ¿Cómo harías eso en una solución basada en expresiones regulares?

+0

try http://github.com/niesfisch/tokenreplacer/ – Marcel

Respuesta

4

Prueba esto:

Nota: Elauthor's final solution construye sobre esta muestra y es mucho más concisa.

public class TokenReplacer { 

    private Pattern tokenPattern; 

    public TokenReplacer() { 
     tokenPattern = Pattern.compile("\\{([^}]+)\\}"); 
    } 

    public String replaceTokens(String text, Map<String, String> valuesByKey) { 
     StringBuilder output = new StringBuilder(); 
     Matcher tokenMatcher = tokenPattern.matcher(text); 

     int cursor = 0; 
     while (tokenMatcher.find()) { 
      // A token is defined as a sequence of the format "{...}". 
      // A key is defined as the content between the brackets. 
      int tokenStart = tokenMatcher.start(); 
      int tokenEnd = tokenMatcher.end(); 
      int keyStart = tokenMatcher.start(1); 
      int keyEnd = tokenMatcher.end(1); 

      output.append(text.substring(cursor, tokenStart)); 

      String token = text.substring(tokenStart, tokenEnd); 
      String key = text.substring(keyStart, keyEnd); 

      if (valuesByKey.containsKey(key)) { 
       String value = valuesByKey.get(key); 
       output.append(value); 
      } else { 
       output.append(token); 
      } 

      cursor = tokenEnd; 
     } 
     output.append(text.substring(cursor)); 

     return output.toString(); 
    } 

} 
+0

Eso recompilará el patrón para cada línea. ¡Prefiero mis patrones tan precompilados como sea posible! :-) Además, será mejor que compruebes la existencia del token. –

+0

Es decir, compruebe que tokenexists en el mapa. –

+0

Puede simplemente hacer que 'tokenPattern' sea una variable de instancia de cualquier clase contendrá este método para evitar compilarlo cada vez. El código se ajustará automáticamente a la situación en la que no se detecta ningún token ('output.append (text.substring (cursor))'). –

0

Los documentos significan que debe preferir escribir un tokenizador basado en expresiones regulares, IIRC. Lo que podría funcionar mejor para usted es una búsqueda regex estándar-reemplazar.

6
String.replaceAll("{FIRST_NAME}", actualName); 

Echa un vistazo a los javadocs para él here.

+0

El rendimiento de eso será o (n * k), donde n es el tamaño de la cadena de entrada, yk el número de teclas. –

+0

@Daniel ¿Has leído el código fuente para llegar a esa conclusión? Java hace algunas cosas bastante inteligentes con cadenas. Espero que exista una gran posibilidad de que supere a cualquier otra solución que se te ocurra. –

+0

@BillK Creo que podría haber querido decir que tendrías que llamar 'replaceAll' repetidamente si tienes más de una clave para reemplazar en la cadena, por lo tanto' * k'. – Svish

2

La más directa parecería ser algo en la línea de lo siguiente:

public static void main(String[] args) { 
    String tokenString = "Hello {FIRST_NAME}, this is a personalized message for you."; 
    Map<String, String> tokenMap = new HashMap<String, String>(); 
    tokenMap.put("{FIRST_NAME}", "Jim"); 
    String transformedString = tokenString; 
    for (String token : tokenMap.keySet()) { 
     transformedString = transformedString.replace(token, tokenMap.get(token)); 
    } 
    System.out.println("New String: " + transformedString); 
} 

Se coloca a través de todas sus fichas y reemplaza cada símbolo, con lo que necesita, y utiliza el método de cadena estándar para el reemplazo, omitiendo así todas las frustraciones de RegEx.

+2

Eso significaría leer toda la cadena para cada token. Si tiene k tokens y n bytes para procesar, entonces el algoritmo tendrá orden o (n * k). Muy ineficiente. –

+1

Teóricamente, es o (n * k) como se dijo, pero su afirmación me parece una optimización prematura. Sin saber más sobre la frecuencia con que se llama este algoritmo, cuántos tokens están presentes en la cadena, cuánto dura la cadena y qué tan crítico es el ahorro de tiempo, es imposible decir qué tan grande es el impacto de la ineficiencia. Si esto solo se llama una vez con un tiempo de ejecución total de 10 ms aunque podría ser tan eficiente en 1 ms (por ejemplo) ciertamente es un orden de magnitud más lento de lo que podría ser, pero la penalización de rendimiento es realmente tan sustancial en el gran esquema de las cosas? – Peter

3

Con java.util.regex importación *:.

Pattern p = Pattern.compile("{([^{}]*)}"); 
Matcher m = p.matcher(line); // line being "Hello, {FIRST_NAME}..." 
while (m.find) { 
    String key = m.group(1); 
    if (map.containsKey(key)) { 
    String value= map.get(key); 
    m.replaceFirst(value); 
    } 
} 

Por lo tanto, se recomienda la expresión regular, ya que puede identificar fácilmente los lugares que requieren sustitución en la cadena, así como extraer el nombre de la clave para la sustitución. Es mucho más eficiente que romper toda la cadena.

Probablemente quiera hacer un bucle con la línea Matcher dentro y la línea Pattern fuera, para que pueda reemplazar todas las líneas. El patrón nunca necesita ser recompilado, y es más eficiente evitar hacerlo innecesariamente.

+1

m.grupo (0) es la coincidencia completa (es decir, {FIRST_NAME}). m.group (1) sería solo la clave (es decir, FIRST_NAME). –

+0

gracias por la captura –

2

Dependiendo de cuán ridículamente compleja sea su cadena, podría intentar usar un lenguaje de plantillas de cadenas más serio, como Velocity. En el caso de la velocidad, que haría algo como esto:

Velocity.init(); 
VelocityContext context = new VelocityContext(); 
context.put("name", "Bob"); 
StringWriter output = new StringWriter(); 
Velocity.evaluate(context, output, "", 
     "Hello, #name, this is a personalized message for you."); 
System.out.println(output.toString()); 

pero que es probable que una exageración si sólo desea reemplazar uno o dos valores.

1
import java.util.HashMap; 

public class ReplaceTest { 

    public static void main(String[] args) { 
    HashMap<String, String> map = new HashMap<String, String>(); 

    map.put("FIRST_NAME", "Jim"); 
    map.put("LAST_NAME", "Johnson"); 
    map.put("PHONE",  "410-555-1212"); 

    String s = "Hello {FIRST_NAME} {LAST_NAME}, this is a personalized message for you."; 

    for (String key : map.keySet()) { 
     s = s.replaceAll("\\{" + key + "\\}", map.get(key)); 
    } 

    System.out.println(s); 
    } 

} 
11

Gracias a todos por las respuestas!

La respuesta de Gizmo fue definitivamente fuera de la caja, y una gran solución, pero lamentablemente no es apropiada ya que el formato no puede limitarse a lo que hace la clase de formateador en este caso.

Adam Paynter realmente llegó al corazón de la cuestión, con el patrón correcto.

Peter Nix y Sean Bright tuvieron una gran solución para evitar todas las complejidades de la expresión regular, pero necesitaba generar algunos errores si había tokens malos, lo que no funcionó.

Pero en términos de hacer un regex y un ciclo de reemplazo razonable, esta es la respuesta que obtuve (con un poco de ayuda de Google y la respuesta existente, incluido el comentario de Sean Bright sobre cómo usar el grupo (1) frente a grupo()):

private static Pattern tokenPattern = Pattern.compile("\\{([^}]*)\\}"); 

public static String process(String template, Map<String, Object> params) { 
    StringBuffer sb = new StringBuffer(); 
    Matcher myMatcher = tokenPattern.matcher(template); 
    while (myMatcher.find()) { 
     String field = myMatcher.group(1); 
     myMatcher.appendReplacement(sb, ""); 
     sb.append(doParameter(field, params)); 
    } 
    myMatcher.appendTail(sb); 
    return sb.toString(); 
} 

Dónde doParameter obtiene el valor del mapa y la convierte en una cadena y lanza una excepción si no está allí.

Tenga en cuenta que también cambié el patrón para encontrar llaves vacías (es decir {}), ya que es una condición de error comprobada explícitamente.

EDIT: Tenga en cuenta que appendReplacement no es independiente del contenido de la cadena. Según los javadocs, reconoce $ y la barra diagonal inversa como un carácter especial, así que agregué algunos de escape para manejar eso al ejemplo anterior. No se hace de la manera más consciente del rendimiento, pero en mi caso no es lo suficientemente grande como para intentar micro-optimizar las creaciones de cuerdas.

Gracias al comentario de Alan M, esto puede hacerse aún más simple para evitar los problemas de carácter especial de appendReplacement.

+0

Esa es una muy buena respuesta. Es una lástima que no haya leído los JavaDocs a fondo ... –

+1

No necesita escapar del reemplazo, simplemente manténgalo alejado de appendReplacement(): 'myMatcher.appendReplacement (sb," "); sb.append (doParameter (field, params)); ' –

+0

Gracias por incluir esta actualización ¡muy útil pregunta y respuesta! –

0

Generalmente usamos MessageFormat en un caso como este, junto con la carga del texto del mensaje real de un ResourceBundle. Esto le brinda la ventaja adicional de ser amigable con G10N.

Cuestiones relacionadas