¿Cómo puedo reemplazar dos cadenas de manera que una no termine reemplazando a la otra?

162

Digamos que tengo el siguiente 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);

Después de que se ejecute este código, el valor de storyserá"Once upon a time, there was a foo and a foo."

Un problema similar ocurre si los reemplacé en el orden opuesto:

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

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

Mi objetivo es convertirme storyen "Once upon a time, there was a bar and a foo."¿Cómo podría lograr eso?

Pikamander2
fuente
77
+1 definitivamente debería haber alguna función swap(String s1, String s2, String s3)que intercambie todas las apariciones de s2con s3, y viceversa.
Ryan
¿Podemos suponer que solo hay una aparición de cada una de las palabras intercambiables en la entrada?
icza
14
Esquina: ¿Qué esperamos como salida al intercambiar "ab" y "ba" en "ababababababa"?
Hagen von Eitzen
1
Tiene algunas buenas soluciones a continuación, pero ¿comprende por qué su enfoque no funcionó? Primero, tienes "había un tonto y un bar". Después del primer reemplazo ("foo" -> "bar") tienes "había un bar y un bar". Ahora tiene 2 apariciones de "barra", por lo que su segundo reemplazo no hace lo que espera: no tiene forma de saber que solo desea reemplazar el que ya no reemplazó la última vez. @HagenvonEitzen Interesante. Esperaría que una solución de trabajo coincida y reemplace la primera de las cadenas que encuentre y luego repita desde el final de la sección reemplazada.
DeveloperInDevelopment
1
La solución de Jeroen es una que uso con frecuencia en editores de texto, cuando necesito hacer un cambio de nombre masivo. Es simple, fácil de entender, no requiere una biblioteca especial y puede ser infalible con un poco de pensamiento.
Hot Licks

Respuestas:

88

Use el replaceEach()método de Apache Commons StringUtils :

StringUtils.replaceEach(story, new String[]{"foo", "bar"}, new String[]{"bar", "foo"})
Alan Hay
fuente
2
¿Alguna idea de lo que reemplaza exactamente cada uno internamente?
Marek
3
@Marek es muy probable que la función realice una búsqueda e indexe cada elemento encontrado, luego los reemplace a todos una vez que hayan sido indexados.
16
Puede encontrar la fuente de esto aquí en la línea 4684.
Jeroen Vannevel
Sin embargo, es una pena que sea un no-op cuando nullse pasa.
derecha el
87

Utiliza un valor intermedio (que aún no está presente en la oración).

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

Como respuesta a las críticas: si usa una cadena poco común lo suficientemente grande como zq515sqdqs5d5sq1dqs4d1q5dqqé "& é5d4sqjshsjddjhodfqsqc, nvùq ^ µù; d & € sdq: d:;) àçàçlala y usará ese punto, aunque sea poco probable que lo discuta. que un usuario alguna vez ingresará esto. La única manera de saber si un usuario lo hará es conociendo el código fuente y en ese punto usted está con un nivel completamente diferente de preocupaciones.

Sí, tal vez hay formas elegantes de expresiones regulares. Prefiero algo legible que sé que tampoco me explotará.

También reiterando el excelente consejo dado por @David Conrad en los comentarios :

No use una cuerda inteligentemente (estúpidamente) elegida para ser improbable. Utilice caracteres del Área de uso privado de Unicode, U + E000..U + F8FF. Elimine primero dichos caracteres, ya que no deberían estar legítimamente en la entrada (solo tienen un significado específico de la aplicación dentro de alguna aplicación), luego úselos como marcadores de posición al reemplazar.

