Jak mogę zamienić dwa ciągi w taki sposób, aby jeden nie zastąpił drugiego?


162

Powiedzmy, że mam następujący kod:

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

Po uruchomieniu tego kodu wartość storywill"Once upon a time, there was a foo and a foo."

Podobny problem występuje, gdy wymieniłem je w odwrotnej kolejności:

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

Wartość storybędzie"Once upon a time, there was a bar and a bar."

Moim celem jest zamienić się storyw "Once upon a time, there was a bar and a foo."Jak mogłem to osiągnąć?


7
+1 zdecydowanie powinna istnieć funkcja, swap(String s1, String s2, String s3)która zamienia wszystkie wystąpienia s2z s3i na odwrót.
Ryan

Czy możemy założyć, że w danych wejściowych występuje tylko jedno wystąpienie każdego wymiennego słowa?
icza

14
Narożny przypadek: Czego oczekujemy jako wyniku podczas zamiany „ab” i „ba” w „ababababababa”?
Hagen von Eitzen

1
Masz poniżej kilka dobrych rozwiązań, ale czy rozumiesz, dlaczego Twoje podejście nie zadziałało? Najpierw masz „było foo i bar”. Po pierwszym zastąpieniu („foo” -> „bar”) masz „był pasek i pasek”. Masz teraz 2 wystąpienia „bar”, więc twoja druga zamiana nie robi tego, czego oczekujesz - nie ma sposobu, aby wiedzieć, że chcesz zastąpić tylko to, którego nie zastąpiłeś poprzednio. @HagenvonEitzen Interesting. Spodziewałbym się, że działające rozwiązanie dopasuje i zastąpi pierwszy znaleziony ciąg, a następnie powtórzy od końca zastępowanej sekcji.
DeveloperInDevelopment

1
Rozwiązanie Jeroena jest tym, którego często używam w edytorach tekstu, kiedy potrzebuję zbiorczej zmiany nazwy. Jest prosty, łatwy do zrozumienia, nie wymaga specjalnej biblioteki i może być niezawodny przy odrobinie myśli.
Hot Licks

Odpowiedzi:


88

Użyj replaceEach()metody z Apache Commons StringUtils :

StringUtils.replaceEach(story, new String[]{"foo", "bar"}, new String[]{"bar", "foo"})

2
masz pomysł, co dokładnie replaceEach robi wewnętrznie?
Marek

3
@Marek jest bardzo prawdopodobne, że funkcja wyszukuje i indeksuje każdy znaleziony element, a następnie zastępuje je wszystkie, gdy wszystkie zostaną zindeksowane.

16
Możesz znaleźć źródło tego tutaj, w okolicy linii 4684.
Jeroen Vannevel,

Szkoda jednak, że nie jest to operacja, gdy nulljest zdana.
prawostronny

87

Używasz wartości pośredniej (której jeszcze nie ma w zdaniu).

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

W odpowiedzi na krytykę: jeśli użyjesz wystarczająco dużego, nietypowego ciągu znaków, takiego jak zq515sqdqs5d5sq1dqs4d1q5dqqé "& é5d4sqjshsjddjhodfqsqc, nvùq ^ µù; d & € sdq: d:;), nawet jeśli debata jest nieprawdopodobna, nie jest to nawet prawdopodobne. że użytkownik kiedykolwiek to wprowadzi. Jedynym sposobem, aby dowiedzieć się, czy użytkownik to zrobi, jest znajomość kodu źródłowego i w tym momencie masz zupełnie inny poziom zmartwień.

Tak, może istnieją fantazyjne sposoby wyrażenia regularnego. Wolę coś czytelnego, o czym wiem, że mnie też nie wyrwie.

Powtarzając również doskonałą radę udzieloną przez @Davida Conrada w komentarzach :

Nie używaj jakiegoś sznurka, sprytnie (głupio) wybranego jako mało prawdopodobne. Użyj znaków z obszaru prywatnego użytku Unicode, U + E000..U + F8FF. Najpierw usuń wszystkie takie znaki, ponieważ nie powinny one poprawnie znajdować się na wejściu (mają tylko znaczenie specyficzne dla aplikacji w niektórych aplikacjach), a następnie użyj ich jako symboli zastępczych podczas zastępowania.


