Como posso substituir duas cadeias de uma maneira que uma não acabe substituindo a outra?

162

Digamos que eu tenho o seguinte código:

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

Após a execução desse código, o valor de storyserá"Once upon a time, there was a foo and a foo."

Um problema semelhante ocorre se eu os substitui na ordem oposta:

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

O valor de storyserá"Once upon a time, there was a bar and a bar."

Meu objetivo é transformar storyem "Once upon a time, there was a bar and a foo."Como eu poderia fazer isso?

Pikamander2
fonte
7
+1 definitivamente deve haver alguma função swap(String s1, String s2, String s3)que troque todas as ocorrências de s2with s3e vice-versa.
Ryan
Podemos assumir que há apenas uma ocorrência de cada uma das palavras trocáveis ​​na entrada?
Icza
14
Caso de canto: o que esperamos como saída ao trocar "ab" e "ba" em "ababababababa"?
Hagen von Eitzen
1
Você tem algumas boas soluções abaixo, mas entende por que sua abordagem não funcionou? Primeiro, você tem "houve um foo e um bar". Após a primeira substituição ("foo" -> "bar"), você tem "havia um bar e um bar". Agora você tem 2 ocorrências de "bar", portanto, sua segunda substituição não faz o que você espera - não há como saber que você deseja substituir apenas a que você ainda não substituiu da última vez. @HagenvonEitzen Interesting. Eu esperaria que uma solução de trabalho correspondesse e substituísse a primeira das duas seqüências que encontrar e depois repetisse do final da seção substituída.
DeveloperInDevelopment
1
A solução da Jeroen é aquela que uso com frequência em editores de texto, quando preciso renomear em massa. É simples, fácil de entender, não requer biblioteca especial e pode ser infalível com um pouco de pensamento.
Hot Licks

Respostas:

88

Use o replaceEach()método do Apache Commons StringUtils :

StringUtils.replaceEach(story, new String[]{"foo", "bar"}, new String[]{"bar", "foo"})
Alan Hay
fonte
2
Alguma idéia do que exatamente substituir Cada um faz internamente?
Marek
3
@Marek é muito provável que a função faça uma pesquisa e indexe cada item encontrado, depois substitua todos eles depois que todos tiverem sido indexados.
16
Você pode encontrar a fonte para este aqui em torno da linha 4684.
Jeroen Vannevel
É uma pena que não seja uma opção quando nullé aprovada, no entanto.
rightfold
87

Você usa um valor intermediário (que ainda não está presente na frase).

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

Como resposta às críticas: se você usar uma seqüência incomum grande o suficiente, como zq515sqdqs5d5sq1dqs4d1q5dqqé "& é5d4sqjshsjddjhodfqsqc, nvùq ^ µù; d & € sdq: d:;) àçlala e uso isso, é improvável que o ponto onde eu não vou nem discutir isso A única maneira de saber se o usuário o digitará é conhecendo o código-fonte e, nesse ponto, você estará com um outro nível de preocupação.

Sim, talvez haja maneiras regex sofisticadas. Eu prefiro algo legível que eu sei que também não vai acontecer comigo.

Reiterando também o excelente conselho dado por @David Conrad nos comentários :

Não use alguma corda inteligente (estupidamente) escolhida para ser improvável. Use caracteres da área de uso privado Unicode, U + E000..U + F8FF. Remova esses caracteres primeiro, pois eles não devem estar legitimamente na entrada (eles só têm significado específico de aplicativo em algum aplicativo) e, em seguida, use-os como espaços reservados ao substituir.

Jeroen Vannevel
fonte
4
@arshajii Acho que depende da sua definição de "melhor" ... se funcionar e tiver um desempenho aceitável, passe para a próxima tarefa de programação e melhore-a mais tarde durante a refatoração, seria a minha abordagem.
Matt Coubrough
24
Obviamente "lala" é apenas um exemplo. Na produção, você deve usar " zq515sqdqs5d5sq1dqs4d1q5dqqé" & é & € sdq: d:;) àçàçlala ".
Jeroen Vannevel 6/14
81
Não use alguma corda inteligente (estupidamente) escolhida para ser improvável. Use caracteres da área de uso privado Unicode, U + E000..U + F8FF. Remova esses caracteres primeiro, pois eles não devem estar legitimamente na entrada (eles só têm significado específico de aplicativo em algum aplicativo), depois use-os como espaços reservados ao substituir.
David Conrad
22
Na verdade, depois de ler as Perguntas frequentes sobre Unicode , acho que os não caracteres no intervalo U + FDD0..U + FDEF seriam uma opção ainda melhor.
7264 David Conrad
6
@ Taemyr Claro, mas alguém tem que higienizar a entrada, certo? Eu esperaria que uma função de substituição de string funcione em todas as strings, mas essa função quebra para entradas inseguras.
Navin
33

