Come posso sostituire due stringhe in modo che una non finisca per sostituire l'altra?


162

Diciamo che ho il seguente codice:

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

Dopo l'esecuzione di questo codice, il valore di storysarà"Once upon a time, there was a foo and a foo."

Un problema simile si verifica se li ho sostituiti nell'ordine opposto:

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

Il valore di storysarà"Once upon a time, there was a bar and a bar."

Il mio obiettivo è quello di trasformarmi storyin "Once upon a time, there was a bar and a foo."Come potrei farlo?


7
+1 dovrebbe sicuramente esserci una funzione swap(String s1, String s2, String s3)che scambia tutte le occorrenze di s2con s3e viceversa.
Ryan,

Possiamo supporre che ci sia una sola occorrenza di ciascuna delle parole scambiabili nell'input?
Icza,

14
Caso d'angolo: cosa ci aspettiamo come output quando si scambiano "ab" e "ba" in "ababababababa"?
Hagen von Eitzen,

1
Di seguito hai alcune buone soluzioni, ma capisci perché il tuo approccio non ha funzionato? Innanzitutto, hai "c'era un foo e un bar". Dopo il primo rimpiazzo ("pippo" -> "bar") hai "c'era un bar e un bar". Ora hai 2 occorrenze di "barra", quindi il tuo secondo rimpiazzo non fa quello che ti aspetti - non ha modo di sapere che vuoi solo sostituire quello che non hai già sostituito l'ultima volta. @HagenvonEitzen Interessante. Mi aspetterei che una soluzione funzionante corrisponda e sostituisca la prima di una delle stringhe che trova e quindi ripeti dalla fine della sezione sostituita.
DeveloperInDevelopment

1
La soluzione di Jeroen è quella che uso frequentemente negli editor di testo, quando ho bisogno di rinominare in blocco. È semplice, facile da capire, non richiede alcuna libreria speciale e può essere infallibile con un minimo di pensiero.
Hot Licks

Risposte:


88

Utilizzare il replaceEach()metodo da Apache Commons StringUtils :

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

2
hai idea di cosa sostituisca esattamente ogni cosa internamente?
Marek,

3
@Marek è molto probabile che la funzione esegua una ricerca e indicizzi ogni elemento trovato, quindi li sostituisca tutti dopo che sono stati tutti indicizzati.

16
Puoi trovare la fonte per questo qui intorno alla linea 4684.
Jeroen Vannevel

È un peccato che sia una no-op quando nullviene superato, però.
destra

87

Si utilizza un valore intermedio (che non è ancora presente nella frase).

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

In risposta alle critiche: se si utilizza una stringa di grandi dimensioni non comuni abbastanza come zq515sqdqs5d5sq1dqs4d1q5dqqé "& é5d4sqjshsjddjhodfqsqc, nvùq ^ uU; d & € SDQ: d:;) àçàçlala e l'uso che, è improbabile che il punto in cui non voglio neanche discuterne che un utente potrà mai inserire questo. L'unico modo per sapere se un utente lo farà è conoscere il codice sorgente e a quel punto sei con un altro livello di preoccupazioni.

Sì, forse ci sono modi regex fantasiosi. Preferisco qualcosa di leggibile che so che non scoppierà nemmeno su di me.

Ribadendo anche l'eccellente consiglio dato da @David Conrad nei commenti :

Non usare una stringa abilmente (stupidamente) scelta per essere improbabile. Usa i caratteri dell'area di utilizzo privato Unicode, U + E000..U + F8FF. Rimuovere prima tali caratteri, poiché non dovrebbero essere legittimamente presenti nell'input (hanno solo un significato specifico dell'applicazione all'interno di un'applicazione), quindi usarli come segnaposto durante la sostituzione.


4
@arshajii Immagino che dipende dalla tua definizione di "migliore" ... se funziona ed è accettabilmente performante, passare al prossimo compito di programmazione e migliorarlo in seguito durante il refactoring sarebbe il mio approccio.
Matt Coubrough,

24
Ovviamente "lala" è solo un esempio. In produzione, dovresti usare " zq515sqdqs5d5sq1dqs4d1q5dqqé" & é & € sdq: d:;) àçàçlala ".
Jeroen Vannevel