4
@arshajii Wydaje mi się, że zależy to od twojej definicji „lepszego”… jeśli to działa i jest akceptowalnie wydajne, przejdź do następnego zadania programistycznego i popraw go później podczas refaktoryzacji.
Matt Coubrough,

24
Oczywiście „lala” to tylko przykład. W produkcji powinieneś używać „ zq515sqdqs5d5sq1dqs4d1q5dqqé” & é & € sdq: d:;) àçàçlala ”.
Jeroen Vannevel,

81
Nie używaj jakiegoś sznurka, sprytnie (głupio) wybranego jako mało prawdopodobne. Użyj znaków z obszaru prywatnego użytku Unicode, U + E000..U + F8FF. Najpierw usuń wszystkie takie znaki, ponieważ nie powinny one poprawnie znajdować się na wejściu (mają tylko znaczenie specyficzne dla aplikacji w niektórych aplikacjach), a następnie użyj ich jako symboli zastępczych podczas zastępowania.
David Conrad,

22
Właściwie, po przeczytaniu FAQ Unicode na ten temat , myślę, że non-znaki z zakresu U + FDD0..U + FDEF byłyby jeszcze lepszym wyborem.
David Conrad,

6
@Taemyr Jasne, ale ktoś musi oczyścić wejście, prawda? Spodziewałbym się, że funkcja zastępująca ciąg działa na wszystkich ciągach, ale ta funkcja nie działa w przypadku niebezpiecznych danych wejściowych.
Navin

33

Możesz spróbować czegoś takiego, używając Matcher#appendReplacementi 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());
Kiedyś był bar i foo.

2
To działa, jeśli foo, bari storywszyscy mają nieznanych wartości?
Stephen P,

1
@StephenP Zasadniczo zakodowałem na stałe ciągi znaków "foo"i "bar"zastępujące, tak jak OP w swoim kodzie, ale ten sam typ podejścia działałby dobrze, nawet jeśli te wartości nie są znane (musiałbyś użyć if/ else ifzamiast a switchw while-pętla).
arshajii

6
Musisz zachować ostrożność podczas tworzenia wyrażenia regularnego. Pattern.quoteprzyda się, lub \Qi \E.
David Conrad

1
@arshajii - tak, udowodniłem to sobie jako metodę „swapThese”, przyjmując słowo1, słowo2 i historię jako parametry. +1
Stephen P

4
Jeszcze lepszym rozwiązaniem byłoby użycie wzorca, (foo)|(bar)a następnie sprawdzenie m.group(1) != null, aby uniknąć powtarzania dopasowanych słów.
Jörn Horstmann

32

To nie jest łatwy problem. Im więcej masz parametrów zastępujących wyszukiwanie, tym jest to trudniejsze. Masz kilka opcji, rozrzuconych na palecie brzydko-eleganckich, wydajnych-marnotrawnych:

  • Użyj StringUtils.replaceEachz Apache Commons zgodnie z zaleceniami @AlanHay . Jest to dobra opcja, jeśli możesz dodawać nowe zależności w swoim projekcie. Możesz mieć szczęście: zależność może być już uwzględniona w twoim projekcie

  • Użyj tymczasowego symbolu zastępczego, jak sugerował @Jeroen , i wykonaj wymianę w 2 krokach:

    1. Zastąp wszystkie wzorce wyszukiwania unikalnym tagiem, którego nie ma w oryginalnym tekście
    2. Zastąp symbole zastępcze rzeczywistym zamiennikiem celu

    Nie jest to dobre podejście z kilku powodów: musi zapewnić, że tagi użyte w pierwszym kroku są naprawdę niepowtarzalne; wykonuje więcej operacji wymiany łańcucha, niż jest to naprawdę konieczne

  • Budować regex ze wszystkich wzorów i korzystać z metody MatcheriStringBuffer jak sugeruje @arshajii . Nie jest to straszne, ale też nie takie świetne, ponieważ tworzenie wyrażenia regularnego jest trochę hakerskie i wymaga tego, StringBufferco wyszło z mody na korzyść StringBuilder.

  • Użyj rozwiązania rekurencyjnego zaproponowanego przez @mjolka , dzieląc ciąg na pasujące wzorce i rekurencyjnie na pozostałych segmentach. To dobre rozwiązanie, kompaktowe i dość eleganckie. Jego słabością jest potencjalnie wiele operacji podciągów i konkatenacji oraz ograniczenia rozmiaru stosu, które mają zastosowanie do wszystkich rozwiązań rekurencyjnych

  • Podziel tekst na słowa i użyj strumieni Java 8, aby elegancko wykonać zamiany, jak sugerował @msandiford , ale oczywiście działa to tylko wtedy, gdy dzielenie na granicach słów jest w porządku, co sprawia, że ​​nie nadaje się jako ogólne rozwiązanie