Jeroen Vannevel
fuente
44
@arshajii Supongo que eso depende de tu definición de "mejor" ... si funciona y tiene un rendimiento aceptable, pasar a la siguiente tarea de programación y mejorarla más tarde durante la refactorización sería mi enfoque.
Matt Coubrough
24
Obviamente "lala" es solo un ejemplo. En producción, debe usar " zq515sqdqs5d5sq1dqs4d1q5dqqé" & é & € sdq: d:;) àçàçlala ".
Jeroen Vannevel
81
No use una cuerda inteligentemente (estúpidamente) elegida para ser improbable. Utilice caracteres del Área de uso privado de Unicode, U + E000..U + F8FF. Elimine primero dichos caracteres, ya que no deberían estar legítimamente en la entrada (solo tienen un significado específico de la aplicación dentro de alguna aplicación), luego úselos como marcadores de posición al reemplazar.
David Conrad
22
En realidad, después de leer las preguntas frecuentes de Unicode sobre él , creo que los no caracteres en el rango U + FDD0..U + FDEF serían una opción aún mejor.
David Conrad
66
@Taemyr Claro, pero alguien tiene que desinfectar la entrada, ¿verdad? Esperaría que una función de reemplazo de cadena funcione en todas las cadenas, pero esta función se rompe para entradas inseguras.
Navin
33

Puedes probar algo como esto, usando Matcher#appendReplacementy 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());
Érase una vez, había un bar y un foo.
arshajii
fuente
2
¿Funciona esto si foo, bary storytodos tienen valores desconocidos?
Stephen P
1
@StephenP He codificado esencialmente el "foo" y "bar"sustitución de cadenas como el PO tenía en su código, pero el mismo tipo de enfoque podría funcionar bien incluso si no se conocen los valores (que tendría que usar if/ else ifen lugar de una switchdentro de la while-lazo).
arshajii
66
Tendría que tener cuidado al crear la expresión regular. Pattern.quotesería útil, o \Qy \E.
David Conrad
1
@arshajii - sí, me lo demostró como un método "swapThese" tomando word1, word2 e story como parámetros. +1
Stephen P
44
Incluso más limpio sería usar el patrón (foo)|(bar)y luego verificarm.group(1) != null , para evitar repetir las palabras para que coincidan.
Jörn Horstmann
32

Este no es un problema fácil. Y cuantos más parámetros de reemplazo de búsqueda tenga, más complicado será. Tienes varias opciones, dispersas en la paleta de feo-elegante, eficiente-derrochador:

  • Uso StringUtils.replaceEachde Apache Commons como @AlanHay recomendado. Esta es una buena opción si es libre de agregar nuevas dependencias en su proyecto. Puede tener suerte: la dependencia puede estar incluida ya en su proyecto

  • Use un marcador de posición temporal como sugirió @Jeroen y realice el reemplazo en 2 pasos:

    1. Reemplace todos los patrones de búsqueda con una etiqueta única que no existe en el texto original
    2. Reemplace los marcadores de posición con el reemplazo de destino real

    Este no es un gran enfoque, por varias razones: debe asegurarse de que las etiquetas utilizadas en el primer paso sean realmente únicas; realiza más operaciones de reemplazo de cadenas de las realmente necesarias

  • Cree una expresión regular a partir de todos los patrones y use el método con MatcheryStringBuffer como lo sugiere @arshajii . Esto no es terrible, pero tampoco tan bueno, ya que construir la expresión regular es algo hack, e implica lo StringBufferque pasó de moda hace un tiempo a favor StringBuilder.

  • Use una solución recursiva propuesta por @mjolka , dividiendo la cadena en los patrones coincidentes y recurriendo en los segmentos restantes. Esta es una buena solución, compacta y bastante elegante. Su debilidad es el potencial de muchas operaciones de subcadena y concatenación, y los límites de tamaño de pila que se aplican a todas las soluciones recursivas.

  • Divida el texto en palabras y use secuencias de Java 8 para realizar los reemplazos con elegancia como sugirió @msandiford , pero por supuesto eso solo funciona si está de acuerdo con la división en los límites de las palabras, lo que lo hace no adecuado como una solución general

Aquí está mi versión, basada en ideas tomadas de la implementación de Apache . No es simple ni elegante, pero funciona, y debería ser relativamente eficiente, sin pasos innecesarios. En pocas palabras, funciona así: encuentra repetidamente el siguiente patrón de búsqueda coincidente en el texto y usa unStringBuilder para acumular los segmentos no y los reemplazos.

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

Pruebas unitarias:

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

Busque la primera palabra que se reemplazará. Si está en la cadena, repita en la parte de la cadena antes de la ocurrencia y en la parte de la cadena después de la ocurrencia.

De lo contrario, continúe con la siguiente palabra para ser reemplazado.

Una implementación ingenua podría verse así

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 de la muestra:

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

Salida:

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

