Wie kann ich zwei Saiten so ersetzen, dass eine nicht die andere ersetzt?
Lesezeit: 13 Minuten
Pikamander2
Nehmen wir an, ich habe den folgenden Code:
String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar."
story = story.replace("foo", word1);
story = story.replace("bar", word2);
Nachdem dieser Code ausgeführt wurde, wird der Wert von story wird sein "Once upon a time, there was a foo and a foo."
Ein ähnliches Problem tritt auf, wenn ich sie in der umgekehrten Reihenfolge ersetzt habe:
String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar."
story = story.replace("bar", word2);
story = story.replace("foo", word1);
Der Wert von story wird sein "Once upon a time, there was a bar and a bar."
Mein Ziel ist es zu drehen story hinein "Once upon a time, there was a bar and a foo." Wie könnte ich das erreichen?
+1 Es sollte definitiv eine Funktion geben swap(String s1, String s2, String s3) das tauscht alle Vorkommen von aus s2 mit s3und umgekehrt.
– Ryan
7. November 2014 um 3:14 Uhr
Können wir davon ausgehen, dass jedes austauschbare Wort in der Eingabe nur einmal vorkommt?
– Eis
7. November 2014 um 7:39 Uhr
Sonderfall: Was erwarten wir als Ausgabe, wenn wir „ab“ und „ba“ in „ababababababa“ vertauschen?
– Hagen von Eitzen
8. November 2014 um 19:15 Uhr
Sie haben unten einige gute Lösungen, aber verstehen Sie, warum Ihr Ansatz nicht funktioniert hat? Zuerst haben Sie “es gab ein Foo und eine Bar”. Nach dem ersten Ersetzen (“foo”->”bar”) haben Sie “there was a bar and a bar”. Sie haben jetzt 2 Vorkommen von “bar”, also tut Ihr zweites Ersetzen nicht das, was Sie erwarten – es hat keine Möglichkeit zu wissen, dass Sie nur das ersetzen möchten, das Sie beim letzten Mal nicht bereits ersetzt haben. @HagenvonEitzen Interessant. Ich würde erwarten, dass eine funktionierende Lösung die erste der beiden gefundenen Zeichenfolgen abgleicht und ersetzt und dann ab dem Ende des ersetzten Abschnitts wiederholt.
– EntwicklerInEntwicklung
9. November 2014 um 1:44 Uhr
Die Lösung von Jeroen verwende ich häufig in Texteditoren, wenn ich Massenumbenennungen durchführen muss. Es ist einfach, leicht zu verstehen, erfordert keine spezielle Bibliothek und kann mit ein wenig Nachdenken narrensicher sein.
StringUtils.replaceEach(story, new String[]{"foo", "bar"}, new String[]{"bar", "foo"})
Irgendeine Idee, was genau replaceEach intern tut?
– Marek
7. November 2014 um 15:51 Uhr
@Marek Es ist sehr wahrscheinlich, dass die Funktion eine Suche durchführt und jedes gefundene Element indiziert und dann alle ersetzt, sobald sie alle indiziert wurden.
– Benutzer820304
7. November 2014 um 16:21 Uhr
Sie können die Quelle dafür finden hier um Linie 4684.
– Jeroen Vannevel
7. November 2014 um 18:09 Uhr
Schade, dass es ein No-Op ist, wenn null ist aber bestanden.
– Benutzer1804599
9. November 2014 um 11:05 Uhr
Jeroen Vannevel
Sie verwenden einen Zwischenwert (der noch nicht im Satz vorhanden ist).
story = story.replace("foo", "lala");
story = story.replace("bar", "foo");
story = story.replace("lala", "bar");
Als Antwort auf die Kritik: Wenn Sie eine ausreichend große ungewöhnliche Saite verwenden möchten zq515sqdqs5d5sq1dqs4d1q5dqqé”&é5d4sqjshsjddjhodfqsqc, nvùq^µù;d&€sdq: d: ;)àçàçlala und verwenden, ist es unwahrscheinlich, dass ein Benutzer dies jemals eingeben wird. Der einzige Weg, um zu wissen, ob ein Benutzer dies tun wird, besteht darin, den Quellcode zu kennen, und an diesem Punkt haben Sie eine ganz andere Ebene von Sorgen.
Ja, vielleicht gibt es ausgefallene Regex-Möglichkeiten. Ich bevorzuge etwas Lesbares, von dem ich weiß, dass es mir auch nicht ausbricht.
Wiederholen Sie auch die hervorragenden Ratschläge von @David Conrad in den Kommentaren:
Verwenden Sie keine Zeichenfolge, die clever (dumm) so gewählt wurde, dass sie unwahrscheinlich ist. Verwenden Sie Zeichen aus dem Unicode Private Use Area, U+E000..U+F8FF. Entfernen Sie zuerst solche Zeichen, da sie eigentlich nicht in der Eingabe enthalten sein sollten (sie haben nur innerhalb einiger Anwendungen eine anwendungsspezifische Bedeutung), und verwenden Sie sie dann beim Ersetzen als Platzhalter.
@arshajii Ich denke, das hängt von Ihrer Definition von “besser” ab … wenn es funktioniert und akzeptabel leistungsfähig ist, fahren Sie mit der nächsten Programmieraufgabe fort und verbessern Sie es später während des Refactorings, wäre mein Ansatz.
– Matt Coubrough
6. November 2014 um 23:38 Uhr
Offensichtlich ist “lala” nur ein Beispiel. In der Produktion sollten Sie “zq515sqdqs5d5sq1dqs4d1q5dqqé”&é&€sdq:d:;)àçàçlala“.
– Jeroen Vannevel
6. November 2014 um 23:42 Uhr
Verwenden Sie keine Zeichenfolge, die clever (dumm) so gewählt wurde, dass sie unwahrscheinlich ist. Verwenden Sie Zeichen aus dem Unicode Private Use Area, U+E000..U+F8FF. Entfernen Sie zuerst solche Zeichen, da sie eigentlich nicht in der Eingabe enthalten sein sollten (sie haben nur innerhalb einiger Anwendungen eine anwendungsspezifische Bedeutung), und verwenden Sie sie dann beim Ersetzen als Platzhalter.
– David Konrad
7. November 2014 um 0:00 Uhr
Eigentlich nach dem Lesen der Unicode-FAQ dazuich denke, die Nichtzeichen im Bereich U+FDD0..U+FDEF wären eine noch bessere Wahl.
– David Konrad
7. November 2014 um 0:24 Uhr
@Taemyr Sicher, aber jemand muss die Eingabe bereinigen, oder? Ich würde erwarten, dass eine String-Ersetzungsfunktion für alle Strings funktioniert, aber diese Funktion bricht bei unsicheren Eingaben ab.
String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar.";
Pattern p = Pattern.compile("foo|bar");
Matcher m = p.matcher(story);
StringBuffer sb = new StringBuffer();
while (m.find()) {
/* do the swap... */
switch (m.group()) {
case "foo":
m.appendReplacement(sb, word1);
break;
case "bar":
m.appendReplacement(sb, word2);
break;
default:
/* error */
break;
}
}
m.appendTail(sb);
System.out.println(sb.toString());
Once upon a time, there was a bar and a foo.
Funktioniert das ggf foo, barund story alle haben unbekannte Werte?
– Stefan P
6. November 2014 um 23:55 Uhr
@StephenP Ich habe das im Wesentlichen fest codiert "foo" und "bar" Ersatzzeichenfolgen, wie sie das OP in seinem Code hatte, aber der gleiche Ansatz würde auch dann gut funktionieren, wenn diese Werte nicht bekannt sind (Sie müssten verwenden if/else if anstelle einer switch innerhalb der while-Schleife).
– arshajii
6. November 2014 um 23:57 Uhr
Sie müssen beim Erstellen der Regex vorsichtig sein. Pattern.quote wäre praktisch, bzw \Q und \E.
– David Konrad
7. November 2014 um 0:06 Uhr
@arshajii – ja, habe es mir selbst als “swapThese” -Methode bewiesen, bei der Wort1, Wort2 und Geschichte als Parameter verwendet wurden. +1
– Stefan P
7. November 2014 um 0:08 Uhr
Noch sauberer wäre es, das Muster zu verwenden (foo)|(bar) und dann gegen prüfen m.group(1) != nullum zu vermeiden, dass die passenden Wörter wiederholt werden.
– Jörn Horstmann
7. November 2014 um 17:12 Uhr
janos
Dies ist kein einfaches Problem. Und je mehr Suchersetzungsparameter Sie haben, desto kniffliger wird es. Sie haben mehrere Möglichkeiten, verstreut auf der Palette von hässlich-elegant, effizient-verschwenderisch:
Verwenden StringUtils.replaceEach von Apache Commons wie von @AlanHay empfohlen. Dies ist eine gute Option, wenn Sie Ihrem Projekt neue Abhängigkeiten hinzufügen können. Vielleicht haben Sie Glück: Die Abhängigkeit ist möglicherweise bereits in Ihrem Projekt enthalten
Verwenden Sie einen temporären Platzhalter, wie von @Jeroen vorgeschlagen, und führen Sie die Ersetzung in zwei Schritten durch:
Ersetzen Sie alle Suchmuster durch ein eindeutiges Tag, das im Originaltext nicht vorhanden ist
Ersetzen Sie die Platzhalter durch den tatsächlichen Zielersatz
Dies ist aus mehreren Gründen kein guter Ansatz: Es muss sichergestellt werden, dass die im ersten Schritt verwendeten Tags wirklich einzigartig sind; es führt mehr Zeichenfolgenersetzungsoperationen durch als wirklich notwendig
Erstellen Sie eine Regex aus allen Mustern und verwenden Sie die Methode mit Matcher und StringBuffer wie von @arshajii vorgeschlagen. Das ist nicht schlimm, aber auch nicht so toll, da das Erstellen der Regex ziemlich hackig ist und es beinhaltet StringBuffer die zugunsten von vor einiger Zeit aus der Mode gekommen ist StringBuilder.
Verwenden Sie eine von @mjolka vorgeschlagene rekursive Lösung, indem Sie die Zeichenfolge an den übereinstimmenden Mustern aufteilen und die verbleibenden Segmente rekursiv ausführen. Dies ist eine feine Lösung, kompakt und sehr elegant. Seine Schwäche sind die potentiell vielen Substring- und Verkettungsoperationen und die Stapelgrößenbeschränkungen, die für alle rekursiven Lösungen gelten
Teilen Sie den Text in Wörter auf und verwenden Sie Java 8-Streams, um die Ersetzungen elegant durchzuführen, wie @msandiford vorgeschlagen hat, aber das funktioniert natürlich nur, wenn Sie mit der Aufteilung an Wortgrenzen einverstanden sind, was es nicht als allgemeine Lösung geeignet macht
Hier ist meine Version, basierend auf geliehenen Ideen Apaches Implementierung. Es ist weder einfach noch elegant, aber es funktioniert und sollte ohne unnötige Schritte relativ effizient sein. Kurz gesagt funktioniert das so: wiederholt das nächste passende Suchmuster im Text finden und a verwenden StringBuilder um die nicht übereinstimmenden Segmente und die Ersetzungen zu akkumulieren.
public static String replaceEach(String text, String[] searchList, String[] replacementList) {
// TODO: throw new IllegalArgumentException() if any param doesn't make sense
//validateParams(text, searchList, replacementList);
SearchTracker tracker = new SearchTracker(text, searchList, replacementList);
if (!tracker.hasNextMatch(0)) {
return text;
}
StringBuilder buf = new StringBuilder(text.length() * 2);
int start = 0;
do {
SearchTracker.MatchInfo matchInfo = tracker.matchInfo;
int textIndex = matchInfo.textIndex;
String pattern = matchInfo.pattern;
String replacement = matchInfo.replacement;
buf.append(text.substring(start, textIndex));
buf.append(replacement);
start = textIndex + pattern.length();
} while (tracker.hasNextMatch(start));
return buf.append(text.substring(start)).toString();
}
private static class SearchTracker {
private final String text;
private final Map<String, String> patternToReplacement = new HashMap<>();
private final Set<String> pendingPatterns = new HashSet<>();
private MatchInfo matchInfo = null;
private static class MatchInfo {
private final String pattern;
private final String replacement;
private final int textIndex;
private MatchInfo(String pattern, String replacement, int textIndex) {
this.pattern = pattern;
this.replacement = replacement;
this.textIndex = textIndex;
}
}
private SearchTracker(String text, String[] searchList, String[] replacementList) {
this.text = text;
for (int i = 0; i < searchList.length; ++i) {
String pattern = searchList[i];
patternToReplacement.put(pattern, replacementList[i]);
pendingPatterns.add(pattern);
}
}
boolean hasNextMatch(int start) {
int textIndex = -1;
String nextPattern = null;
for (String pattern : new ArrayList<>(pendingPatterns)) {
int matchIndex = text.indexOf(pattern, start);
if (matchIndex == -1) {
pendingPatterns.remove(pattern);
} else {
if (textIndex == -1 || matchIndex < textIndex) {
textIndex = matchIndex;
nextPattern = pattern;
}
}
}
if (nextPattern != null) {
matchInfo = new MatchInfo(nextPattern, patternToReplacement.get(nextPattern), textIndex);
return true;
}
return false;
}
}
Einheitentests:
@Test
public void testSingleExact() {
assertEquals("bar", StringUtils.replaceEach("foo", new String[]{"foo"}, new String[]{"bar"}));
}
@Test
public void testReplaceTwice() {
assertEquals("barbar", StringUtils.replaceEach("foofoo", new String[]{"foo"}, new String[]{"bar"}));
}
@Test
public void testReplaceTwoPatterns() {
assertEquals("barbaz", StringUtils.replaceEach("foobar",
new String[]{"foo", "bar"},
new String[]{"bar", "baz"}));
}
@Test
public void testReplaceNone() {
assertEquals("foofoo", StringUtils.replaceEach("foofoo", new String[]{"x"}, new String[]{"bar"}));
}
@Test
public void testStory() {
assertEquals("Once upon a foo, there was a bar and a baz, and another bar and a cat.",
StringUtils.replaceEach("Once upon a baz, there was a foo and a bar, and another foo and a cat.",
new String[]{"foo", "bar", "baz"},
new String[]{"bar", "baz", "foo"})
);
}
mjolka
Suchen Sie nach dem ersten zu ersetzenden Wort. Wenn es sich in der Zeichenfolge befindet, rekursive für den Teil der Zeichenfolge vor dem Vorkommen und für den Teil der Zeichenfolge nach dem Vorkommen.
Fahren Sie andernfalls mit dem nächsten zu ersetzenden Wort fort.
Eine naive Implementierung könnte so aussehen
public static String replaceAll(String input, String[] search, String[] replace) {
return replaceAll(input, search, replace, 0);
}
private static String replaceAll(String input, String[] search, String[] replace, int i) {
if (i == search.length) {
return input;
}
int j = input.indexOf(search[i]);
if (j == -1) {
return replaceAll(input, search, replace, i + 1);
}
return replaceAll(input.substring(0, j), search, replace, i + 1) +
replace[i] +
replaceAll(input.substring(j + search[i].length()), search, replace, i);
}
Beispielnutzung:
String input = "Once upon a baz, there was a foo and a bar.";
String[] search = new String[] { "foo", "bar", "baz" };
String[] replace = new String[] { "bar", "baz", "foo" };
System.out.println(replaceAll(input, search, replace));
Ausgabe:
Once upon a foo, there was a bar and a baz.
Eine weniger naive Version:
public static String replaceAll(String input, String[] search, String[] replace) {
StringBuilder sb = new StringBuilder();
replaceAll(sb, input, 0, input.length(), search, replace, 0);
return sb.toString();
}
private static void replaceAll(StringBuilder sb, String input, int start, int end, String[] search, String[] replace, int i) {
while (i < search.length && start < end) {
int j = indexOf(input, search[i], start, end);
if (j == -1) {
i++;
} else {
replaceAll(sb, input, start, j, search, replace, i + 1);
sb.append(replace[i]);
start = j + search[i].length();
}
}
sb.append(input, start, end);
}
Leider Javas String hat kein indexOf(String str, int fromIndex, int toIndex) Methode. Ich habe die Implementierung von weggelassen indexOf hier, da ich nicht sicher bin, ob es richtig ist, aber es kann auf gefunden werden Ideezusammen mit einigen ungefähren Zeitangaben für verschiedene Lösungen, die hier veröffentlicht werden.
Obwohl die Verwendung einer vorhandenen Bibliothek wie Apache Commons für solche Dinge zweifellos der einfachste Weg ist, dieses ziemlich häufige Problem zu lösen, haben Sie eine Implementierung gezeigt, die mit Teilen von Wörtern funktioniert, mit Wörtern, die zur Laufzeit entschieden werden und im Gegensatz zu Substrings nicht durch magische Token ersetzt werden (derzeit) höher bewertete Antworten. +1
– Buhb
7. November 2014 um 14:13 Uhr
Schön, geht aber in die Hose, wenn eine Eingangsdatei von 100 mb geliefert wird.
Wenn die Wörter spezielle Regex-Zeichen enthalten können, verwenden Sie Muster.zitat ihnen zu entkommen.
Ich verwende guava ImmutableMap aus Gründen der Prägnanz, aber offensichtlich wird auch jede andere Map die Aufgabe erfüllen.
Obwohl die Verwendung einer vorhandenen Bibliothek wie Apache Commons für solche Dinge zweifellos der einfachste Weg ist, dieses ziemlich häufige Problem zu lösen, haben Sie eine Implementierung gezeigt, die mit Teilen von Wörtern funktioniert, mit Wörtern, die zur Laufzeit entschieden werden und im Gegensatz zu Substrings nicht durch magische Token ersetzt werden (derzeit) höher bewertete Antworten. +1
– Buhb
7. November 2014 um 14:13 Uhr
Schön, geht aber in die Hose, wenn eine Eingangsdatei von 100 mb geliefert wird.
– Christophe de Troyer
10. November 2014 um 20:44 Uhr
Hier ist eine Java-8-Streams-Möglichkeit, die für manche interessant sein könnte:
String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar.";
// Map is from untranslated word to translated word
Map<String, String> wordMap = new HashMap<>();
wordMap.put(word1, word2);
wordMap.put(word2, word1);
// Split on word boundaries so we retain whitespace.
String translated = Arrays.stream(story.split("\\b"))
.map(w -> wordMap.getOrDefault(w, w))
.collect(Collectors.joining());
System.out.println(translated);
Hier ist eine Annäherung desselben Algorithmus in Java 7:
String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar.";
// Map is from untranslated word to translated word
Map<String, String> wordMap = new HashMap<>();
wordMap.put(word1, word2);
wordMap.put(word2, word1);
// Split on word boundaries so we retain whitespace.
StringBuilder translated = new StringBuilder();
for (String w : story.split("\\b"))
{
String tw = wordMap.get(w);
translated.append(tw != null ? tw : w);
}
System.out.println(translated);
Dies ist ein netter Vorschlag, wenn die Dinge, die Sie ersetzen möchten, tatsächlich vorhanden sind Wörter getrennt durch Leerzeichen (oder ähnliches), aber dies würde nicht funktionieren, um Teilzeichenfolgen eines Wortes zu ersetzen.
– Simon Forsberg
7. November 2014 um 0:52 Uhr
+1 für Java8-Streams. Schade, dass dies ein Trennzeichen erfordert.
– Navin
7. November 2014 um 8:29 Uhr
13455300cookie-checkWie kann ich zwei Saiten so ersetzen, dass eine nicht die andere ersetzt?yes
+1 Es sollte definitiv eine Funktion geben
swap(String s1, String s2, String s3)
das tauscht alle Vorkommen von auss2
mits3
und umgekehrt.– Ryan
7. November 2014 um 3:14 Uhr
Können wir davon ausgehen, dass jedes austauschbare Wort in der Eingabe nur einmal vorkommt?
– Eis
7. November 2014 um 7:39 Uhr
Sonderfall: Was erwarten wir als Ausgabe, wenn wir „ab“ und „ba“ in „ababababababa“ vertauschen?
– Hagen von Eitzen
8. November 2014 um 19:15 Uhr
Sie haben unten einige gute Lösungen, aber verstehen Sie, warum Ihr Ansatz nicht funktioniert hat? Zuerst haben Sie “es gab ein Foo und eine Bar”. Nach dem ersten Ersetzen (“foo”->”bar”) haben Sie “there was a bar and a bar”. Sie haben jetzt 2 Vorkommen von “bar”, also tut Ihr zweites Ersetzen nicht das, was Sie erwarten – es hat keine Möglichkeit zu wissen, dass Sie nur das ersetzen möchten, das Sie beim letzten Mal nicht bereits ersetzt haben. @HagenvonEitzen Interessant. Ich würde erwarten, dass eine funktionierende Lösung die erste der beiden gefundenen Zeichenfolgen abgleicht und ersetzt und dann ab dem Ende des ersetzten Abschnitts wiederholt.
– EntwicklerInEntwicklung
9. November 2014 um 1:44 Uhr
Die Lösung von Jeroen verwende ich häufig in Texteditoren, wenn ich Massenumbenennungen durchführen muss. Es ist einfach, leicht zu verstehen, erfordert keine spezielle Bibliothek und kann mit ein wenig Nachdenken narrensicher sein.
– Heiße Licks
9. November 2014 um 14:40 Uhr