Você pode tentar algo assim, 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());
Era uma vez, havia um bar e um foo.
arshajii
fonte
2
Faz este trabalho se foo, bare storytodos têm valores desconhecidos?
Stephen P
1
@StephenP Eu essencialmente codifiquei as seqüências de substituição "foo"e "bar"como o OP tinha em seu código, mas o mesmo tipo de abordagem funcionaria bem mesmo que esses valores não fossem conhecidos (você teria que usar if/ em else ifvez de um switchdentro do while-ciclo).
Arshajii #
6
Você teria que ter cuidado ao criar a regex. Pattern.quoteviria a calhar, ou \Qe \E.
7268 David Conrad
1
@arshajii - sim, provou isso para mim mesmo como um método "swapThese", tomando a palavra1, a palavra2 e a história como parâmetros. 1
Stephen P
4
Ainda mais limpo seria usar o padrão (foo)|(bar)e depois m.group(1) != nullcomparar, para evitar repetir as palavras correspondentes.
Jörn Horstmann
32

Este não é um problema fácil. E quanto mais parâmetros de substituição de pesquisa você tiver, mais complicado será. Você tem várias opções, espalhadas na paleta de feio e elegante, eficiente e inútil:

  • Use StringUtils.replaceEachno Apache Commons como @AlanHay recomendado. Essa é uma boa opção se você estiver livre para adicionar novas dependências no seu projeto. Você pode ter sorte: a dependência já pode estar incluída no seu projeto

  • Use um espaço reservado temporário como @Jeroen sugeriu e execute a substituição em 2 etapas:

    1. Substitua todos os padrões de pesquisa por uma tag exclusiva que não existe no texto original
    2. Substitua os espaços reservados pela substituição de destino real

    Essa não é uma ótima abordagem, por várias razões: ela precisa garantir que as tags usadas na primeira etapa sejam realmente únicas; ele executa mais operações de substituição de cadeia do que o necessário

  • Crie uma regex a partir de todos os padrões e use o método com MatchereStringBuffer como sugerido por @arshajii . Isso não é terrível, mas também não é ótimo, já que a construção do regex é meio hackish, e envolve o StringBufferque saiu de moda há algum tempo a favor deStringBuilder .

  • Use uma solução recursiva proposta por @mjolka , dividindo a sequência nos padrões correspondentes e recorrendo nos segmentos restantes. Esta é uma solução fina, compacta e bastante elegante. Sua fraqueza são as potencialmente muitas operações de substring e concatenação e os limites de tamanho de pilha que se aplicam a todas as soluções recursivas

  • Divida o texto em palavras e use fluxos Java 8 para executar as substituições de maneira elegante, como sugerido pelo @msandiford , mas é claro que só funciona se você estiver de acordo com a divisão nos limites das palavras, o que o torna inadequado como solução geral

Aqui está minha versão, com base nas idéias emprestadas da implementação do Apache . Não é simples nem elegante, mas funciona e deve ser relativamente eficiente, sem etapas desnecessárias. Em poucas palavras, funciona da seguinte maneira: encontre repetidamente o próximo padrão de pesquisa correspondente no texto e use a StringBuilderpara acumular os segmentos não correspondentes e as substituições.

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

Testes unitários:

@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
fonte
21

Procure a primeira palavra a ser substituída. Se estiver na sequência, recursione a parte da sequência antes da ocorrência e a parte da sequência após a ocorrência.

Caso contrário, continue com a próxima palavra a ser substituída.

Uma implementação ingênua pode se parecer com isso

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

Uso da amostra:

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

Resultado:

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

Uma versão menos ingênua:

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