Oto moja wersja, oparta na pomysłach zapożyczonych z implementacji Apache . Nie jest to ani proste, ani eleganckie, ale działa i powinno być stosunkowo wydajne, bez zbędnych kroków. Krótko mówiąc, działa to w następujący sposób: wielokrotnie znajdź następny pasujący wzorzec wyszukiwania w tekście i użyj a, StringBuilderaby zebrać niedopasowane segmenty i zamienniki.

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

Testy jednostkowe:

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

21

Wyszukaj pierwsze słowo do zastąpienia. Jeśli znajduje się w ciągu, powtórz na części ciągu przed wystąpieniem i na części ciągu po wystąpieniu.

W przeciwnym razie przejdź do następnego słowa, które ma zostać zastąpione.

Tak może wyglądać naiwna implementacja

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

Przykładowe użycie:

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

Wynik:

Once upon a foo, there was a bar and a baz.

Mniej naiwna wersja:

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

Niestety, Java Stringnie ma indexOf(String str, int fromIndex, int toIndex)metody. PominąłemindexOf tutaj implementację , ponieważ nie jestem pewien, czy jest poprawna, ale można ją znaleźć na ideone , wraz z niektórymi przybliżonymi czasami różnych rozwiązań zamieszczonych tutaj.


2
Chociaż użycie istniejącej biblioteki, takiej jak Apache commons do takich rzeczy, jest bez wątpienia najłatwiejszym sposobem rozwiązania tego dość powszechnego problemu, pokazałeś implementację, która działa na częściach słów, na słowach ustalonych w czasie wykonywania i bez zastępowania podciągów magicznymi tokenami, w przeciwieństwie do (obecnie) wyżej ocenione odpowiedzi. +1
Buhb

Piękne, ale uderza w ziemię, gdy dostarczony jest plik wejściowy o wielkości 100 MB.
Christophe De Troyer

12

Jedna linijka w Javie 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());

11

Oto możliwość strumieni Java 8, która może być interesująca dla niektórych:

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

Oto przybliżenie tego samego algorytmu w Javie 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);

10
To fajna sugestia, gdy rzeczy, które chcesz zastąpić, są rzeczywistymi słowami oddzielonymi spacjami (lub podobnymi), ale to nie zadziała przy zamianie podciągów słowa.
Simon Forsberg,

+1 dla strumieni Java8. Szkoda, że ​​wymaga to separatora.
Navin

6

Jeśli chcesz zamienić słowa w zdaniu oddzielone białymi znakami, jak pokazano na przykładzie, możesz użyć tego prostego algorytmu.

  1. Podziel się historią na białej przestrzeni
  2. Wymień każdy element, jeśli foo, zamień go na bar i vice varsa
  3. Połącz tablicę z powrotem w jeden ciąg

Jeśli dzielenie w przestrzeni jest nie do przyjęcia, można zastosować ten alternatywny algorytm. Najpierw musisz użyć dłuższego sznurka. Jeśli stringi są foo i fool, musisz najpierw użyć fool, a następnie foo.

  1. Podziel na słowo foo
  2. Zastąp bar foo każdym elementem tablicy
  3. Połącz tę tablicę z powrotem, dodając pasek po każdym elemencie oprócz ostatniego

