Wie kann ich zwei Zeichenfolgen so ersetzen, dass die eine nicht die andere ersetzt?

162

Angenommen, 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 wird , wird der Wert von storyseinem"Once upon a time, there was a foo and a foo."

Ein ähnliches Problem tritt auf, wenn ich sie in umgekehrter 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 storywird sein"Once upon a time, there was a bar and a bar."

Mein Ziel ist es, sich storyin "Once upon a time, there was a bar and a foo."Wie kann ich das erreichen?

Pikamander2
quelle
7
+1 Es sollte definitiv eine Funktion geben swap(String s1, String s2, String s3), die alle Vorkommen von s2mit vertauscht s3und umgekehrt.
Ryan
Können wir annehmen, dass jedes der austauschbaren Wörter in der Eingabe nur einmal vorkommt?
icza
14
Eckfall: Was erwarten wir als Ausgabe, wenn wir "ab" und "ba" in "ababababababa" tauschen?
Hagen von Eitzen
1
Sie haben unten einige gute Lösungen, aber verstehen Sie, warum Ihr Ansatz nicht funktioniert hat? Erstens haben Sie "es gab ein Foo und eine Bar". Nach dem ersten Ersetzen ("foo" -> "bar") haben Sie "es gab einen Balken und einen Balken". Sie haben jetzt 2 Vorkommen von "Balken", sodass Ihr zweiter Ersatz nicht das tut, was Sie erwarten - er kann nicht wissen, dass Sie nur den ersetzen möchten, den Sie beim letzten Mal noch nicht ersetzt haben. @HagenvonEitzen Interessant. Ich würde erwarten, dass eine funktionierende Lösung mit der ersten gefundenen Zeichenfolge übereinstimmt und diese ersetzt und dann am Ende des ersetzten Abschnitts wiederholt.
DeveloperInDevelopment
1
Jeroens Lösung ist eine, die ich häufig in Texteditoren verwende, wenn ich eine Massenumbenennung durchführen muss. Es ist einfach, leicht zu verstehen, erfordert keine spezielle Bibliothek und kann mit ein wenig Nachdenken kinderleicht sein.
Hot Licks

Antworten:

88

Verwenden Sie die replaceEach()Methode von Apache Commons StringUtils :

StringUtils.replaceEach(story, new String[]{"foo", "bar"}, new String[]{"bar", "foo"})
Alan Hay
quelle
2
Irgendeine Idee, was genau replaceEach intern macht?
Marek
3
@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.
16
Die Quelle hierfür finden Sie hier in der Zeile 4684.
Jeroen Vannevel
Es ist schade, dass es ein No-Op nullist, wenn es bestanden wird.
Rechtsfalte
87

Sie verwenden einen Zwischenwert (der im Satz noch nicht vorhanden ist).

story = story.replace("foo", "lala");
story = story.replace("bar", "foo");
story = story.replace("lala", "bar");

Als Antwort auf Kritik: Wenn Sie eine ausreichend große ungewöhnliche Zeichenfolge wie zq515sqdqs5d5sq1dqs4d1q5dqqé "& é5d4sqjshsjddjhodfqsqc, nvùq ^ µù; d & € sdq: d :); àçàçlala verwenden und Gebrauch , dass es auf den Punkt unwahrscheinlich ist , wo ich es nicht einmal debattieren dass ein Benutzer dies jemals eingeben wird. Der einzige Weg 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 auch bei mir nicht ausbricht.

Wiederholen Sie auch den ausgezeichneten Rat von @David Conrad in den Kommentaren :

Verwenden Sie keine Saite, die geschickt (dumm) ausgewählt wurde, um unwahrscheinlich zu sein. Verwenden Sie Zeichen aus dem Unicode-Bereich für den privaten Gebrauch, U + E000..U + F8FF. Entfernen Sie solche Zeichen zuerst, da sie nicht legitimerweise in der Eingabe enthalten sein sollten (sie haben nur in einigen Anwendungen eine anwendungsspezifische Bedeutung), und verwenden Sie sie dann beim Ersetzen als Platzhalter.