Infelizmente, o Java Stringnão tem indexOf(String str, int fromIndex, int toIndex)método. Omiti a implementação indexOfaqui, pois não tenho certeza de que esteja correta, mas ela pode ser encontrada no ideone , juntamente com alguns tempos aproximados de várias soluções postadas aqui.

mjolka
fonte
2
Embora o uso de uma biblioteca existente como o apache commons para coisas como essa seja sem dúvida a maneira mais fácil de resolver esse problema bastante comum, você mostrou uma implementação que funciona em partes de palavras, em palavras decididas em tempo de execução e sem substituir substrings por tokens mágicos diferentes (atualmente) respostas com maior número de votos. 1
Buhb
Bonito, mas atinge o chão quando um arquivo de entrada de 100 mb é fornecido.
Christophe De Troyer
12

Uma linha no 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());
Vitalii Fedorenko
fonte
11

Aqui está uma possibilidade de fluxos do Java 8 que pode ser interessante para alguns:

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

Aqui está uma aproximação do mesmo algoritmo no 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
fonte
10
Essa é uma boa sugestão quando as coisas que você deseja substituir são palavras reais separadas por espaços (ou similares), mas isso não funcionaria para substituir substrings de uma palavra.
Simon Forsberg
+1 para fluxos Java8. Pena que isso requer um delimitador.
Navin
6

Se você deseja substituir palavras em uma frase que são separadas por espaço em branco, como mostrado no seu exemplo, você pode usar este algoritmo simples.

  1. Dividir história no espaço em branco
  2. Substitua cada elemento, se for substituí-lo pela barra e vice-versa
  3. Associe a matriz novamente em uma sequência

Se a divisão no espaço não for aceitável, pode-se seguir este algoritmo alternativo. Você precisa usar a string mais longa primeiro. Se as cordas são bobas e bobas, você precisa primeiro usar bobas e depois bobinhas.

  1. Dividir a palavra foo
  2. Substitua barra por foo cada elemento da matriz
  3. Junte-se a essa matriz novamente adicionando barra após cada elemento, exceto o último
fastcodejava
fonte
1
Isto é o que eu estava pensando em sugerir também. Embora adicione uma restrição de que o texto seja palavras cercadas por espaços. :)
Desenvolvedor Marius Žilėnas
@ MariusŽilėnas Adicionei um algoritmo alternativo.
precisa
5

Aqui está uma resposta menos complicada usando o Mapa.

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 o método é chamado

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

O resultado é: awesome is Raffy, Raffy Raffy is awesome awesome

WillingLearner
fonte
1
correr replaced.replaceAll("Raffy", "Barney");atrás disso tornará legen ... espere; Dary !!!
Keale
3

Se você quiser lidar com várias ocorrências das cadeias de pesquisa a serem substituídas, faça isso facilmente dividindo a cadeia em cada termo de pesquisa e substituindo-a. Aqui está um exemplo:

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
fonte
3

Você pode atingir seu objetivo com o seguinte bloco de código:

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

Ele substitui as palavras independentemente da ordem. Você pode estender esse princípio para um método utilitário, como:

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

Que seria consumido como:

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
fonte
3

Isso funciona e é simples:

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

Você usa assim:

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