1
To też chciałem zasugerować. Chociaż dodaje ograniczenie, że tekst jest słowami otoczonymi spacjami. :)
Deweloper Marius Žilėnas

@ MariusŽilėnas Dodałem alternatywny algorytm.
fastcodejava

5

Oto mniej skomplikowana odpowiedź przy użyciu mapy.

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

I nazywa się metoda

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

Wynik: awesome jest Raffy, Raffy Raffy jest niesamowity, niesamowity


1
bieganie replaced.replaceAll("Raffy", "Barney");za tym sprawi, że będzie legen… poczekaj na to; Dary !!!
Keale

3

Jeśli chcesz mieć możliwość obsługi wielu wystąpień ciągów wyszukiwania, które mają zostać zastąpione, możesz to łatwo zrobić, dzieląc ciąg dla każdego wyszukiwanego terminu, a następnie zastępując go. Oto przykład:

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

3

Możesz osiągnąć swój cel za pomocą następującego bloku kodu:

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

Zastępuje słowa niezależnie od kolejności. Możesz rozszerzyć tę zasadę na metodę użytkową, taką jak:

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

Które byłyby spożywane jako:

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

3

To działa i jest proste:

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

Używasz tego w ten sposób:

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

Uwaga: liczy się to, że ciągi nie zawierają znaku \ufdd0, który jest znakiem trwale zarezerwowanym do użytku wewnętrznego przez Unicode (patrz http://www.unicode.org/faq/private_use.html ):

Nie sądzę, aby to było konieczne, ale jeśli chcesz być całkowicie bezpieczny, możesz użyć:

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

3

Zamiana tylko jednego wystąpienia

Jeśli na wejściu występuje tylko jedno wystąpienie każdego z wymienialnych ciągów, możesz wykonać następujące czynności:

Przed przystąpieniem do jakiejkolwiek zamiany, uzyskaj indeksy wystąpień słów. Następnie zastępujemy tylko słowo znalezione w tych indeksach, a nie wszystkie wystąpienia. To rozwiązanie wykorzystuje StringBuilderi nie wytwarza Stringpodobnych produktów pośrednich String.replace().

Jedna uwaga: jeśli wymienialne słowa mają różne długości, po pierwszym zastąpieniu drugi indeks może się zmienić (jeśli pierwsze słowo występuje przed drugim) dokładnie z różnicą dwóch długości. Zatem wyrównanie drugiego indeksu zapewni, że będzie to działać, nawet jeśli zamieniamy słowa o różnej długości.

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

Zamiana dowolnej liczby wystąpień

Analogicznie do poprzedniego przypadku najpierw zbierzemy indeksy (wystąpienia) słów, ale w tym przypadku będzie to lista liczb całkowitych dla każdego słowa, a nie tylko jednego int. W tym celu użyjemy następującej metody narzędziowej:

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

Używając tego, zastąpimy te słowa innym, zmniejszając indeks (co może wymagać naprzemiennego przełączania między 2 wymiennymi słowami), abyśmy nie musieli nawet poprawiać indeksów po zamianie:

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

Nie jestem pewien, jak java obsługuje Unicode, ale odpowiednik tego kodu w języku C # byłby niepoprawny. Problem polega na tym, że podciąg, który indexOfpasuje, może nie mieć takiej samej długości jak szukany ciąg ze względu na idiosynkrazje równoważności ciągów znaków Unicode.
CodesInChaos

@CodesInChaos Działa bezbłędnie w Javie, ponieważ Java Stringjest tablicą znaków, a nie tablicą bajtów. Wszystkie metody Stringi StringBuilderoperują na znakach nie na bajtach, które są „wolne od kodowania”. Zatem indexOfdopasowania mają dokładnie taką samą (znakową) długość jak wyszukiwane ciągi.
icza

Zarówno w języku C #, jak iw języku Java, ciąg jest sekwencją jednostek kodu UTF-16. Problem polega na tym, że istnieją różne sekwencje punktów kodowych, które Unicode uważa za równoważne. Na przykład ämoże być zakodowany jako pojedynczy punkt kodowy lub jako anastępująca po nim kombinacja ¨. Istnieją również pewne punkty kodowe, które są ignorowane, na przykład łączniki o zerowej szerokości (nie). Nie ma znaczenia, czy łańcuch składa się z bajtów, znaków czy czegokolwiek, ale jakie reguły porównawcze są indexOfużywane. Może używać prostego porównania jednostka-jednostka przez jednostkę kodu („porządkowa”) lub może implementować równoważność Unicode. Nie wiem, który java wybrał.
CodesInChaos

Na przykład "ab\u00ADc".IndexOf("bc")zwraca 1w .net dopasowując ciąg dwóch znaków bcdo ciągu trzech znaków.
CodesInChaos

1
@CodesInChaos Teraz rozumiem, co masz na myśli. W Javie "ab\u00ADc".indexOf("bc")zwraca, -1które oznacza, że "bc"nie znaleziono w "ab\u00ADc". Tak więc nadal indexOf()wygląda na to, że w Javie powyższy algorytm działa, dopasowania mają dokładnie taką samą ( indexOf()znakową ) długość jak wyszukiwane ciągi i zgłaszają dopasowania tylko wtedy, gdy są zgodne sekwencje znaków (punkty kodowe).
icza

2

Łatwo jest napisać metodę, aby to zrobić, używając 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();
}