Jeroen Vannevel
quelle
4
@arshajii Ich denke, das hängt von Ihrer Definition von "besser" ab. Wenn es funktioniert und akzeptabel performant ist, fahren Sie mit der nächsten Programmieraufgabe fort und verbessern Sie sie später während des Refactorings.
Matt Coubrough
24
Offensichtlich ist "Lala" nur ein Beispiel. In der Produktion sollten Sie " zq515sqdqs5d5sq1dqs4d1q5dqqé" & é & € sdq: d :); àçàçlala "verwenden.
Jeroen Vannevel
81
Verwenden Sie keine Saite, die geschickt (dumm) ausgewählt wurde, um unwahrscheinlich zu sein. Verwenden Sie Zeichen aus dem Unicode-Bereich für den privaten Gebrauch, U + E000..U + F8FF. Entfernen Sie solche Zeichen zuerst, da sie nicht legitimerweise in der Eingabe enthalten sein sollten (sie haben nur in einigen Anwendungen eine anwendungsspezifische Bedeutung), und verwenden Sie sie dann beim Ersetzen als Platzhalter.
David Conrad
22
Nach dem Lesen der Unicode-FAQ denke ich, dass die Nicht-Zeichen im Bereich U + FDD0..U + FDEF eine noch bessere Wahl wären.
David Conrad
6
@ 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 für unsichere Eingaben ab.
Navin
33

Sie können so etwas ausprobieren, indem Sie Matcher#appendReplacementund Matcher#appendTail:

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());
Es war einmal eine Bar und ein Foo.
arshajii
quelle
2
Ist diese Arbeit , wenn foo, barund storyalle haben unbekannte Werte?
Stephen P
1
@StephenP Ich habe die "foo"und die "bar"Ersetzungszeichenfolgen im Wesentlichen fest codiert, wie sie das OP in seinem Code hatte, aber der gleiche Ansatz würde gut funktionieren, selbst wenn diese Werte nicht bekannt sind (Sie müssten if/ else ifanstelle von a switchinnerhalb der verwenden while-Schleife).
Arshajii
6
Sie müssten beim Erstellen des regulären Ausdrucks vorsichtig sein. Pattern.quotewürde sich als nützlich erweisen, oder \Qund \E.
David Conrad
1
@arshajii - yep, hat es mir als "swapThese" -Methode bewiesen, bei der Wort1, Wort2 und Geschichte als Parameter verwendet wurden. +1
Stephen P
4
Noch sauberer wäre es, das Muster zu verwenden (foo)|(bar)und dann dagegen zu prüfen m.group(1) != null, um zu vermeiden, dass die passenden Wörter wiederholt werden.
Jörn Horstmann
32

Dies ist kein einfaches Problem. Und je mehr Suchersetzungsparameter Sie haben, desto schwieriger wird es. Sie haben mehrere Möglichkeiten, die auf der Palette hässlich-elegant, effizient-verschwenderisch verteilt sind:

  • Verwendung StringUtils.replaceEachvon Apache Commons wie von @AlanHay empfohlen. Dies ist eine gute Option, wenn Sie Ihrem Projekt neue Abhängigkeiten hinzufügen können. Sie könnten Glück haben: 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 den Austausch in zwei Schritten durch:

    1. Ersetzen Sie alle Suchmuster durch ein eindeutiges Tag, das im Originaltext nicht vorhanden ist
    2. Ersetzen Sie die Platzhalter durch den tatsächlichen Zielersatz

    Dies ist aus mehreren Gründen kein guter Ansatz: Er muss sicherstellen, dass die im ersten Schritt verwendeten Tags wirklich eindeutig sind. Es führt mehr String-Ersetzungsoperationen durch als wirklich notwendig

  • Erstellen Sie einen regulären Ausdruck aus allen Mustern und verwenden Sie die Methode mit MatcherundStringBuffer wie von @arshajii vorgeschlagen . Das ist nicht schrecklich, aber auch nicht so toll, da der Aufbau des regulären Ausdrucks irgendwie hackisch ist und beinhaltet, StringBufferwas vor einiger Zeit zugunsten von 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 gute Lösung, kompakt und sehr elegant. Seine Schwäche sind die potenziell vielen Teilzeichenfolgen- und Verkettungsvorgänge sowie 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 von @msandiford vorgeschlagen. Dies funktioniert natürlich nur, wenn Sie mit dem Teilen an Wortgrenzen einverstanden sind, was es nicht als allgemeine Lösung geeignet macht

Hier ist meine Version, basierend auf Ideen, die aus der Implementierung von Apache entlehnt wurden . Es ist weder einfach noch elegant, aber es funktioniert und sollte relativ effizient sein, ohne unnötige Schritte. Kurz gesagt, es funktioniert folgendermaßen: Suchen Sie wiederholt das nächste übereinstimmende Suchmuster im Text und verwenden Sie a 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;
    }
}

Unit Tests:

@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"})
    );
}
Janos
quelle
21

Suchen Sie nach dem ersten Wort, das ersetzt werden soll. Wenn es sich in der Zeichenfolge befindet, wiederholen Sie den Teil der Zeichenfolge vor dem Auftreten und den Teil der Zeichenfolge nach dem Auftreten.

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 hat Java Stringkeine indexOf(String str, int fromIndex, int toIndex)Methode. Ich habe die Implementierung von indexOfhier weggelassen , da ich nicht sicher bin, ob sie korrekt ist, aber sie kann auf ideone gefunden werden , zusammen mit einigen groben Zeitplänen verschiedener hier veröffentlichter Lösungen.