Una versión menos 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);
}

Desafortunadamente, Java Stringno tiene indexOf(String str, int fromIndex, int toIndex)método. He omitido la implementación de indexOfaquí, ya que no estoy seguro de que sea correcto, pero se puede encontrar en ideone , junto con algunos tiempos difíciles de varias soluciones publicadas aquí.

mjolka
fuente
2
Aunque el uso de una biblioteca existente como apache commons para cosas como esta es sin duda la forma más fácil de resolver este problema bastante común, ha mostrado una implementación que funciona en partes de palabras, en palabras decididas en tiempo de ejecución y sin reemplazar subcadenas con fichas mágicas a diferencia de (actualmente) respuestas más votadas. +1
Buhb
Hermoso, pero toca el suelo cuando se suministra un archivo de entrada de 100 mb.
Christophe De Troyer
12

One-liner en 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
fuente
11

Aquí hay una posibilidad de secuencias de Java 8 que puede ser interesante para algunos:

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

Aquí hay una aproximación del mismo algoritmo en 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
fuente
10
Esta es una buena sugerencia cuando las cosas que desea reemplazar son palabras reales separadas por espacios (o similares), pero esto no funcionaría para reemplazar las subcadenas de una palabra.
Simon Forsberg el
+1 para las secuencias Java8. Lástima que esto requiera un delimitador.
Navin
6

Si desea reemplazar palabras en una oración que están separadas por espacios en blanco como se muestra en su ejemplo, puede usar este algoritmo simple.

  1. Historia dividida en espacio en blanco
  2. Reemplace cada elemento, si lo reemplaza por barra y viceversa
  3. Unir la matriz de nuevo en una cadena

Si la división en el espacio no es aceptable, se puede seguir este algoritmo alternativo. Primero debes usar la cadena más larga. Si los hilos son tontos y tontos, primero debes usar tontos y luego tontos.

  1. Split en la palabra foo
  2. Reemplace la barra con foo cada elemento de la matriz
  3. Únase a esa matriz y agregue la barra después de cada elemento, excepto el último
fastcodejava
fuente
1
Esto es lo que estaba pensando sugerir también. Aunque agrega una restricción de que el texto son palabras rodeadas de espacios. :)
Desarrollador Marius Žilėnas
@ MariusŽilėnas He agregado un algoritmo alternativo.
fastcodejava
5

Aquí hay una respuesta menos complicada 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;
     }

Y el método se llama

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

La salida es: impresionante es Raffy, Raffy Raffy es increíble increíble

WillingLearner
fuente
1
correr replaced.replaceAll("Raffy", "Barney");después de esto lo hará legendario ... espera; Dary !!!
Keale
3

Si desea poder manejar múltiples ocurrencias de las cadenas de búsqueda que se reemplazarán, puede hacerlo fácilmente dividiendo la cadena en cada término de búsqueda y luego reemplazándola. Aquí hay un ejemplo:

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

Puede lograr su objetivo con el siguiente bloque 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);

Reemplaza las palabras independientemente del orden. Puede extender este principio a un método de utilidad, 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 se consumiría 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
fuente
3

Esto funciona y es simple:

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

Lo usas así:

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