Testowanie:

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

Wynik:

Są trzy psy i dwie papużki.

Nie jest to od razu oczywiste, ale taka funkcja może nadal zależeć od kolejności, w której określane są zamienniki. Rozważać:

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

Wynik:

Java jest dla JavaScript jak Ham dla Hamster

Ale odwróć zamienniki:

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

Wynik:

Java jest dla JavaScript jak Ham dla HamScript

Ups! :)

Dlatego czasami warto upewnić się, że szuka się najdłuższego dopasowania (tak jak strtrna przykład robi to funkcja PHP ). Ta wersja metody zrobi to:

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

Zwróć uwagę, że powyższe metody uwzględniają wielkość liter. Jeśli potrzebujesz wersji bez rozróżniania wielkości liter, możesz łatwo zmodyfikować powyższe, ponieważ String.regionMatchesmoże przyjmować ignoreCaseparametr.


2

Jeśli nie chcesz żadnych zależności, możesz po prostu użyć tablicy, która pozwala tylko na jednorazową zmianę. Nie jest to najbardziej wydajne rozwiązanie, ale powinno działać.

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

Wtedy to powinno zadziałać.

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.

2

Na wejściu wykonujesz wiele operacji wyszukiwania-zamiany. Spowoduje to niepożądane wyniki, gdy ciągi zastępcze zawierają ciągi wyszukiwania. Rozważmy przykład foo-> bar, bar-foo, oto wyniki dla każdej iteracji:

  1. Kiedyś było tam foo i bar. (Wejście)
  2. Kiedyś był bar i bar. (foo-> bar)
  3. Dawno, dawno temu było foo i foo. (bar-> foo, wyjście)

Musisz wykonać zamianę w jednej iteracji bez cofania się. Rozwiązanie siłowe jest następujące:

  1. Przeszukuj dane wejściowe od bieżącej pozycji do końca pod kątem wielu ciągów wyszukiwania, aż do znalezienia dopasowania
  2. Zastąp pasujący ciąg wyszukiwania odpowiednim ciągiem zastępczym
  3. Ustaw bieżącą pozycję na następny znak po zastąpionym ciągu
  4. Powtarzać

Taka funkcja String.indexOfAny(String[]) -> int[]{index, whichString}byłaby przydatna. Oto przykład (nie najbardziej wydajny):

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

Niektóre testy:

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 na IDEONE
Demo na IDEONE, alternatywny kod


1

Zawsze możesz zastąpić go słowem, co do którego jesteś pewien, że nie pojawi się nigdzie indziej w ciągu, a następnie wykonaj drugą zamianę później:

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