81
Non usare una stringa abilmente (stupidamente) scelta per essere improbabile. Usa i caratteri dell'area di utilizzo privato Unicode, U + E000..U + F8FF. Rimuovere prima tali caratteri, poiché non dovrebbero essere legittimamente presenti nell'input (hanno solo un significato specifico dell'applicazione all'interno di un'applicazione), quindi usarli come segnaposto durante la sostituzione.
David Conrad,

22
In realtà, dopo aver letto le FAQ Unicode su di esso , penso che i non caratteri nell'intervallo U + FDD0..U + FDEF sarebbero una scelta ancora migliore.
David Conrad,

6
@Taemyr Certo, ma qualcuno deve disinfettare l'input, giusto? Mi aspetto che una funzione di sostituzione delle stringhe funzioni su tutte le stringhe, ma questa funzione si interrompe per input non sicuri.
Navin,

33

Puoi provare qualcosa del genere, usando Matcher#appendReplacemente 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());
C'era una volta un bar e un foo.

2
Funziona se foo, bare storytutti hanno valori sconosciuti?
Stephen P,

1
@StephenP Ho essenzialmente codificato il codice "foo"e le "bar"stringhe di sostituzione come l'OP aveva nel suo codice, ma lo stesso tipo di approccio funzionerebbe bene anche se quei valori non fossero noti (dovresti usare if/ else ifinvece di switchall'interno di while-ciclo continuo).
Arshajii,

6
Dovresti stare attento a creare la regex. Pattern.quotesarebbe utile, o \Qe \E.
David Conrad,

1
@arshajii - sì, l'ho dimostrato a me stesso come un metodo "swapThese" prendendo parola1, parola2 e storia come parametri. +1
Stephen P,

4
Anche più pulito sarebbe usare il modello (foo)|(bar)e quindi controllare contro m.group(1) != null, per evitare di ripetere le parole per abbinare.
Jörn Horstmann,

32

Questo non è un problema facile. E più parametri di sostituzione della ricerca hai, più diventa difficile. Hai diverse opzioni, sparse sulla tavolozza di brutti-eleganti, efficienti-dispendiosi:

  • Utilizzare StringUtils.replaceEachda Apache Commons come raccomandato da @AlanHay . Questa è una buona opzione se sei libero di aggiungere nuove dipendenze nel tuo progetto. Potresti essere fortunato: la dipendenza potrebbe essere già inclusa nel tuo progetto

  • Usa un segnaposto temporaneo come suggerito da @Jeroen ed esegui la sostituzione in 2 passaggi:

    1. Sostituisci tutti i modelli di ricerca con un tag univoco che non esiste nel testo originale
    2. Sostituisci i segnaposto con la sostituzione del bersaglio reale

    Questo non è un ottimo approccio, per diversi motivi: deve garantire che i tag utilizzati nel primo passaggio siano davvero unici; esegue più operazioni di sostituzione delle stringhe del necessario

  • Crea una regex da tutti gli schemi e usa il metodo con MatchereStringBuffer come suggerito da @arshajii . Questo non è terribile, ma neanche così grande, dato che costruire la regex è un po 'hacker, e implica StringBufferche è passato di moda qualche tempo fa a favore StringBuilder.

  • Utilizzare una soluzione ricorsiva proposta da @mjolka , suddividendo la stringa in corrispondenza dei motivi corrispondenti e ricorrendo ai segmenti rimanenti. Questa è una soluzione eccellente, compatta e piuttosto elegante. Il suo punto debole sono le numerose operazioni di sottostringa e concatenazione e i limiti di dimensione dello stack che si applicano a tutte le soluzioni ricorsive

  • Dividi il testo in parole e usa i flussi Java 8 per eseguire le sostituzioni in modo elegante come suggerito da @msandiford , ma ovviamente funziona solo se sei d' accordo con la divisione ai confini delle parole, il che lo rende non adatto come soluzione generale

Ecco la mia versione, basata su idee prese in prestito dall'implementazione di Apache . Non è né semplice né elegante, ma funziona e dovrebbe essere relativamente efficiente, senza passaggi inutili. In breve, funziona in questo modo: trova ripetutamente il modello di ricerca corrispondente corrispondente nel testo e usa a StringBuilderper accumulare i segmenti senza pari e le sostituzioni.

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

Test unitari:

@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

Cerca la prima parola da sostituire. Se si trova nella stringa, ricorrere alla parte della stringa prima dell'occorrenza e alla parte della stringa dopo l'occorrenza.

Altrimenti, continua con la parola successiva da sostituire.