mjolka
quelle
2
Obwohl die Verwendung einer vorhandenen Bibliothek wie Apache Commons für solche Dinge zweifellos der einfachste Weg ist, um dieses häufig auftretende Problem zu lösen, haben Sie eine Implementierung gezeigt, die mit Teilen von Wörtern, mit Wörtern, die zur Laufzeit festgelegt wurden, funktioniert und ohne Teilzeichenfolgen durch magische Token zu ersetzen (derzeit) höher gestimmte Antworten. +1
Buhb
Schön, aber auf den Boden trifft, wenn eine Eingabedatei von 100 MB geliefert wird.
Christophe De Troyer
12

Einzeiler in Java 8:

    story = Pattern
        .compile(String.format("(?<=%1$s)|(?=%1$s)", "foo|bar"))
        .splitAsStream(story)
        .map(w -> ImmutableMap.of("bar", "foo", "foo", "bar").getOrDefault(w, w))
        .collect(Collectors.joining());
  • Lookaround reguläre Ausdrücke ( ?<=, ?=): http://www.regular-expressions.info/lookaround.html
  • Wenn die Wörter spezielle Regex-Zeichen enthalten können, verwenden Sie Pattern.quote , um sie zu .
  • Ich benutze Guave ImmutableMap aus Gründen der Übersichtlichkeit, aber natürlich wird auch jede andere Map den Job erledigen.
Vitalii Fedorenko
quelle
11

Hier ist eine Möglichkeit für Java 8-Streams, die für einige 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 an denselben 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);
clstrfsck
quelle
10
Dies ist ein netter Vorschlag, wenn das zu ersetzende Material tatsächliche Wörter sind, die durch Leerzeichen (oder ähnliches) getrennt sind. Dies würde jedoch nicht zum Ersetzen von Teilzeichenfolgen eines Wortes funktionieren.
Simon Forsberg
+1 für Java8-Streams. Schade, dass dafür ein Trennzeichen erforderlich ist.
Navin
6

Wenn Sie Wörter in einem Satz ersetzen möchten, die wie in Ihrem Beispiel durch Leerzeichen getrennt sind, können Sie diesen einfachen Algorithmus verwenden.

  1. Geteilte Geschichte auf Leerraum
  2. Ersetzen Sie jedes Element, wenn foo es durch bar und vice varsa ersetzt
  3. Verbinden Sie das Array wieder zu einer Zeichenfolge

Wenn das Aufteilen auf Platz nicht akzeptabel ist, kann man diesem alternativen Algorithmus folgen. Sie müssen zuerst die längere Zeichenfolge verwenden. Wenn die Zeichenfolgen foo und dumm sind, müssen Sie zuerst dumm und dann foo verwenden.

  1. Teilen Sie das Wort foo auf
  2. Ersetzen Sie die Leiste durch foo für jedes Element des Arrays
  3. Verbinden Sie dieses Array wieder und fügen Sie nach jedem Element außer dem letzten einen Balken hinzu
fastcodejava
quelle
1
Das wollte ich auch vorschlagen. Es wird jedoch eine Einschränkung hinzugefügt, dass der Text aus Wörtern besteht, die von Leerzeichen umgeben sind. :)
Entwickler Marius Žilėnas
@ MariusŽilėnas Ich habe einen alternativen Algorithmus hinzugefügt.
Fastcodejava
5

Hier ist eine weniger komplizierte Antwort mit Map.

private static String replaceEach(String str,Map<String, String> map) {

         Object[] keys = map.keySet().toArray();
         for(int x = 0 ; x < keys.length ; x ++ ) {
             str = str.replace((String) keys[x],"%"+x);
         }

         for(int x = 0 ; x < keys.length ; x ++) {
             str = str.replace("%"+x,map.get(keys[x]));
         }
         return str;
     }

Und Methode heißt

Map<String, String> replaceStr = new HashMap<>();
replaceStr.put("Raffy","awesome");
replaceStr.put("awesome","Raffy");
String replaced = replaceEach("Raffy is awesome, awesome awesome is Raffy Raffy", replaceStr);

Die Ausgabe ist: fantastisch ist Raffy, Raffy Raffy ist fantastisch fantastisch

WillingLearner
quelle
1
läuft replaced.replaceAll("Raffy", "Barney");danach wird es für sie legen ... wait machen; Dary !!!
Keale
3

Wenn Sie in der Lage sein möchten, mehrere Vorkommen der zu ersetzenden Suchzeichenfolgen zu verarbeiten, können Sie dies einfach tun, indem Sie die Zeichenfolge auf jeden Suchbegriff aufteilen und dann ersetzen. Hier ist ein Beispiel:

String regex = word1 + "|" + word2;
String[] values = Pattern.compile(regex).split(story);

String result;
foreach subStr in values
{
   subStr = subStr.replace(word1, word2);
   subStr = subStr.replace(word2, word1);
   result += subStr;
}
ventsyv
quelle
3

Sie können Ihr Ziel mit dem folgenden Codeblock erreichen:

String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, in a foo, there was a foo and a bar.";
story = String.format(story.replace(word1, "%1$s").replace(word2, "%2$s"),
    word2, word1);

Es ersetzt die Wörter unabhängig von der Reihenfolge. Sie können dieses Prinzip auf eine Utility-Methode erweitern, z.

private static String replace(String source, String[] targets, String[] replacements) throws IllegalArgumentException {
    if (source == null) {
        throw new IllegalArgumentException("The parameter \"source\" cannot be null.");
    }

    if (targets == null || replacements == null) {
        throw new IllegalArgumentException("Neither parameters \"targets\" or \"replacements\" can be null.");
    }

    if (targets.length == 0 || targets.length != replacements.length) {
        throw new IllegalArgumentException("The parameters \"targets\" and \"replacements\" must have at least one item and have the same length.");
    }

    String outputMask = source;
    for (int i = 0; i < targets.length; i++) {
        outputMask = outputMask.replace(targets[i], "%" + (i + 1) + "$s");
    }

    return String.format(outputMask, (Object[])replacements);
}

Welches würde verbraucht werden als:

String story = "Once upon a time, in a foo, there was a foo and a bar.";
story = replace(story, new String[] { "bar", "foo" },
    new String[] { "foo", "bar" }));
Leonardo Braga
quelle
3

Das funktioniert und ist einfach:

public String replaceBoth(String text, String token1, String token2) {            
    return text.replace(token1, "\ufdd0").replace(token2, token1).replace("\ufdd0", token2);
    }

Sie verwenden es so:

replaceBoth("Once upon a time, there was a foo and a bar.", "foo", "bar");