Zauważ, że to nie zadziała, jeśli "StringYouAreSureWillNeverOccur"tak się stanie.


5
Użyj znaków z obszaru prywatnego użytku Unicode, U + E000..U + F8FF, tworząc StringThatCannotEverOccur. Możesz je wcześniej odfiltrować, ponieważ nie powinny istnieć w danych wejściowych.
David Conrad,

Lub U + FDD0..U + FDEF, „Noncharacters”, które są zarezerwowane do użytku wewnętrznego.
David Conrad,

1

Rozważ użycie StringBuilder

Następnie zapisz indeks, od którego powinien zaczynać się każdy ciąg. Jeśli używasz znaku zastępczego w każdej pozycji, usuń go i wstaw ciąg użytkownika. Następnie można odwzorować pozycję końcową, dodając długość ciągu do pozycji początkowej.

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;

1

Jedyne, czym mogę się podzielić, to moja własna metoda.

Możesz użyć tymczasowego String temp = "<?>";lubString.Format();

To jest mój przykładowy kod utworzony w aplikacji konsoli za pośrednictwem - „Tylko pomysł, brak dokładnej odpowiedzi” .

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

Możesz też użyć rozszerzenia 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;
        }

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


To dość hacky. Co zrobisz, jeśli on zechce zastąpić „_”?
Pier-Alexandre Bouchard

@ Pier-AlexandreBouchard W metodach zmieniam wartość tempz "_"na <?>. Ale w razie potrzeby może dodać kolejny parametr do metody, która zmieni temp. - "lepiej jest to proste, prawda?"
Leonel Sarmiento

Chodzi mi o to, że nie możesz zagwarantować oczekiwanego wyniku, ponieważ jeśli temp == zamienisz, twój sposób nie zadziała.
Pier-Alexandre Bouchard

1

Oto moja wersja oparta na słowach:

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

    }

}

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

Trochę trudny sposób, ale musisz jeszcze sprawdzić.

1. przekształcić ciąg znaków w tablicę znaków

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

2.loop na temp i wymienić fooz bari barze foojak nie ma szans na uzyskanie wymienną ciąg ponownie.


1

Cóż, krótsza odpowiedź brzmi ...

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

1

Korzystając z odpowiedzi znalezionej tutaj , możesz znaleźć wszystkie wystąpienia ciągów, które chcesz zastąpić.

Na przykład uruchamiasz kod w powyższej odpowiedzi SO. Utwórz dwie tabele indeksów (powiedzmy, że bar i foo nie pojawiają się tylko raz w ciągu) i możesz pracować z tymi tabelami nad zastąpieniem ich w ciągu.

Teraz do zamiany w określonych lokalizacjach indeksu możesz użyć:

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

Natomiast posjest to indeks, w którym zaczynają się twoje ciągi (z tabel indeksów, które cytowałem powyżej). Powiedzmy, że utworzyłeś dwie tabele indeksów dla każdej z nich. Nazwijmy je indexBari indexFoo.

Teraz zastępując je, możesz po prostu uruchomić dwie pętle, po jednej dla każdej wymiany, którą chcesz wykonać.

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

Podobnie kolejna pętla dla indexFoo.

To może nie być tak wydajne jak inne odpowiedzi tutaj, ale jest łatwiejsze do zrozumienia niż Mapy lub inne rzeczy.

Dałoby to zawsze pożądany wynik i wiele możliwych wystąpień każdego ciągu. Tak długo, jak przechowujesz indeks każdego wystąpienia.

Również ta odpowiedź nie wymaga rekursji ani żadnych zewnętrznych zależności. Jeśli chodzi o złożoność, to prawdopodobnie jest to O (n do kwadratu), podczas gdy n jest sumą wystąpień obu słów.


-1

Opracowałem ten kod, który rozwiąże problem:

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

W głównym zastosowaniu change(story,word2,word1).


2
Będzie działać tylko wtedy, gdy wystąpi dokładnie jeden występ każdego ciągu
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);
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.