2009-07-16 4 views
9

Получая строку следующим образом:Как заменить маркеры в строке без StringTokenizer

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

Где FIRST_NAME является произвольным маркером (ключ в карте передается метода), чтобы написать подпрограмму, которая превратила бы эта строка в:

Hello Jim, this is a personalized message for you. 

с карточкой с пометкой FIRST_NAME -> Джим.

Казалось бы, StringTokenizer - это самый прямой подход, но Javadocs действительно говорит, что вы должны использовать apryach regex. Как вы это сделаете в решении на основе регулярных выражений?

+0

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

ответ

4

Попробуйте это:

Примечание:author's final solution строит на этом образце, и гораздо более кратким.

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

Это позволит перекомпилировать шаблон для каждой строки. Я предпочитаю, чтобы мои шаблоны были предварительно скомпилированы, насколько это возможно! :-) Кроме того, вам лучше проверить наличие токена. –

+0

Я имею в виду, проверьте, что токены на карте. –

+0

Вы можете просто сделать 'tokenPattern' переменной экземпляра любого класса, который будет содержать этот метод, чтобы избежать его компиляции каждый раз. Код автоматически будет учитывать ситуацию, при которой не будет обнаружен токен ('output.append (text.substring (cursor))'). –

0

Документы означают, что вам следует предпочесть писать токенизатор, основанный на регулярном выражении, IIRC. Что может работать лучше для вас, это стандартный поиск в регулярном выражении.

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

Посмотрите на него javadocs here.

+0

Производительность этого будет o (n * k), где n - размер входной строки, а k - количество ключей. –

+0

@ Даниэль Вы прочитали исходный код, чтобы прийти к такому выводу? Java делает некоторые довольно интеллектуальные вещи со строками. Я бы ожидал, что есть очень хороший шанс, что он превзойдет любое другое решение, которое вы могли бы придумать. –

+0

@BillK Я думаю, он мог бы означать, что вам придется многократно называть 'replaceAll', если в строке есть более одного ключа, поэтому' * k'. – Svish

2

Самый прямой, казалось бы, что-то вдоль линий этого:

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

Он перебирает все ваши маркеры и заменяет каждый маркер с тем, что вам нужно, и использует стандартный метод String, для замены, тем самым пропуская все разочарования RegEx.

+2

Это означало бы считывание всей строки для каждого токена. Если у вас есть k токенов и n байтов для обработки, тогда алгоритм будет иметь порядок o (n * k). Очень неэффективно. –

+1

Теоретически это о (n * k), как указано, но ваше утверждение кажется мне преждевременной оптимизацией. Не зная больше о том, как часто этот алгоритм вызывается, сколько токенов присутствует в строке, как долго строка и насколько важно экономить время, невозможно сказать, насколько большое влияние оказывает неэффективность. Если это вызов только один раз с общим временем работы 10 мс, хотя он может быть таким же эффективным в 1 мс (например), конечно, он на порядок медленнее, чем это могло бы быть, но действительно является снижением производительности на самом деле, что существенное в великой схеме вещей? – Peter

3

С импорта 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); 
    } 
} 

Таким образом, регулярное выражение рекомендуется, поскольку он может легко определить места, которые требуют замены в строке, а также извлечения имени ключа для замещения. Это намного эффективнее, чем разрыв всей строки.

Возможно, вам захочется выполнить петлю с линией сопряжения внутри и с линией шаблона снаружи, чтобы вы могли заменить все линии. Шаблон никогда не нуждается в перекомпиляции, и более эффективно избегать ненужного использования.

+1

m.group (0) - полное совпадение (т. Е. {FIRST_NAME}). m.group (1) будет всего лишь ключом (то есть FIRST_NAME). –

+0

спасибо за уловку –

2

В зависимости от того, насколько смехотворно сложна ваша строка, вы можете попробовать использовать более строгий язык шаблонов, например Velocity. В случае Velocity, вы бы сделать что-то вроде этого:

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

Но это, вероятно, излишним, если вы хотите, чтобы заменить один или два значения.

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

Спасибо всем за ответы!

Ответ Gizmo определенно был вне коробки и отличное решение, но, к сожалению, не уместно, поскольку формат не может быть ограничен тем, что делает класс Formatter в этом случае.

Адам Пейнтер действительно добрался до сути дела, с правильным рисунком.

У Питера Никса и Шона Брайта было большое обходное решение, чтобы избежать всех сложностей регулярного выражения, но мне нужно было поднять некоторые ошибки, если были плохие жетоны, чего не делали.

Но с точки зрения как регулярного выражения, так и разумного цикла замены это ответ, который я получил (с небольшой помощью от Google и существующего ответа, включая комментарий Шона Брайта о том, как использовать группу (1) против группы()):

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

Где doParameter получает значение из карты и преобразует его в строку и выбрасывает исключение, если она не существует.

Обратите внимание, что я изменил шаблон, чтобы найти пустые фигурные скобки (т.е. {}), поскольку это условие ошибки явно проверено.

EDIT: Обратите внимание, что appendReplacement не является агностиком относительно содержимого строки. В javadocs он распознает $ и обратную косую черту как специальный символ, поэтому я добавил некоторое экранирование для обработки этого примера. Не сделано с максимальной эффективностью, но в моем случае это не достаточно большая сделка, чтобы попытаться микро-оптимизировать создание строк.

Благодаря комментарию от Алана М, это может быть сделано еще проще, чтобы избежать особых проблем с символом appendReplacement.

+0

Это очень хороший ответ. Жаль, что я не полностью прочитал JavaDocs ... –

+1

Вам не нужно скрывать замену, просто держите ее подальше от appendReplacement(): 'myMatcher.appendReplacement (sb," "); sb.append (doParameter (поле, параметры)); ' –

+0

Благодарим за включение этого обновления в очень полезный вопрос и ответ! –

0

Обычно мы будем использовать MessageFormat в таком случае, в сочетании с загрузкой фактического текста сообщения из ResourceBundle. Это дает вам дополнительную выгоду от дружественности G10N.