Bagaimana cara mengganti satu set token dalam String Java?

106

Saya memiliki template String berikut: "Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]".

Saya juga memiliki variabel String untuk nama, nomor faktur, dan tanggal jatuh tempo - apa cara terbaik untuk mengganti token di template dengan variabel?

(Perhatikan bahwa jika suatu variabel mengandung token, itu TIDAK boleh diganti).


EDIT

Dengan terima kasih kepada @laginimaineb dan @ alan-moore, inilah solusi saya:

public static String replaceTokens(String text, 
                                   Map<String, String> replacements) {
    Pattern pattern = Pattern.compile("\\[(.+?)\\]");
    Matcher matcher = pattern.matcher(text);
    StringBuffer buffer = new StringBuffer();

    while (matcher.find()) {
        String replacement = replacements.get(matcher.group(1));
        if (replacement != null) {
            // matcher.appendReplacement(buffer, replacement);
            // see comment 
            matcher.appendReplacement(buffer, "");
            buffer.append(replacement);
        }
    }
    matcher.appendTail(buffer);
    return buffer.toString();
}
Menandai
sumber
Satu hal yang perlu diperhatikan, adalah bahwa StringBuffer sama dengan StringBuilder yang baru saja disinkronkan. Namun, karena dalam contoh ini Anda tidak perlu menyinkronkan pembuatan String, Anda mungkin lebih baik menggunakan StringBuilder (meskipun memperoleh kunci hampir merupakan operasi tanpa biaya).
laginimaineb
1
Sayangnya, Anda harus menggunakan StringBuffer dalam kasus ini; itulah yang diharapkan oleh metode appendXXX (). Mereka sudah ada sejak Java 4, dan StringBuilder tidak ditambahkan hingga Java 5. Seperti yang Anda katakan, ini bukan masalah besar, hanya mengganggu.
Alan Moore
4
Satu hal lagi: appendReplacement (), seperti metode replaceXXX (), mencari referensi grup tangkapan seperti $ 1, $ 2, dll., Dan menggantinya dengan teks dari grup tangkapan terkait. Jika teks pengganti Anda mungkin berisi tanda dolar atau garis miring terbalik (yang digunakan untuk menghindari tanda dolar), Anda mungkin mengalami masalah. Cara termudah untuk mengatasinya adalah dengan memecah operasi append menjadi dua langkah seperti yang telah saya lakukan pada kode di atas.
Alan Moore
Alan - sangat terkesan Anda melihatnya. Saya tidak berpikir masalah sesederhana itu akan begitu sulit dipecahkan!
Tandai

Jawaban:

65

Cara paling efisien adalah menggunakan matcher untuk terus menemukan ekspresi dan menggantinya, lalu menambahkan teks ke pembuat string:

Pattern pattern = Pattern.compile("\\[(.+?)\\]");
Matcher matcher = pattern.matcher(text);
HashMap<String,String> replacements = new HashMap<String,String>();
//populate the replacements map ...
StringBuilder builder = new StringBuilder();
int i = 0;
while (matcher.find()) {
    String replacement = replacements.get(matcher.group(1));
    builder.append(text.substring(i, matcher.start()));
    if (replacement == null)
        builder.append(matcher.group(0));
    else
        builder.append(replacement);
    i = matcher.end();
}
builder.append(text.substring(i, text.length()));
return builder.toString();
laginimaineb
sumber
10
Beginilah cara saya melakukannya, kecuali saya akan menggunakan metode appendReplacement () dan appendTail () dari Matcher untuk menyalin teks yang tidak cocok; tidak perlu melakukan itu dengan tangan.
Alan Moore
5
Sebenarnya metode appendReplacement () dan appentTail () memerlukan StringBuffer, yang tersinkronisasi (yang tidak berguna di sini). Jawaban yang diberikan menggunakan StringBuilder, yang 20% ​​lebih cepat dalam pengujian saya.
dube
103

Saya benar-benar tidak berpikir Anda perlu menggunakan mesin templating atau semacamnya untuk ini. Anda dapat menggunakan String.formatmetode ini, seperti:

String template = "Hello %s Please find attached %s which is due on %s";