Un'implementazione ingenua potrebbe apparire così

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

Esempio di utilizzo:

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

Produzione:

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

Una versione meno ingenua:

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

Sfortunatamente, Java Stringnon ha un indexOf(String str, int fromIndex, int toIndex)metodo. Ho omesso l'implementazione di indexOfqui poiché non sono sicuro che sia corretto, ma può essere trovato su ideone , insieme ad alcuni tempi approssimativi di varie soluzioni pubblicate qui.


2
Sebbene l'utilizzo di una libreria esistente come Apache Commons per cose come questa sia senza dubbio il modo più semplice per risolvere questo problema abbastanza comune, hai mostrato un'implementazione che funziona su parti di parole, su parole decise in runtime e senza sostituire sottostringhe con token magici a differenza (attualmente) risposte più votate. +1
Buhb

Bello, ma colpisce il terreno quando viene fornito un file di input di 100 mb.
Christophe De Troyer,

12

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

11

Ecco una possibilità di flussi Java 8 che potrebbe essere interessante per alcuni:

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

Ecco un'approssimazione dello stesso algoritmo 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);

10
Questo è un buon suggerimento quando le cose che vuoi sostituire sono parole reali separate da spazi (o simili), ma ciò non funzionerebbe per sostituire sottostringhe di una parola.
Simon Forsberg,

+1 per flussi Java8. Peccato che questo richiede un delimitatore.
Navin,

6

Se vuoi sostituire le parole in una frase che sono separate da uno spazio bianco come mostrato nel tuo esempio, puoi usare questo semplice algoritmo.

  1. Storia divisa su uno spazio bianco
  2. Sostituisci ogni elemento, se lo sostituisci a barra e viceversa
  3. Unire nuovamente l'array in una stringa

Se la divisione sullo spazio non è accettabile, è possibile seguire questo algoritmo alternativo. Devi prima usare la stringa più lunga. Se le stringhe sono pazzi e pazzi, devi prima usare il pazzo e poi il pazzo.

  1. Dividi sulla parola pippo
  2. Sostituire la barra con foo ogni elemento dell'array
  3. Unisciti all'array indietro aggiungendo la barra dopo ogni elemento tranne l'ultimo

1
Questo è quello che stavo pensando di suggerire anche io. Anche se aggiunge una restrizione che il testo è parole circondate da spazi. :)
Sviluppatore Marius Žilėnas,

@ MariusŽilėnas Ho aggiunto un algoritmo alternativo.
fastcodejava,

5

Ecco una risposta meno complicata usando 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;
     }

E viene chiamato il metodo

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

L'output è: fantastico è Raffy, Raffy Raffy è fantastico


1
correre replaced.replaceAll("Raffy", "Barney");dietro questo lo renderà legen ... aspettarlo; Dary !!!
Keale,

3

Se vuoi essere in grado di gestire più occorrenze delle stringhe di ricerca da sostituire, puoi farlo facilmente suddividendo la stringa su ciascun termine di ricerca, quindi sostituendola. Ecco un esempio:

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

Puoi raggiungere il tuo obiettivo con il seguente blocco di codice:

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

Sostituisce le parole indipendentemente dall'ordine. È possibile estendere questo principio in un metodo di utilità, come:

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

Che sarebbe consumato come:

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

Funziona ed è semplice:

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

Lo usi in questo modo:

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