Nota: esto cuenta con cadenas que no contienen caracteres \ufdd0, que es un carácter reservado permanentemente para uso interno de Unicode (consulte http://www.unicode.org/faq/private_use.html ):

No creo que sea necesario, pero si quieres estar absolutamente seguro puedes 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
fuente
3

Intercambiando solo una ocurrencia

Si solo hay una aparición de cada una de las cadenas intercambiables en la entrada, puede hacer lo siguiente:

Antes de proceder a cualquier reemplazo, obtenga los índices de las ocurrencias de las palabras. Después de eso solo reemplazamos la palabra encontrada en estos índices, y no todas las ocurrencias. Esta solución utiliza StringBuildery no produce productos intermedios Stringsimilares String.replace().

Una cosa a tener en cuenta: si las palabras intercambiables tienen diferentes longitudes, después del primer reemplazo, el segundo índice puede cambiar (si la primera palabra aparece antes que la segunda) exactamente con la diferencia de las 2 longitudes. Al alinear el segundo índice se garantizará que esto funcione incluso si estamos intercambiando palabras con diferentes longitudes.

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

Intercambio de número arbitrario de ocurrencias

De manera análoga al caso anterior, primero recopilaremos los índices (ocurrencias) de las palabras, pero en este caso será una lista de enteros para cada palabra, no solo una int. Para esto utilizaremos el siguiente método de utilidad:

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

Y usando esto, reemplazaremos las palabras con la otra disminuyendo el índice (lo que puede requerir alternar entre las 2 palabras intercambiables) para que ni siquiera tengamos que corregir los índices después de un reemplazo:

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
fuente
No estoy seguro de cómo Java maneja Unicode, pero el equivalente de C # de este código sería incorrecto. El problema es que la subcadena que indexOfcoincide puede no tener la misma longitud que la cadena de búsqueda gracias a las idiosincrasias de equivalencia de cadena unicode.
CodesInChaos
@CodesInChaos Funciona perfectamente en Java porque un Java Stringes una matriz de caracteres y no una matriz de bytes. Todos los métodos de Stringy StringBuilderfuncionan en caracteres no en bytes, que están "libres de codificación". Por lo tanto, las indexOfcoincidencias tienen exactamente la misma longitud (carácter) que las cadenas de búsqueda.
icza
Tanto en C # como en java, una cadena es una secuencia de unidades de código UTF-16. El problema es que hay diferentes secuencias de puntos de código que Unicode considera equivalentes. Por ejemplo, äpuede codificarse como un único punto de código o como aseguido de una combinación ¨. También hay algunos puntos de código que se ignoran, como las uniones de ancho cero (no). No importa si la cadena consiste en bytes, caracteres o lo que sea, sino qué reglas de comparación indexOfutiliza. Podría usar simplemente la comparación de unidad de código por unidad de código ("Ordinal") o podría implementar equivalencia unicode. No sé cuál eligió Java.
CodesInChaos
Por ejemplo, "ab\u00ADc".IndexOf("bc")regresa 1en .net haciendo coincidir la cadena de dos caracteres bccon una cadena de tres caracteres.
CodesInChaos
1
@CodesInChaos Ahora veo lo que quieres decir. En Java "ab\u00ADc".indexOf("bc")devuelve lo -1que significa que "bc"no se encontró en "ab\u00ADc". Por lo tanto, sigue en pie que en Java el algoritmo anterior funciona, las indexOf()coincidencias tienen exactamente la misma longitud (caracteres) que las cadenas de búsqueda, y indexOf()solo informa coincidencias si coinciden las coincidencias (puntos de código).
icza
2

Es fácil escribir un método para hacer esto 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();
}

Pruebas:

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

Salida:

Hay tres perros y dos periquitos.

No es inmediatamente obvio, pero una función como esta todavía puede depender del orden en que se especifican los reemplazos. Considerar:

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

Salida:

Java es a JavaScript como Ham es a Hamster

Pero invierta los reemplazos:

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

Salida:

Java es a JavaScript como Ham es a HamScript

¡Uy! :)

Por lo tanto, a veces es útil asegurarse de buscar la coincidencia más larga (como lo hace la strtrfunción de PHP , por ejemplo). Esta versión del método hará eso:

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

Tenga en cuenta que los métodos anteriores distinguen entre mayúsculas y minúsculas. Si necesita una versión que no distinga entre mayúsculas y minúsculas, es fácil modificar lo anterior porque String.regionMatchespuede tomar un ignoreCaseparámetro.

Boann
fuente
2

Si no desea ninguna dependencia, simplemente puede usar una matriz que permita un cambio único. Esta no es la solución más eficiente, pero debería 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();
}

Entonces, debería funcionar.

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

Está realizando múltiples operaciones de búsqueda de reemplazo en la entrada. Esto producirá resultados no deseados cuando las cadenas de reemplazo contengan cadenas de búsqueda. Considere el ejemplo foo-> bar, bar-foo, aquí están los resultados para cada iteración:

  1. Había una vez un foo y un bar. (entrada)
  2. Érase una vez, había un bar y un bar. (foo-> bar)
  3. Había una vez un foo y un foo. (bar-> foo, salida)