Hinweis: Dies gilt für Zeichenfolgen, die kein Zeichen enthalten. Dieses Zeichen \ufdd0ist permanent für die interne Verwendung durch Unicode reserviert (siehe http://www.unicode.org/faq/private_use.html) ):

Ich denke nicht, dass es notwendig ist, aber wenn Sie absolut sicher sein wollen, können Sie verwenden:

public String replaceBoth(String text, String token1, String token2) {
    if (text.contains("\ufdd0") || token1.contains("\ufdd0") || token2.contains("\ufdd0")) throw new IllegalArgumentException("Invalid character.");
    return text.replace(token1, "\ufdd0").replace(token2, token1).replace("\ufdd0", token2);
    }
MarcG
quelle
3

Nur ein Ereignis tauschen

Wenn die austauschbaren Zeichenfolgen in der Eingabe nur einmal vorkommen, können Sie Folgendes tun:

Bevor Sie mit dem Ersetzen fortfahren, ermitteln Sie die Indizes für das Vorkommen der Wörter. Danach ersetzen wir nur das Wort, das in diesen Indizes gefunden wurde, und nicht alle Vorkommen. Diese Lösung verwendet StringBuilderund produziert keine Zwischenprodukte StringwieString.replace() .

Eines ist zu beachten: Wenn die austauschbaren Wörter unterschiedliche Längen haben, kann sich nach dem ersten Ersetzen der zweite Index genau mit der Differenz der beiden Längen ändern (wenn das erste Wort vor dem zweiten vorkommt). Durch Ausrichten des zweiten Index wird sichergestellt, dass dies auch dann funktioniert, wenn Wörter mit unterschiedlichen Längen ausgetauscht werden.

public static String swap(String src, String s1, String s2) {
    StringBuilder sb = new StringBuilder(src);
    int i1 = src.indexOf(s1);
    int i2 = src.indexOf(s2);

    sb.replace(i1, i1 + s1.length(), s2); // Replace s1 with s2
    // If s1 was before s2, idx2 might have changed after the replace
    if (i1 < i2)
        i2 += s2.length() - s1.length();
    sb.replace(i2, i2 + s2.length(), s1); // Replace s2 with s1

    return sb.toString();
}

Beliebige Anzahl von Vorkommen tauschen

Analog zum vorherigen Fall werden wir zuerst die Indizes (Vorkommen) der Wörter sammeln, aber in diesem Fall wird eine Liste von ganzen Zahlen für jedes Wort erstellt, nicht nur für eine int. Hierzu verwenden wir die folgende Dienstprogrammmethode:

public static List<Integer> occurrences(String src, String s) {
    List<Integer> list = new ArrayList<>();
    for (int idx = 0;;)
        if ((idx = src.indexOf(s, idx)) >= 0) {
            list.add(idx);
            idx += s.length();
        } else
            return list;
}

Und wenn wir dies verwenden, werden wir die Wörter durch die anderen ersetzen, indem wir den Index verringern (was möglicherweise einen Wechsel zwischen den beiden austauschbaren Wörtern erforderlich macht), damit wir nach einem Ersetzen nicht einmal die Indizes korrigieren müssen:

public static String swapAll(String src, String s1, String s2) {
    List<Integer> l1 = occurrences(src, s1), l2 = occurrences(src, s2);

    StringBuilder sb = new StringBuilder(src);

    // Replace occurrences by decreasing index, alternating between s1 and s2
    for (int i1 = l1.size() - 1, i2 = l2.size() - 1; i1 >= 0 || i2 >= 0;) {
        int idx1 = i1 < 0 ? -1 : l1.get(i1);
        int idx2 = i2 < 0 ? -1 : l2.get(i2);
        if (idx1 > idx2) { // Replace s1 with s2
            sb.replace(idx1, idx1 + s1.length(), s2);
            i1--;
        } else { // Replace s2 with s1
            sb.replace(idx2, idx2 + s2.length(), s1);
            i2--;
        }
    }

    return sb.toString();
}
icza
quelle
Ich bin nicht sicher, wie Java mit Unicode umgeht, aber das C # -Äquivalent dieses Codes wäre falsch. Das Problem ist, dass die indexOfübereinstimmende Teilzeichenfolge aufgrund der Besonderheiten der Unicode-Zeichenfolgenäquivalenz möglicherweise nicht die gleiche Länge wie die Suchzeichenfolge hat.
CodesInChaos
@CodesInChaos Es funktioniert einwandfrei in Java, da ein Java Stringein Zeichenarray und kein Byte-Array ist. Alle Methoden von Stringund StringBuilderarbeiten mit Zeichen, nicht mit Bytes, die "codierungsfrei" sind. So indexOfhaben Streichhölzer genau das gleiche (Zeichen) Länge wie die Suchzeichenfolgen.
icza
Sowohl in C # als auch in Java ist eine Zeichenfolge eine Folge von UTF-16-Codeeinheiten. Das Problem ist, dass es verschiedene Sequenzen von Codepunkten gibt, die Unicode als äquivalent betrachtet. Zum Beispiel äkann als einzelner Codepunkt oder als agefolgt von einer Kombination codiert werden ¨. Es gibt auch einige Codepunkte, die ignoriert werden, z. B. (Nicht-) Joiner mit einer Breite von Null. Es spielt keine Rolle, ob die Zeichenfolge aus Bytes, Zeichen oder was auch immer besteht, sondern welche Vergleichsregeln indexOfverwendet werden. Es kann einfach Code-Einheit durch Code-Einheit-Vergleich ("Ordinal") verwenden oder Unicode-Äquivalenz implementieren. Ich weiß nicht, welchen Java gewählt hat.
CodesInChaos
Zum Beispiel "ab\u00ADc".IndexOf("bc")kehrt 1in .net die zwei passende Zeichenkette bczu einer Drei - Zeichenfolge.
CodesInChaos
1
@CodesInChaos Ich verstehe, was du jetzt meinst. In Java wird "ab\u00ADc".indexOf("bc")zurückgegeben, -1was bedeutet, dass "bc"in nicht gefunden wurde "ab\u00ADc". Es steht also immer noch fest, dass in Java der obige Algorithmus funktioniert, indexOf()Übereinstimmungen genau die gleiche (Zeichen-) Länge wie indexOf()die Suchzeichenfolgen haben und nur dann Übereinstimmungen melden, wenn die Zeichenfolgen (Codepunkte) übereinstimmen.
icza
2

Es ist einfach, eine Methode zu schreiben, um dies zu tun String.regionMatches:

public static String simultaneousReplace(String subject, String... pairs) {
    if (pairs.length % 2 != 0) throw new IllegalArgumentException(
        "Strings to find and replace are not paired.");
    StringBuilder sb = new StringBuilder();
    outer:
    for (int i = 0; i < subject.length(); i++) {
        for (int j = 0; j < pairs.length; j += 2) {
            String find = pairs[j];
            if (subject.regionMatches(i, find, 0, find.length())) {
                sb.append(pairs[j + 1]);
                i += find.length() - 1;
                continue outer;
            }
        }
        sb.append(subject.charAt(i));
    }
    return sb.toString();
}

Testen:

String s = "There are three cats and two dogs.";
s = simultaneousReplace(s,
    "cats", "dogs",
    "dogs", "budgies");
System.out.println(s);

Ausgabe:

Es gibt drei Hunde und zwei Wellensittiche.

Es ist nicht sofort offensichtlich, aber eine Funktion wie diese kann immer noch von der Reihenfolge abhängen, in der die Ersetzungen angegeben sind. Erwägen:

String truth = "Java is to JavaScript";
truth += " as " + simultaneousReplace(truth,
    "JavaScript", "Hamster",
    "Java", "Ham");
System.out.println(truth);

Ausgabe:

Java ist zu JavaScript wie Ham zu Hamster

Aber vertauschen Sie die Ersetzungen:

truth += " as " + simultaneousReplace(truth,
    "Java", "Ham",
    "JavaScript", "Hamster");

Ausgabe:

Java ist zu JavaScript wie Ham zu HamScript

Hoppla! :) :)