String message = String.format(template, name, invoiceNumber, dueDate);
Paul Morie
sumber
4
Satu kelemahan dari ini adalah Anda harus meletakkan parameter dalam urutan yang benar
gerrytan
Hal lainnya adalah Anda tidak dapat menentukan format token pengganti Anda sendiri.
Franz D.
lain adalah bahwa itu tidak bekerja secara dinamis, dapat memiliki kumpulan data kunci / nilai dan kemudian menerapkannya ke string apa pun
Brad Parks
43

Sayangnya metode String.format nyaman yang disebutkan di atas hanya tersedia mulai dengan Java 1.5 (yang seharusnya cukup standar saat ini, tetapi Anda tidak pernah tahu). Selain itu, Anda juga dapat menggunakan class MessageFormat Java untuk mengganti placeholder.

Ini mendukung placeholder dalam bentuk '{number}', jadi pesan Anda akan terlihat seperti "Halo {0} Silakan temukan lampiran {1} yang jatuh tempo pada {2}". String ini dapat dengan mudah dieksternalisasi menggunakan ResourceBundles (misalnya untuk pelokalan dengan beberapa lokal). Penggantian akan dilakukan menggunakan metode static'format 'kelas MessageFormat:

String msg = "Hello {0} Please find attached {1} which is due on {2}";
String[] values = {
  "John Doe", "invoice #123", "2009-06-30"
};
System.out.println(MessageFormat.format(msg, values));
toolkit
sumber
3
Saya tidak dapat mengingat nama MessageFormat, dan agak konyol berapa banyak Googling yang harus saya lakukan untuk menemukan jawaban ini. Semua orang bertindak seperti itu String.format atau menggunakan pihak ketiga, melupakan utilitas yang sangat berguna ini.
Patrick
1
Ini telah tersedia sejak 2004 - mengapa saya baru mempelajarinya sekarang, di tahun 2017? Saya refactoring beberapa kode yang tercakup dalam StringBuilder.append()s dan saya berpikir "Tentunya ada cara yang lebih baik ... sesuatu yang lebih Pythonic ..." - dan sial, saya pikir metode ini mungkin mendahului metode pemformatan Python. Sebenarnya ... ini mungkin lebih tua dari 2002 ... Saya tidak dapat menemukan kapan ini benar-benar muncul ...
ArtOfWarfare
42

Anda dapat mencoba menggunakan pustaka template seperti Apache Velocity.

http://velocity.apache.org/

Berikut ini contohnya:

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.StringWriter;

public class TemplateExample {
    public static void main(String args[]) throws Exception {
        Velocity.init();

        VelocityContext context = new VelocityContext();
        context.put("name", "Mark");
        context.put("invoiceNumber", "42123");
        context.put("dueDate", "June 6, 2009");

        String template = "Hello $name. Please find attached invoice" +
                          " $invoiceNumber which is due on $dueDate.";
        StringWriter writer = new StringWriter();
        Velocity.evaluate(context, writer, "TemplateName", template);

        System.out.println(writer);
    }
}

Outputnya adalah:

Halo Mark. Temukan faktur terlampir 42123 yang jatuh tempo pada tanggal 6 Juni 2009.
hallidave
sumber
Saya telah menggunakan kecepatan di masa lalu. Bekerja dengan baik.
Hardwareguy
4
setuju, mengapa menemukan kembali roda
objek
6
Agak berlebihan menggunakan seluruh pustaka untuk tugas sederhana seperti ini. Velocity memiliki banyak fitur lain, dan saya sangat yakin itu tidak cocok untuk tugas sederhana seperti ini.
Andrei Ciobanu
24

Anda dapat menggunakan pustaka template untuk penggantian template yang kompleks.

FreeMarker adalah pilihan yang sangat bagus.

http://freemarker.sourceforge.net/

Tetapi untuk tugas sederhana, ada kelas utilitas sederhana yang dapat membantu Anda.

org.apache.commons.lang3.text.StrSubstitutor

Ini sangat kuat, dapat disesuaikan, dan mudah digunakan.

Kelas ini mengambil sepotong teks dan mengganti semua variabel di dalamnya. Definisi default variabel adalah $ {variableName}. Awalan dan sufiks dapat diubah melalui konstruktor dan metode set.

Nilai variabel biasanya diselesaikan dari peta, tetapi juga dapat ditentukan dari properti sistem, atau dengan menyediakan pemecah variabel khusus.