Nota: questo conta su stringhe che non contengono caratteri \ufdd0, che è un carattere permanentemente riservato per uso interno da Unicode (Vedi http://www.unicode.org/faq/private_use.html ):

Non penso sia necessario, ma se vuoi essere assolutamente sicuro puoi usare:

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

Scambio di una sola occorrenza

Se si verifica una sola occorrenza di ciascuna delle stringhe scambiabili nell'input, è possibile effettuare le seguenti operazioni:

Prima di procedere a qualsiasi sostituzione, ottenere gli indici delle occorrenze delle parole. Successivamente sostituiamo solo la parola trovata in questi indici e non tutte le occorrenze. Questa soluzione usa StringBuildere non produce Stringsimili intermedi String.replace().

Una cosa da notare: se le parole scambiabili hanno lunghezze diverse, dopo la prima sostituzione il secondo indice potrebbe cambiare (se la prima parola appare prima della seconda) esattamente con la differenza delle 2 lunghezze. Quindi l'allineamento del secondo indice assicurerà che funzioni anche se scambiamo parole con lunghezze diverse.

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

Scambio di numero arbitrario di occorrenze

Analogamente al caso precedente, per prima cosa raccoglieremo gli indici (occorrenze) delle parole, ma in questo caso verrà visualizzato un elenco di numeri interi per ogni parola, non solo uno int. Per questo useremo il seguente metodo di utilità:

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

E usando questo sostituiremo le parole con l'altro diminuendo l'indice (che potrebbe richiedere di alternare tra le 2 parole scambiabili) in modo che non dovremo nemmeno correggere gli indici dopo una sostituzione:

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

Non sono sicuro di come java gestisca l'unicode, ma l'equivalente C # di questo codice non sarebbe corretto. Il problema è che la sottostringa che indexOfcorrisponde potrebbe non avere la stessa lunghezza della stringa di ricerca grazie alle idiosincrasie dell'equivalenza di stringa unicode.
CodesInChaos

@CodesInChaos Funziona perfettamente in Java perché un Java Stringè un array di caratteri e non un array di byte. Tutti i metodi Stringe StringBuilderoperano su caratteri non su byte, che sono "senza codifica". Pertanto le indexOfcorrispondenze hanno esattamente la stessa lunghezza (carattere) delle stringhe di ricerca.
Icza,

Sia in C # che in java una stringa è una sequenza di unità di codice UTF-16. Il problema è che esistono diverse sequenze di punti di codice che unicode considera equivalenti. Ad esempio äpuò essere codificato come un singolo punto di codice o come aseguito da una combinazione ¨. Ci sono anche alcuni punti di codice che vengono ignorati, come i joiner di larghezza zero (non). Non importa se la stringa è composta da byte, caratteri o altro, ma quali regole di confronto indexOfutilizzano. Potrebbe usare semplicemente unità di codice per confronto unità di codice ("Ordinale") oppure implementare l'equivalenza unicode. Non so quale java abbia scelto.
CodesInChaos

Ad esempio, "ab\u00ADc".IndexOf("bc")restituisce 1in .net abbinando la stringa bcdi due caratteri a una stringa di tre caratteri.
CodesInChaos

1
@CodesInChaos Capisco cosa intendi ora. In Java "ab\u00ADc".indexOf("bc")restituisce il -1che significa che "bc"non è stato trovato in "ab\u00ADc". Quindi è ancora vero che in Java l'algoritmo sopra funziona, le indexOf()corrispondenze hanno esattamente la stessa lunghezza (carattere) delle stringhe di ricerca e indexOf()riportano corrispondenze solo se le sequenze (punti di codice) corrispondono.
Icza,

2

È facile scrivere un metodo per farlo usando 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();
}

test:

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

Produzione:

Ci sono tre cani e due pappagallini.

Non è immediatamente ovvio, ma una funzione come questa può ancora dipendere dall'ordine in cui vengono specificate le sostituzioni. Tener conto di:

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

Produzione:

Java è JavaScript come Ham è come criceto

Ma invertire i ricambi:

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

Produzione:

Java è JavaScript come Ham come HamScript

Oops! :)

Pertanto a volte è utile assicurarsi di cercare la corrispondenza più lunga (come strtrad esempio la funzione di PHP ). Questa versione del metodo lo farà:

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

Si noti che i metodi sopra riportati fanno distinzione tra maiuscole e minuscole. Se hai bisogno di una versione senza distinzione tra maiuscole e minuscole, è facile modificare quanto sopra perché String.regionMatchespuò accettare un ignoreCaseparametro.


2

Se non si desidera alcuna dipendenza, è possibile utilizzare semplicemente un array che consente solo una modifica una tantum. Questa non è la soluzione più efficiente, ma dovrebbe funzionare.

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

Quindi, dovrebbe funzionare.

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

Si stanno eseguendo più operazioni di sostituzione della ricerca sull'input. Ciò produrrà risultati indesiderati quando le stringhe di sostituzione contengono stringhe di ricerca. Considera l'esempio foo-> bar, bar-foo, ecco i risultati per ogni iterazione:

  1. C'era una volta un foo e un bar. (ingresso)
  2. C'era una volta un bar e un bar. (Foo-> bar)
  3. C'era una volta un foo e un foo. (bar-> foo, output)