Daher ist es manchmal nützlich, nach der längsten Übereinstimmung zu suchen (wie es beispielsweise die PHP- strtrFunktion tut). Diese Version der Methode wird das tun:

public static String simultaneousReplace(String subject, String... pairs) {
    if (pairs.length % 2 != 0) throw new IllegalArgumentException(
        "Strings to find and replace are not paired.");
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < subject.length(); i++) {
        int longestMatchIndex = -1;
        int longestMatchLength = -1;
        for (int j = 0; j < pairs.length; j += 2) {
            String find = pairs[j];
            if (subject.regionMatches(i, find, 0, find.length())) {
                if (find.length() > longestMatchLength) {
                    longestMatchIndex = j;
                    longestMatchLength = find.length();
                }
            }
        }
        if (longestMatchIndex >= 0) {
            sb.append(pairs[longestMatchIndex + 1]);
            i += longestMatchLength - 1;
        } else {
            sb.append(subject.charAt(i));
        }
    }
    return sb.toString();
}

Beachten Sie, dass bei den oben genannten Methoden zwischen Groß- und Kleinschreibung unterschieden wird. Wenn Sie eine Version benötigen, bei der die Groß- und Kleinschreibung nicht String.regionMatchesberücksichtigt wird, können Sie die oben genannten Optionen leicht ändern, da sie einen ignoreCaseParameter annehmen können .

Boann
quelle
2

Wenn Sie keine Abhängigkeiten möchten, können Sie einfach ein Array verwenden, das nur eine einmalige Änderung zulässt. Dies ist nicht die effizienteste Lösung, sollte aber funktionieren.

public String replace(String sentence, String[]... replace){
    String[] words = sentence.split("\\s+");
    int[] lock = new int[words.length];
    StringBuilder out = new StringBuilder();

    for (int i = 0; i < words.length; i++) {
        for(String[] r : replace){
            if(words[i].contains(r[0]) && lock[i] == 0){
                words[i] = words[i].replace(r[0], r[1]);
                lock[i] = 1;
            }
        }

        out.append((i < (words.length - 1) ? words[i] + " " : words[i]));
    }

    return out.toString();
}

Dann würde es funktionieren.

String story = "Once upon a time, there was a foo and a bar.";

String[] a = {"foo", "bar"};
String[] b = {"bar", "foo"};
String[] c = {"there", "Pocahontas"};
story = replace(story, a, b, c);

System.out.println(story); // Once upon a time, Pocahontas was a bar and a foo.
Pier-Alexandre Bouchard
quelle
2

Sie führen mehrere Such- / Ersetzungsvorgänge für die Eingabe aus. Dies führt zu unerwünschten Ergebnissen, wenn die Ersatzzeichenfolgen Suchzeichenfolgen enthalten. Betrachten Sie das Beispiel foo-> bar, bar-foo. Hier sind die Ergebnisse für jede Iteration:

  1. Es war einmal ein Foo und eine Bar. (Eingang)
  2. Es war einmal eine Bar und eine Bar. (foo-> bar)
  3. Es war einmal ein Foo und ein Foo. (bar-> foo, Ausgabe)

Sie müssen den Austausch in einer Iteration durchführen, ohne zurück zu gehen. Eine Brute-Force-Lösung lautet wie folgt:

  1. Durchsuchen Sie die Eingabe von der aktuellen Position bis zum Ende nach mehreren Suchzeichenfolgen, bis eine Übereinstimmung gefunden wird
  2. Ersetzen Sie die übereinstimmende Suchzeichenfolge durch die entsprechende Ersetzungszeichenfolge
  3. Setzen Sie die aktuelle Position auf das nächste Zeichen nach der ersetzten Zeichenfolge
  4. Wiederholen

Eine Funktion wie String.indexOfAny(String[]) -> int[]{index, whichString}wäre nützlich. Hier ist ein Beispiel (nicht das effizienteste):