Misalnya, jika Anda ingin mengganti variabel lingkungan sistem menjadi string template, berikut kodenya:

public class SysEnvSubstitutor {
    public static final String replace(final String source) {
        StrSubstitutor strSubstitutor = new StrSubstitutor(
                new StrLookup<Object>() {
                    @Override
                    public String lookup(final String key) {
                        return System.getenv(key);
                    }
                });
        return strSubstitutor.replace(source);
    }
}
Li Ying
sumber
2
org.apache.commons.lang3.text.StrSubstitutor bekerja sangat baik untuk saya
ps0604
17
System.out.println(MessageFormat.format("Hello {0}! You have {1} messages", "Join",10L));

Output: Halo Gabung! Anda memiliki 10 pesan "

pengguna2845137
sumber
2
John dengan jelas memeriksa pesannya sesering saya memeriksa folder "spam" saya karena folder itu Long.
Hemmels
9

Itu tergantung di mana data sebenarnya yang ingin Anda ganti berada. Anda mungkin memiliki Peta seperti ini:

Map<String, String> values = new HashMap<String, String>();

berisi semua data yang bisa diganti. Kemudian Anda dapat mengulang peta dan mengubah semua yang ada di String sebagai berikut:

String s = "Your String with [Fields]";
for (Map.Entry<String, String> e : values.entrySet()) {
  s = s.replaceAll("\\[" + e.getKey() + "\\]", e.getValue());
}

Anda juga bisa mengulang String dan menemukan elemen di peta. Tapi itu sedikit lebih rumit karena Anda perlu mengurai String yang mencari []. Anda dapat melakukannya dengan ekspresi reguler menggunakan Pola dan Pencocokan.

Ricardo Marimon
sumber
9
String.format("Hello %s Please find attached %s which is due on %s", name, invoice, date)
Bruno Ranschaert
sumber
1
Terima kasih - tetapi dalam kasus saya, string template dapat dimodifikasi oleh pengguna, jadi saya tidak dapat memastikan urutan token
Tandai
3

Solusi saya untuk mengganti token gaya $ {variable} (terinspirasi oleh jawaban di sini dan oleh Spring UriTemplate):

public static String substituteVariables(String template, Map<String, String> variables) {
    Pattern pattern = Pattern.compile("\\$\\{(.+?)\\}");
    Matcher matcher = pattern.matcher(template);
    // StringBuilder cannot be used here because Matcher expects StringBuffer
    StringBuffer buffer = new StringBuffer();
    while (matcher.find()) {
        if (variables.containsKey(matcher.group(1))) {
            String replacement = variables.get(matcher.group(1));
            // quote to work properly with $ and {,} signs
            matcher.appendReplacement(buffer, replacement != null ? Matcher.quoteReplacement(replacement) : "null");
        }
    }
    matcher.appendTail(buffer);
    return buffer.toString();
}
mihu86
sumber
1

Dengan Apache Commons Library, Anda cukup menggunakan Stringutils.replaceEach :

public static String replaceEach(String text,
                             String[] searchList,
                             String[] replacementList)

Dari dokumentasi :

Mengganti semua kemunculan String dalam String lain.

Referensi null yang diteruskan ke metode ini adalah tanpa operasi, atau jika "string penelusuran" atau "string yang akan diganti" bernilai null, penggantian itu akan diabaikan. Ini tidak akan berulang. Untuk penggantian berulang, panggil metode kelebihan beban.

 StringUtils.replaceEach(null, *, *)        = null

  StringUtils.replaceEach("", *, *)          = ""

  StringUtils.replaceEach("aba", null, null) = "aba"

  StringUtils.replaceEach("aba", new String[0], null) = "aba"

  StringUtils.replaceEach("aba", null, new String[0]) = "aba"

  StringUtils.replaceEach("aba", new String[]{"a"}, null)  = "aba"

  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""})  = "b"

  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"})  = "aba"

  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})  = "wcte"
  (example of how it does not repeat)

StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"})  = "dcte"
AR1
sumber
1

FYI

Dalam bahasa baru Kotlin, Anda dapat menggunakan "Template String" di kode sumber Anda secara langsung, tidak ada library pihak ketiga atau mesin template yang perlu melakukan penggantian variabel.

Itu adalah ciri dari bahasa itu sendiri.