Debe realizar el reemplazo en una iteración sin volver atrás. Una solución de fuerza bruta es la siguiente:

  1. Busque en la entrada desde la posición actual hasta el final varias cadenas de búsqueda hasta encontrar una coincidencia
  2. Reemplace la cadena de búsqueda coincidente con la cadena de reemplazo correspondiente
  3. Establecer la posición actual al siguiente carácter después de la cadena reemplazada
  4. Repetir

Una función como String.indexOfAny(String[]) -> int[]{index, whichString}sería útil. Aquí hay un ejemplo (no el más 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;
}

Algunas pruebas:

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

Salman A
fuente
1

Siempre puede reemplazarlo con una palabra que está seguro que no aparecerá en ningún otro lugar de la cadena, y luego haga el segundo reemplazo más 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");

Tenga en cuenta que esto no funcionará correctamente si "StringYouAreSureWillNeverOccur"ocurre.

Pokechu22
fuente
55
Use caracteres del Área de uso privado de Unicode, U + E000..U + F8FF, creando un StringThatCannotEverOccur. Puede filtrarlos de antemano ya que no deberían existir en la entrada.
David Conrad
O U + FDD0..U + FDEF, los "No caracteres", que están reservados para uso interno.
David Conrad
1

Considere usar StringBuilder

Luego almacene el índice donde debe comenzar cada cadena. Si usa un carácter de marcador de posición en cada posición, elimínelo e inserte la cadena de usuarios. Luego puede asignar la posición final agregando la longitud de la cadena a la posición 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
fuente
1

Lo que solo puedo compartir es mi propio método.

Puedes usar un temporal String temp = "<?>";oString.Format();

Este es mi código de ejemplo creado en la aplicación de consola a través de - "Solo idea, no respuesta exacta" .

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

O también puedes usar el 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;
        }

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

Leonel Sarmiento
fuente
Es bastante hacky. ¿Qué harás si él quiere reemplazar "_"?
Pier-Alexandre Bouchard
@ Pier-AlexandreBouchard En los métodos, cambio el valor de tempde "_"a <?>. Pero si es necesario, lo que puede hacer es agregar otro parámetro al método que cambiará la temperatura. - "es mejor mantenerlo simple ¿verdad?"
Leonel Sarmiento
Mi punto es que no puede garantizar el resultado esperado porque si la temp == reemplaza, su camino no funcionará.
Pier-Alexandre Bouchard
1

Aquí está mi versión, que está basada en palabras:

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

Un poco complicado, pero debes hacer algunas verificaciones más.

1.convertir cadena a matriz de caracteres

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

2.seleccione la temperatura y reemplace foocon bary barcon fooya que no hay posibilidades de obtener una cuerda reemplazable nuevamente.

Key_coder
fuente
1

Bueno, la respuesta más corta es ...

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

Usando la respuesta que se encuentra aquí , puede encontrar todas las ocurrencias de las cadenas con las que desea reemplazar.

Entonces, por ejemplo, ejecuta el código en la respuesta SO anterior. Cree dos tablas de índices (digamos que bar y foo no aparecen solo una vez en su cadena) y puede trabajar con esas tablas para reemplazarlas en su cadena.

Ahora para reemplazar en ubicaciones de índice específicas puede usar:

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

Mientras que poses el índice donde comienzan sus cadenas (de las tablas de índice que cité anteriormente). Digamos que creó dos tablas de índices para cada una. Vamos a llamarlos indexBary indexFoo.

Ahora, al reemplazarlos, simplemente puede ejecutar dos bucles, uno para cada reemplazo que desee realizar.

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

Del mismo modo otro bucle para indexFoo.

Esto puede no ser tan eficiente como otras respuestas aquí, pero es más fácil de entender que Maps u otras cosas.

Esto siempre le daría el resultado que desea y para múltiples posibles ocurrencias de cada cadena. Siempre que almacene el índice de cada aparición.

Además, esta respuesta no necesita recurrencia ni dependencias externas. En lo que respecta a la complejidad, es probable que sea O (n al cuadrado), mientras que n es la suma de las ocurrencias de ambas palabras.

John Demetriou
fuente
-1

Desarrollé este código resolverá el 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;
}

En el uso principal change(story,word2,word1).

Onur
fuente
2
Solo funcionará si hay exactamente una apariencia de cada cadena
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
fuente