È necessario eseguire la sostituzione in un'unica iterazione senza tornare indietro. Una soluzione a forza bruta è la seguente:

  1. Cerca l'input dalla posizione corrente alla fine per più stringhe di ricerca fino a quando non viene trovata una corrispondenza
  2. Sostituisci la stringa di ricerca corrispondente con la stringa di sostituzione corrispondente
  3. Imposta la posizione corrente sul carattere successivo dopo la stringa sostituita
  4. Ripetere

Una funzione come String.indexOfAny(String[]) -> int[]{index, whichString}sarebbe utile. Ecco un esempio (non il più efficiente):

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

Alcuni test:

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 su IDEONE
Demo su IDEONE, codice alternativo


1

Puoi sempre sostituirlo con una parola che sicuramente non apparirà in nessun altro punto della stringa, quindi esegui la seconda sostituzione in seguito:

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

Si noti che questo non funzionerà correttamente se "StringYouAreSureWillNeverOccur"si verifica.


5
Utilizzare i caratteri dell'area di utilizzo privato Unicode, U + E000..U + F8FF, creando un StringThatCannotEverOccur. Puoi filtrarli in anticipo poiché non dovrebbero esistere nell'input.
David Conrad,

Oppure U + FDD0..U + FDEF, i "Non caratteri", che sono riservati per uso interno.
David Conrad,

1

Prendi in considerazione l'utilizzo di StringBuilder

Quindi memorizzare l'indice da cui dovrebbe iniziare ciascuna stringa. Se usi un carattere segnaposto in ogni posizione, rimuovilo e inserisci la stringa dell'utente. È quindi possibile mappare la posizione finale aggiungendo la lunghezza della stringa alla posizione iniziale.

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

Quello che posso solo condividere è il mio metodo.

Puoi usare un temporaneo String temp = "<?>";oString.Format();

Questo è il mio codice di esempio creato nell'applicazione console tramite - "Solo idea, risposta non esatta" .

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

Oppure puoi anche usare il 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;
        }

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


È piuttosto confuso. Cosa farai se vuole sostituire "_"?
Pier-Alexandre Bouchard,

@ Pier-AlexandreBouchard Nei metodi cambio il valore di tempda "_"in <?>. Ma se necessario, ciò che può fare è aggiungere un altro parametro al metodo che cambierà la temperatura. - "è meglio mantenerlo semplice, giusto?"
Leonel Sarmiento,

Il punto è che non puoi garantire il risultato atteso perché se la temp == sostituisce, la tua strada non funzionerà.
Pier-Alexandre Bouchard,

1

Ecco la mia versione, che è basata su parole:

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."

Un modo un po 'complicato ma devi fare qualche altro controllo.

1. converte la stringa nella matrice di caratteri

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

2.loop su temp e sostituirlo foocon bare barcon foopoiché non vi è alcuna possibilità di ottenere nuovamente una stringa sostituibile.


1

Bene, la risposta più breve è ...

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

Utilizzando la risposta trovata qui è possibile trovare tutte le occorrenze delle stringhe con cui si desidera sostituire.

Quindi, ad esempio, esegui il codice nella risposta SO sopra. Crea due tabelle di indici (diciamo che bar e foo non compaiono una sola volta nella tua stringa) e puoi lavorare con quelle tabelle per sostituirle nella tua stringa.

Ora per la sostituzione in posizioni di indice specifiche è possibile utilizzare:

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

Considerando che posè l'indice da cui iniziano le stringhe (dalle tabelle dell'indice che ho citato sopra). Supponiamo quindi che tu abbia creato due tabelle di indici per ognuna. Chiamiamoli indexBare indexFoo.

Ora sostituendoli puoi semplicemente eseguire due loop, uno per ogni sostituzione che desideri effettuare.

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

Allo stesso modo un altro loop per indexFoo.

Questo potrebbe non essere efficiente come le altre risposte qui, ma è più semplice da capire rispetto a Maps o altre cose.

Questo ti darebbe sempre il risultato che desideri e per più occorrenze possibili di ogni stringa. Finché memorizzi l'indice di ogni occorrenza.

Anche questa risposta non necessita di ricorsione né di dipendenze esterne. Per quanto riguarda la complessità, è probabilmente O (n al quadrato), mentre n è la somma delle occorrenze di entrambe le parole.


-1

Ho sviluppato questo codice risolverà il problema:

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

Nell'uso principale change(story,word2,word1).


2
Funzionerà solo se ci sarà esattamente un aspetto di ogni stringa
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);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.