Lihat: https://kotlinlang.org/docs/reference/basic-types.html#string-templates

Li Ying
sumber
0

Di masa lalu, saya telah memecahkan masalah semacam ini dengan StringTemplate dan Template Groovy .

Pada akhirnya, keputusan menggunakan mesin template atau tidak harus didasarkan pada faktor-faktor berikut:

  • Apakah Anda akan memiliki banyak template ini dalam aplikasi?
  • Apakah Anda memerlukan kemampuan untuk mengubah template tanpa memulai ulang aplikasi?
  • Siapa yang akan mempertahankan template ini? Seorang programmer Java atau analis bisnis yang terlibat dalam proyek?
  • Apakah Anda perlu kemampuan untuk meletakkan logika di template Anda, seperti teks bersyarat berdasarkan nilai dalam variabel?
  • Apakah Anda memerlukan kemampuan untuk memasukkan templat lain ke dalam templat?

Jika salah satu hal di atas berlaku untuk proyek Anda, saya akan mempertimbangkan untuk menggunakan mesin templating, yang sebagian besar menyediakan fungsionalitas ini, dan banyak lagi.

Kerikil Francois
sumber
0

Saya dulu

String template = "Hello %s Please find attached %s which is due on %s";

String message = String.format(template, name, invoiceNumber, dueDate);
mtwom.dll
sumber
2
Itu akan berhasil, tetapi dalam kasus saya, string template dapat disesuaikan oleh pengguna, jadi saya tidak tahu dalam urutan apa token akan muncul.
Tandai
0

Variabel berikut menggantikan formulir <<VAR>>, dengan nilai-nilai yang dicari dari Peta. Anda dapat mengujinya secara online di sini

Misalnya, dengan string input berikut

BMI=(<<Weight>>/(<<Height>>*<<Height>>)) * 70
Hi there <<Weight>> was here

dan nilai variabel berikut

Weight, 42
Height, HEIGHT 51

menghasilkan berikut ini

BMI=(42/(HEIGHT 51*HEIGHT 51)) * 70

Hi there 42 was here

Ini kodenya

  static Pattern pattern = Pattern.compile("<<([a-z][a-z0-9]*)>>", Pattern.CASE_INSENSITIVE);

  public static String replaceVarsWithValues(String message, Map<String,String> varValues) {
    try {
      StringBuffer newStr = new StringBuffer(message);
      int lenDiff = 0;
      Matcher m = pattern.matcher(message);
      while (m.find()) {
        String fullText = m.group(0);
        String keyName = m.group(1);
        String newValue = varValues.get(keyName)+"";
        String replacementText = newValue;
        newStr = newStr.replace(m.start() - lenDiff, m.end() - lenDiff, replacementText);
        lenDiff += fullText.length() - replacementText.length();
      }
      return newStr.toString();
    } catch (Exception e) {
      return message;
    }
  }


  public static void main(String args[]) throws Exception {
      String testString = "BMI=(<<Weight>>/(<<Height>>*<<Height>>)) * 70\n\nHi there <<Weight>> was here";
      HashMap<String,String> values = new HashMap<>();
      values.put("Weight", "42");
      values.put("Height", "HEIGHT 51");
      System.out.println(replaceVarsWithValues(testString, values));
  }

dan meskipun tidak diminta, Anda dapat menggunakan pendekatan serupa untuk mengganti variabel dalam string dengan properti dari file application.properties Anda, meskipun ini mungkin sudah dilakukan:

private static Pattern patternMatchForProperties =
      Pattern.compile("[$][{]([.a-z0-9_]*)[}]", Pattern.CASE_INSENSITIVE);

protected String replaceVarsWithProperties(String message) {
    try {
      StringBuffer newStr = new StringBuffer(message);
      int lenDiff = 0;
      Matcher m = patternMatchForProperties.matcher(message);
      while (m.find()) {
        String fullText = m.group(0);
        String keyName = m.group(1);
        String newValue = System.getProperty(keyName);
        String replacementText = newValue;
        newStr = newStr.replace(m.start() - lenDiff, m.end() - lenDiff, replacementText);
        lenDiff += fullText.length() - replacementText.length();
      }
      return newStr.toString();
    } catch (Exception e) {
      return message;
    }
  }
Brad Parks
sumber