private static String replaceEach(String str, String[] searchWords, String[] replaceWords) {
    String ret = "";
    while (str.length() > 0) {
        int i;
        for (i = 0; i < searchWords.length; i++) {
            String search = searchWords[i];
            String replace = replaceWords[i];
            if (str.startsWith(search)) {
                ret += replace;
                str = str.substring(search.length());
                break;
            }
        }
        if (i == searchWords.length) {
            ret += str.substring(0, 1);
            str = str.substring(1);
        }
    }
    return ret;
}

Einige Tests:

System.out.println(replaceEach(
    "Once upon a time, there was a foo and a bar.",
    new String[]{"foo", "bar"},
    new String[]{"bar", "foo"}
));
// Once upon a time, there was a bar and a foo.

System.out.println(replaceEach(
    "a p",
    new String[]{"a", "p"},
    new String[]{"apple", "pear"}
));
// apple pear

System.out.println(replaceEach(
    "ABCDE",
    new String[]{"A", "B", "C", "D", "E"},
    new String[]{"B", "C", "E", "E", "F"}
));
// BCEEF

System.out.println(replaceEach(
    "ABCDEF",
    new String[]{"ABCDEF", "ABC", "DEF"},
    new String[]{"XXXXXX", "YYY", "ZZZ"}
));
// XXXXXX
// note the order of search strings, longer strings should be placed first 
// in order to make the replacement greedy

Demo auf IDEONE
Demo auf IDEONE, alternativer Code

Salman A.
quelle
1

Sie können es jederzeit durch ein Wort ersetzen, von dem Sie sicher sind, dass es nirgendwo anders in der Zeichenfolge vorkommt, und dann das zweite Ersetzen später durchführen:

String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar."
story = story.replace("foo", "StringYouAreSureWillNeverOccur").replace("bar", "word2").replace("StringYouAreSureWillNeverOccur", "word1");

Beachten Sie, dass dies in diesem "StringYouAreSureWillNeverOccur"Fall nicht richtig funktioniert.

Pokechu22
quelle
5
Verwenden Sie Zeichen aus dem Unicode-Bereich für den privaten Gebrauch, U + E000..U + F8FF, und erstellen Sie einen StringThatCannotEverOccur. Sie können sie vorher herausfiltern, da sie in der Eingabe nicht vorhanden sein sollten.
David Conrad
Oder U + FDD0..U + FDEF, die "Noncharacters", die für den internen Gebrauch reserviert sind.
David Conrad
1

Erwägen Sie die Verwendung von StringBuilder

Speichern Sie dann den Index, in dem jede Zeichenfolge beginnen soll. Wenn Sie an jeder Position ein Platzhalterzeichen verwenden, entfernen Sie es und fügen Sie die Benutzerzeichenfolge ein. Sie können dann die Endposition zuordnen, indem Sie die Zeichenfolgenlänge zur Startposition hinzufügen.

String firstString = "???";
String secondString  = "???"

StringBuilder story = new StringBuilder("One upon a time, there was a " 
    + firstString
    + " and a "
    + secondString);

int  firstWord = 30;
int  secondWord = firstWord + firstString.length() + 7;

story.replace(firstWord, firstWord + firstString.length(), userStringOne);
story.replace(secondWord, secondWord + secondString.length(), userStringTwo);

firstString = userStringOne;
secondString = userStringTwo;

return story;
Imheroldman
quelle
1

Was ich nur teilen kann, ist meine eigene Methode.

Sie können eine temporäre verwenden String temp = "<?>"; oder verwendenString.Format();

Dies ist mein Beispielcode, der in der Konsolenanwendung über erstellt wurde - "Nur Idee, keine genaue Antwort" .

static void Main(string[] args)
    {
        String[] word1 = {"foo", "Once"};
        String[] word2 = {"bar", "time"};
        String story = "Once upon a time, there was a foo and a bar.";

        story = Switcher(story,word1,word2);
        Console.WriteLine(story);
        Console.Read();
    }
    // Using a temporary string.
    static string Switcher(string text, string[] target, string[] value)
    {
        string temp = "<?>";
        if (target.Length == value.Length)
        {
            for (int i = 0; i < target.Length; i++)
            {
                text = text.Replace(target[i], temp);
                text = text.Replace(value[i], target[i]);
                text = text.Replace(temp, value[i]);
            }
        }
        return text;
    }

Oder Sie können auch die verwenden String.Format();

static string Switcher(string text, string[] target, string[] value)
        {
            if (target.Length == value.Length)
            {
                for (int i = 0; i < target.Length; i++)
                {
                    text = text.Replace(target[i], "{0}").Replace(value[i], "{1}");
                    text = String.Format(text, value[i], target[i]);
                }
            }
            return text;
        }

Ausgabe: time upon a Once, there was a bar and a foo.

