2009-07-16 17 views
9

Data una stringa in questo modo:Come sostituire i token in una stringa senza StringTokenizer

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

Dove FIRST_NAME è un token arbitraria (una chiave in una mappa passato al metodo), di scrivere una routine atta a rendere quella stringa in:

Hello Jim, this is a personalized message for you. 

data una mappa con una voce FIRST_NAME -> Jim.

Sembrerebbe che StringTokenizer sia l'approccio più semplice, ma i Javadoc dicono davvero che si dovrebbe preferire l'uso dell'apice di regex. Come faresti in una soluzione basata su regex?

+0

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

risposta

4

Prova questo:

Nota: Il author's final solution costruisce su questo campione ed è molto più conciso.

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

Questo ricompilerà il modello per ogni riga. Preferisco i miei schemi precompilati il ​​più possibile! :-) Inoltre, è meglio controllare l'esistenza del token. –

+0

Voglio dire, controlla che i tokenicisti siano sulla mappa. –

+0

Puoi semplicemente rendere 'tokenPattern' una variabile di istanza di qualunque classe conterrà questo metodo per evitare di compilarlo ogni volta. Il codice si adatta automaticamente alla situazione in cui non viene rilevato alcun token ('output.append (text.substring (cursor))'). –

0

I documenti indicano che si preferisce scrivere un tokenizzatore basato su espressioni regolari, IIRC. Quello che potrebbe funzionare meglio per te è una ricerca-regex standard-sostitutiva.

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

Controllare i javadoc per esso here.

+0

Le prestazioni saranno o (n * k), dove n è la dimensione della stringa di input e k il numero di tasti. –

+0

@Daniel Hai letto il codice sorgente per giungere a questa conclusione? Java fa cose piuttosto intelligenti con stringhe. Mi aspetto che ci siano ottime possibilità che superi le altre soluzioni che potresti ottenere. –

+0

@BillK Penso che avrebbe potuto significare che dovresti chiamare 'replaceAll' ripetutamente se hai più di una chiave da sostituire nella stringa, quindi' * k'. – Svish

2

L'attaccante più dritto sembrerebbe essere qualcosa sulla falsariga di questo:

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

E scorre tutti i tuoi gettoni e sostituisce ogni token con quello che ti serve, e utilizza il metodo stringa standard per la sostituzione, saltando così tutte le frustrazioni RegEx.

+2

Ciò significherebbe leggere l'intera stringa per ogni token. Se si hanno k token e n byte da elaborare, allora l'algoritmo avrà l'ordine o (n * k). Molto inefficiente. –

+1

In teoria, è o (n * k) come affermato, ma la tua affermazione mi sembra un'ottimizzazione prematura. Senza sapere di più su quanto spesso viene chiamato questo algoritmo, quanti token sono presenti nella stringa, quanto è lunga la stringa e quanto è importante il risparmio di tempo, è impossibile dire quanto grande è l'impatto dell'inefficienza. Se questo è chiamato solo una volta con un tempo di esecuzione totale di 10 ms anche se potrebbe essere efficiente a 1 ms (ad esempio) certamente è un ordine di grandezza più lento di quanto potrebbe essere, ma è la penalità delle prestazioni davvero sostanziale nel grande schema delle cose? – Peter

3

Con import java.util.regex *:.

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

Così, l'espressione regolare è raccomandato perché può facilmente identificare i luoghi che richiedono la sostituzione nella stringa, così come l'estrazione del nome della chiave per sostituzione. È molto più efficiente di rompere l'intera stringa.

Probabilmente vorrai eseguire il loop con la linea Matcher all'interno e la linea Pattern all'esterno, in modo da poter sostituire tutte le linee. Il pattern non deve mai essere ricompilato ed è più efficiente evitare di farlo inutilmente.

+1

m.group (0) è l'intera corrispondenza (ad esempio {FIRST_NAME}). m.group (1) sarebbe solo la chiave (cioè FIRST_NAME). –

+0

grazie per la presa –

2

A seconda di quanto sia ridicolmente complessa la stringa, è possibile provare a utilizzare un linguaggio di stringa più serio, come Velocity. Nel caso di Velocity, si farebbe qualcosa di simile:

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

ma che è probabilmente eccessivo se desideri solo per sostituire uno o due valori.

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

Grazie a tutti per le risposte!

La risposta di Gizmo era decisamente fuori dagli schemi e un'ottima soluzione, ma sfortunatamente non appropriata in quanto il formato non può essere limitato a ciò che la classe del formatore fa in questo caso.

Adam Paynter è arrivato davvero al cuore della questione, con lo schema giusto.

Peter Nix e Sean Bright hanno avuto una soluzione eccezionale per evitare tutte le complessità della regex, ma avevo bisogno di aumentare alcuni errori se c'erano cattivi token, cosa che non andava.

Ma in termini di una regex e un ciclo di sostituzione ragionevole, questa è la risposta che ho trovato (con un piccolo aiuto da parte di Google e la risposta esistente, compreso il commento di Sean Bright su come usare group (1) vs gruppo()):

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

Dove doParameter ottiene il valore di mappa e lo converte in una stringa e genera un'eccezione se non è lì.

Nota inoltre ho modificato il modello per trovare le parentesi graffe vuote (ad esempio {}), poiché si tratta di una condizione di errore verificata esplicitamente.

MODIFICA: Nota che appendReplacement non è agnostico sul contenuto della stringa. Per javadocs, riconosce $ e backslash come carattere speciale, quindi ho aggiunto un po 'di escape per gestirlo nell'esempio sopra. Non fatto nel modo più consapevole delle prestazioni, ma nel mio caso non è un affare abbastanza grande per valere la pena di tentare di micro-ottimizzare le creazioni di stringhe.

Grazie al commento di Alan M, questo può essere reso ancora più semplice per evitare i problemi di carattere speciale di appendReplacement.

+0

Questa è un'ottima risposta. È un peccato non aver letto a fondo il JavaDocs ... –

+1

Non è necessario sfuggire alla sostituzione, basta tenerlo lontano da appendReplacement(): 'myMatcher.appendReplacement (sb," "); sb.append (doParameter (field, params)); ' –

+0

Grazie per aver incluso questo aggiornamento domanda e risposta molto utili! –

0

Generalmente usiamo MessageFormat in un caso come questo, insieme al caricamento del testo del messaggio effettivo da un ResourceBundle. Questo ti dà il vantaggio di essere amichevole con G10N.

Problemi correlati