Nota: isso conta com Strings que não contêm caracteres \ufdd0, que é permanentemente reservado para uso interno pelo Unicode (consulte http://www.unicode.org/faq/private_use.html ):

Não acho que seja necessário, mas se você quiser estar absolutamente seguro, poderá usar:

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
fonte
3

Troca de apenas uma ocorrência

Se houver apenas uma ocorrência de cada uma das seqüências de caracteres trocáveis ​​na entrada, você poderá fazer o seguinte:

Antes de proceder a qualquer substituição, obtenha os índices das ocorrências das palavras. Depois disso, substituímos apenas a palavra encontrada nesses índices, e não todas as ocorrências. Esta solução usa StringBuildere não produz Strings intermediários String.replace().

Uma coisa a ser observada: se as palavras trocáveis ​​tiverem comprimentos diferentes, após o primeiro substituir, o segundo índice poderá mudar (se a 1ª palavra ocorrer antes do 2º) exatamente pela diferença dos 2 comprimentos. Portanto, alinhar o segundo índice garantirá que isso funcione mesmo se estivermos trocando palavras com comprimentos diferentes.

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

Trocar número arbitrário de ocorrências

Analogamente ao caso anterior, coletaremos primeiro os índices (ocorrências) das palavras, mas, neste caso, será apresentada uma lista de números inteiros para cada palavra, não apenas um int. Para isso, usaremos o seguinte método utilitário:

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 isso, substituiremos as palavras pelas outras, diminuindo o índice (que pode exigir a alternância entre as duas palavras trocáveis), para que nem precisemos corrigir os índices após a substituição:

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
fonte
Não tenho certeza de como o java lida com o unicode, mas o equivalente em c # desse código estaria incorreto. O problema é que a substring indexOfcorrespondente pode não ter o mesmo comprimento que a string de pesquisa, graças às idiossincrasias da equivalência de string unicode.
CodesInChaos
@CodesInChaos Ele funciona perfeitamente em Java porque um Java Stringé uma matriz de caracteres e não uma matriz de bytes. Todos os métodos Stringe StringBuilderoperam com caracteres que não estão em bytes, que são "livres de codificação". Portanto, as indexOfcorrespondências têm exatamente o mesmo comprimento (caractere) que as cadeias de pesquisa.
icza
Em C # e java, uma sequência é uma sequência de unidades de código UTF-16. A questão é que existem diferentes seqüências de pontos de código que o unicode considera equivalentes. Por exemplo, äpode ser codificado como um único ponto de código ou como um aseguido por uma combinação ¨. Também existem alguns pontos de código que são ignorados, como junções de largura zero (não). Não importa se a string consiste em bytes, caracteres ou qualquer outra coisa, mas quais regras de comparação indexOfusam. Ele pode usar simplesmente código-unidade por comparação de código-unidade ("Ordinal") ou pode implementar equivalência unicode. Eu não sei qual java escolheu.
CodesInChaos
Por exemplo, "ab\u00ADc".IndexOf("bc")retorna 1em .net correspondendo a cadeia de dois caracteres bca uma cadeia de três caracteres.
CodesInChaos
1
@CodesInChaos Entendo o que você quer dizer agora. Em "ab\u00ADc".indexOf("bc")retornos Java, o -1que significa que "bc"não foi encontrado em "ab\u00ADc". Portanto, ainda é válido que em Java o algoritmo acima funcione, as indexOf()correspondências tenham exatamente o mesmo comprimento (caractere) que as cadeias de pesquisa e os indexOf()relatórios somente correspondam se as charsequences (codepoints) corresponderem.
Icza
2

É fácil escrever um método para fazer isso 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();
}

Teste:

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

Resultado:

Existem três cães e dois periquitos.

Não é imediatamente óbvio, mas uma função como essa ainda pode depender da ordem em que as substituições são especificadas. Considerar:

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

Resultado:

Java é para JavaScript como Ham é para Hamster

Mas inverta as substituições:

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

Resultado:

Java é para JavaScript como Ham é para HamScript

Opa! :)

Portanto, às vezes é útil garantir a correspondência mais longa (como a strtrfunção do PHP faz, por exemplo). Esta versão do método fará isso:

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

Observe que os métodos acima diferenciam maiúsculas de minúsculas. Se você precisar de uma versão que não diferencie maiúsculas de minúsculas, é fácil modificar as opções acima, pois String.regionMatchespode assumir um ignoreCaseparâmetro.

Boann
fonte
2

Se você não deseja nenhuma dependência, basta usar uma matriz que permita apenas uma alteração única. Esta não é a solução mais eficiente, mas deve funcionar.

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

Então, isso funcionaria.

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
fonte
2

Você está executando várias operações de substituição de pesquisa na entrada. Isso produzirá resultados indesejados quando as cadeias de substituição contiverem cadeias de pesquisa. Considere o exemplo foo-> bar, fo-bar, aqui estão os resultados para cada iteração:

  1. Era uma vez um foo e um bar. (entrada)
  2. Era uma vez, havia um bar e um bar. (foo-> bar)
  3. Era uma vez um foo e um foo. (bar-> foo, saída)