Leonel Sarmiento
quelle
Es ist ziemlich hackig. Was wirst du tun, wenn er "_" ersetzen will?
Pier-Alexandre Bouchard
@ Pier-AlexandreBouchard In Methoden ändere ich den Wert von tempvon "_"in <?>. Bei Bedarf kann er der Methode jedoch einen weiteren Parameter hinzufügen, der die Temperatur ändert. - "Es ist besser, es einfach zu halten, oder?"
Leonel Sarmiento
Mein Punkt ist, dass Sie das erwartete Ergebnis nicht garantieren können, denn wenn die Temperatur == ersetzen, wird Ihr Weg nicht funktionieren.
Pier-Alexandre Bouchard
1

Hier ist meine Version, die wortbasiert ist:

class TextReplace
{

    public static void replaceAll (String text, String [] lookup,
                                   String [] replacement, String delimiter)
    {

        String [] words = text.split(delimiter);

        for (int i = 0; i < words.length; i++)
        {

            int j = find(lookup, words[i]);

            if (j >= 0) words[i] = replacement[j];

        }

        text = StringUtils.join(words, delimiter);

    }

    public static  int find (String [] array, String key)
    {

        for (int i = 0; i < array.length; i++)
            if (array[i].equals(key))
                return i;

        return (-1);

    }

}
Khaled.K
quelle
1
String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar."

Kleiner kniffliger Weg, aber Sie müssen noch einige Überprüfungen durchführen.

1.Konvertieren Sie eine Zeichenfolge in ein Zeichenarray

   String temp[] = story.split(" ");//assume there is only spaces.

2.Schleifen Sie die Temperatur und ersetzen Sie sie foodurch barund barmit, fooda keine Chance besteht, dass die Saite wieder ausgetauscht werden kann.

Key_coder
quelle
1

Nun, die kürzere Antwort ist ...

String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar.";
story = story.replace("foo", "@"+ word1).replace("bar", word2).replace("@" + word2, word1);
System.out.println(story);
Elvis Lima
quelle
1

Mit der hier gefundenen Antwort können Sie alle Vorkommen der Zeichenfolgen finden, durch die Sie ersetzen möchten.

So führen Sie beispielsweise den Code in der obigen SO-Antwort aus. Erstellen Sie zwei Indextabellen (sagen wir, Balken und Foo erscheinen nicht nur einmal in Ihrer Zeichenfolge), und Sie können mit diesen Tabellen arbeiten, um sie in Ihrer Zeichenfolge zu ersetzen.

Zum Ersetzen bestimmter Indexpositionen können Sie nun Folgendes verwenden:

public static String replaceStringAt(String s, int pos, String c) {
   return s.substring(0,pos) + c + s.substring(pos+1);
}

Während posist der Index, wo Ihre Zeichenfolgen beginnen (aus den oben zitierten Indextabellen). Angenommen, Sie haben für jede Tabelle zwei Indextabellen erstellt. Nennen wir sie indexBarund indexFoo.

Wenn Sie sie jetzt ersetzen, können Sie einfach zwei Schleifen ausführen, eine für jede Ersetzung, die Sie vornehmen möchten.

for(int i=0;i<indexBar.Count();i++)
replaceStringAt(originalString,indexBar[i],newString);

Ebenso eine andere Schleife für indexFoo.

Dies ist möglicherweise nicht so effizient wie andere Antworten hier, aber es ist einfacher zu verstehen als Karten oder andere Dinge.

Dies würde Ihnen immer das gewünschte Ergebnis und für mehrere mögliche Vorkommen jeder Zeichenfolge liefern. Solange Sie den Index jedes Vorkommens speichern.

Auch diese Antwort erfordert weder eine Rekursion noch externe Abhängigkeiten. Was die Komplexität betrifft, ist es wahrscheinlich O (n im Quadrat), während n die Summe der Vorkommen beider Wörter ist.

John Demetriou
quelle
-1

Ich habe diesen Code entwickelt, um das Problem zu lösen:

public static String change(String s,String s1, String s2) {
   int length = s.length();
   int x1 = s1.length();
   int x2 = s2.length();
   int x12 = s.indexOf(s1);
   int x22 = s.indexOf(s2);
   String s3=s.substring(0, x12);
   String s4 =s.substring(x12+3, x22);
   s=s3+s2+s4+s1;
   return s;
}

In der Hauptanwendung change(story,word2,word1).

Onur
quelle
2
Es wird nur funktionieren, wenn es genau ein Erscheinungsbild jeder Zeichenfolge gibt
Vic
-1
String word1 = "bar";
String word2 = "foo";

String story = "Once upon a time, there was a foo and a bar."

story = story.replace("foo", "<foo />");
story = story.replace("bar", "<bar />");

story = story.replace("<foo />", word1);
story = story.replace("<bar />", word2);
Amir Saniyan
quelle