2009-07-16 5 views
9

eine Zeichenfolge wie so Gegeben Token in einer Zeichenfolge ersetzen:Wie ohne StringTokenizer

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

Wo FIRST_NAME ein beliebiges Zeichen (einen Schlüssel in der Karte, um die Methode übergeben) ist, eine Routine zu schreiben, die machen würde diese Zeichenfolge in:

Hello Jim, this is a personalized message for you. 

eine Karte mit einem Eintrag FIRST_NAME -> Jim gegeben.

Es scheint, dass StringTokenizer der geradlinigste Ansatz ist, aber die Javadocs sagen wirklich, dass Sie den Regex-Ansatz bevorzugen sollten. Wie würden Sie das in einer Regex-basierten Lösung tun?

+0

versuchen Sie http://github.com/niesfisch/tokenreplacer/ – Marcel

Antwort

4

Versuchen Sie folgendes:

Hinweis: Die author's final solution baut auf dieser Probe und ist viel übersichtlicher.

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

Das wird das Muster für jede Zeile neu kompilieren. Ich bevorzuge meine Muster so vorkompiliert wie möglich! :-) Außerdem solltest du nach dem Vorhandensein des Tokens suchen. –

+0

Ich meine, überprüfen Sie, ob die Tokenexisten in der Karte sind. –

+0

Sie können das 'tokenPattern' einfach zu einer Instanzvariable jeder Klasse machen, die diese Methode enthält, damit sie nicht jedes Mal kompiliert wird. Der Code passt sich automatisch der Situation an, in der kein Token gefunden wird ('output.append (textstring (cursor))'). –

0

Die Dokumente bedeuten, dass Sie lieber einen Regex-basierten Tokenizer, IIRC, schreiben sollten. Was für Sie besser funktionieren könnte, ist ein Standard Regex Search-Replace.

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

Überprüfen Sie die Javadocs dafür here.

+0

Die Leistung davon wird o (n * k) sein, wobei n die Größe der Eingabezeichenfolge und k die Anzahl der Schlüssel ist. –

+0

@Daniel Hast du den Quellcode gelesen, um zu dieser Schlussfolgerung zu kommen? Java macht einige ziemlich intelligente Dinge mit Strings. Ich würde erwarten, dass es eine sehr gute Chance gibt, dass es jede andere Lösung übertrifft, die Sie sich vorstellen können. –

+0

@BillK Ich denke, er könnte bedeuten, dass Sie 'replaceAll' wiederholt aufrufen müssen, wenn Sie mehr als einen Schlüssel in der Zeichenfolge ersetzen müssen, daher' * k'. – Svish

2

Die direkteste scheint etwas entlang der Linien von dieser zu sein:

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

Es durch alle Token-Loops und ersetzt jeden Token mit, was Sie brauchen, und verwendet die Standardmethode String für den Austausch und überspringt so die gesamten RegEx Frustrationen.

+2

Das würde bedeuten, die ganze Zeichenfolge für jeden Token zu lesen. Wenn Sie k Token und n Bytes verarbeiten müssen, hat der Algorithmus die Ordnung o (n * k). Sehr ineffizient. –

+1

Theoretisch ist es o (n * k) wie gesagt, aber Ihre Aussage fühlt sich für mich wie eine vorzeitige Optimierung an. Ohne zu wissen, wie oft dieser Algorithmus aufgerufen wird, wie viele Token in der Zeichenfolge vorhanden sind, wie lange die Zeichenfolge ist und wie wichtig die Zeitersparnis ist, lässt sich nicht sagen, wie groß die Ineffizienz ist. Wenn dies nur einmal mit einer Gesamtlaufzeit von 10 ms aufgerufen wird, obwohl es bei 1 ms (zum Beispiel) genauso effizient sein könnte, ist es sicherlich eine Größenordnung langsamer als es sein könnte, aber ist die Leistungseinbuße wirklich so erheblich im großen Schema der Dinge? – Peter

3

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

So wird die Regex empfohlen, da sie leicht die Orte identifizieren können, die Substitution in der Zeichenfolge erfordern, sowie das Extrahieren des Namens des Schlüssels für die Substitution. Es ist viel effizienter, als die ganze Saite zu zerbrechen.

Sie möchten wahrscheinlich mit der Matcher-Linie innen und der Pattern-Linie außerhalb schleifen, damit Sie alle Linien ersetzen können. Das Muster muss nie neu kompiliert werden, und es ist effizienter, dies unnötigerweise zu vermeiden.

+1

m.group (0) ist die gesamte Übereinstimmung (d. H. {FIRST_NAME}). m.group (1) wäre nur der Schlüssel (d. h. FIRST_NAME). –

+0

danke für den Fang –

2

Je nachdem, wie lächerlich komplex Ihre Zeichenfolge ist, können Sie versuchen, eine strengere String-Templating-Sprache wie Velocity zu verwenden. In Velocity Fall würden Sie so etwas tun:

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

Aber das ist wahrscheinlich übertrieben, wenn Sie nur ein oder zwei Werte ersetzt werden soll.

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

Vielen Dank für die Antworten!

Gizmos Antwort war definitiv out of the box, und eine großartige Lösung, aber leider nicht geeignet, da das Format nicht auf das beschränkt werden kann, was die Formatter-Klasse in diesem Fall tut.

Adam Paynter kam mit dem richtigen Muster wirklich zum Kern der Sache.

Peter Nix und Sean Bright hatten einen großen Workaround, um alle Komplexitäten der Regex zu vermeiden, aber ich musste einige Fehler melden, wenn es schlechte Token gab, was das nicht tat.

Aber in Bezug auf sowohl eine Regex und eine vernünftige Replace-Schleife, das ist die Antwort, die ich kam (mit ein wenig Hilfe von Google und die vorhandene Antwort, einschließlich Sean Bright's Kommentar zur Verwendung der Gruppe (1) vs Gruppe()):

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

Wo doParameter den Wert aus der Karte bekommt und wandelt sie in einen String und wirft eine Ausnahme, wenn sie nicht da ist.

Beachten Sie auch, dass ich das Muster geändert habe, um leere Klammern (d. H. {}) Zu finden, da dies eine Fehlerbedingung ist, auf die ausdrücklich geprüft wird.

EDIT: Beachten Sie, dass appendReplacement nicht agnostisch über den Inhalt der Zeichenfolge ist. Für die Javadocs erkennt es $ und Backslash als Sonderzeichen, also habe ich etwas Escaping hinzugefügt, um das mit dem obigen Beispiel zu behandeln. Nicht auf die leistungsfähigste Weise gemacht, aber in meinem Fall ist es nicht groß genug, um zu versuchen, die String-Kreationen zu optimieren.

Dank des Kommentars von Alan M kann dies noch einfacher gemacht werden, um die speziellen Charakterprobleme von appendReplacement zu vermeiden.

+0

Das ist eine sehr gute Antwort. Es ist eine Schande, dass ich die JavaDocs nicht gründlich gelesen habe ... –

+1

Sie müssen die Ersetzung nicht umgehen, halten Sie sie einfach von appendReplacement() fern: 'myMatcher.appendReplacement (sb," "); sb.append (doParameter (field, params)); ' –

+0

Danke für die Aufnahme dieses Updates sehr nützliche Frage und Antwort! –

0

Im Allgemeinen würden wir MessageFormat in einem solchen Fall verwenden, zusammen mit dem Laden des eigentlichen Nachrichtentextes von einem ResourceBundle. Dies gibt Ihnen den zusätzlichen Vorteil, G10N freundlich zu sein.