Você precisa executar a substituição em uma iteração sem voltar. Uma solução de força bruta é a seguinte:

  1. Pesquise a entrada da posição atual até o final em várias cadeias de pesquisa até encontrar uma correspondência
  2. Substitua a sequência de pesquisa correspondente pela sequência de substituição correspondente
  3. Defina a posição atual para o próximo caractere após a sequência substituída
  4. Repetir

Uma função como String.indexOfAny(String[]) -> int[]{index, whichString}seria útil. Aqui está um exemplo (não o mais eficiente):

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

Alguns testes:

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 no IDEONE
Demo no IDEONE, código alternativo

Salman A
fonte
1

Você sempre pode substituí-lo por uma palavra que, com certeza, não aparecerá em nenhum outro lugar na string e, em seguida, substituir o segundo mais tarde:

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

Observe que isso não funcionará corretamente se "StringYouAreSureWillNeverOccur"ocorrer.

Pokechu22
fonte
5
Use caracteres da área de uso privado Unicode, U + E000..U + F8FF, criando um StringThatCannotEverOccur. Você pode filtrá-los com antecedência, pois eles não deveriam existir na entrada.
7268 David Conrad
Ou U + FDD0..U + FDEF, os "Não Caracteres", reservados para uso interno.
7264 David Conrad
1

Considere usar StringBuilder

Em seguida, armazene o índice onde cada sequência deve começar. Se você usar um caractere de marcador de posição em cada posição, remova-o e insira a sequência de usuários. Você pode mapear a posição final adicionando o comprimento da string à posição inicial.

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
fonte
1

O que eu só posso compartilhar é o meu próprio método.

Você pode usar um temporário String temp = "<?>";ouString.Format();

Este é o meu exemplo de código criado no aplicativo de console via - "Apenas idéia, resposta não exata" .

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

Ou você também pode usar o 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;
        }

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

Leonel Sarmiento
fonte
É muito hacky. O que você fará se ele quiser substituir "_"?
Pier-Alexandre Bouchard
@ Pier-AlexandreBouchard Nos métodos, altero o valor de tempfrom "_"para <?>. Mas, se necessário, o que ele pode fazer é adicionar outro parâmetro ao método que alterará a temperatura. - "é melhor manter as coisas simples, certo?"
Leonel Sarmiento
O que quero dizer é que você não pode garantir o resultado esperado, porque se a temperatura == substituir, seu caminho não funcionará.
Pier-Alexandre Bouchard
1

Aqui está a minha versão, que é baseada em palavras:

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
fonte
1
String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar."

Maneira pouco complicada, mas você precisa fazer mais algumas verificações.

1.converter string em matriz de caracteres

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

2. loop no temp e substitua foopor bare barcom, foopois não há chances de obter uma sequência substituível novamente.

Key_coder
fonte
1

Bem, a resposta mais curta é ...

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
fonte
1

Usando a resposta encontrada aqui, você pode encontrar todas as ocorrências das strings que deseja substituir.

Por exemplo, você executa o código na resposta SO acima. Crie duas tabelas de índices (digamos que bar e foo não apareçam apenas uma vez na sua string) e você poderá trabalhar com essas tabelas para substituí-las na string.

Agora, para substituir em locais de índice específicos, você pode usar:

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

Considerando que posé o índice onde suas strings iniciam (a partir das tabelas de índices citadas acima). Então, digamos que você criou duas tabelas de índices para cada uma. Vamos chamá-los indexBare indexFoo.

Agora, ao substituí-los, você pode simplesmente executar dois loops, um para cada substituição que deseja fazer.

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

Da mesma forma, outro loop para indexFoo.

Isso pode não ser tão eficiente quanto as outras respostas aqui, mas é mais simples de entender do que o Maps ou outras coisas.

Isso sempre forneceria o resultado desejado e várias ocorrências possíveis de cada sequência. Contanto que você armazene o índice de cada ocorrência.

Além disso, esta resposta não precisa de recursão nem dependência externa. No que diz respeito à complexidade, provavelmente é O (n ao quadrado), enquanto n é a soma das ocorrências de ambas as palavras.

John Demetriou
fonte
-1

Eu desenvolvi este código vai resolver o 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;
}

No uso principal change(story,word2,word1).

Onur
fonte
2
Só funcionará se houver exatamente uma aparência de cada sequência de caracteres.
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
fonte