Comment puis-je remplacer deux chaînes de manière à ce que l'une ne remplace pas l'autre?

162

Disons que j'ai le code suivant:

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

Une fois ce code exécuté, la valeur de storysera"Once upon a time, there was a foo and a foo."

Un problème similaire se produit si je les remplace dans l'ordre inverse:

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

La valeur de storysera"Once upon a time, there was a bar and a bar."

Mon objectif est de storydevenir "Once upon a time, there was a bar and a foo."Comment pourrais-je accomplir cela?

Pikamander2
la source
7
+1 il devrait certainement y avoir une fonction swap(String s1, String s2, String s3)qui permute toutes les occurrences de s2with s3, et vice versa.
Ryan
Pouvons-nous supposer qu'il n'y a qu'une seule occurrence de chacun des mots permutables dans l'entrée?
icza
14
Cas d'angle: à quoi s'attend-on en tant que sortie lors de l'échange de "ab" et "ba" dans "ababababababa"?
Hagen von Eitzen
1
Vous avez quelques bonnes solutions ci-dessous, mais comprenez-vous pourquoi votre approche n'a pas fonctionné? Tout d'abord, vous avez "il y avait un foo et un bar". Après le premier remplacement ("foo" -> "bar") vous avez "il y avait un bar et un bar". Vous avez maintenant 2 occurrences de "bar", donc votre deuxième remplacement ne fait pas ce que vous attendez - il n'a aucun moyen de savoir que vous voulez seulement remplacer celui que vous n'avez pas déjà remplacé la dernière fois. @HagenvonEitzen Intéressant. Je m'attendrais à ce qu'une solution de travail corresponde et remplace la première des deux chaînes qu'elle trouve, puis répète à partir de la fin de la section remplacée.
DeveloperInDevelopment
1
La solution de Jeroen est celle que j'utilise fréquemment dans les éditeurs de texte, lorsque je dois renommer en masse. C'est simple, facile à comprendre, ne nécessite aucune bibliothèque spéciale et peut être infaillible avec un minimum de réflexion.
Hot Licks

Réponses:

88

Utilisez la replaceEach()méthode d' Apache Commons StringUtils :

StringUtils.replaceEach(story, new String[]{"foo", "bar"}, new String[]{"bar", "foo"})
Alan Hay
la source
2
une idée de ce que remplace exactement chaque fait en interne?
Marek
3
@Marek il est très probable que la fonction effectue une recherche et indexe chaque élément trouvé, puis les remplace tous une fois qu'ils ont tous été indexés.
16
Vous pouvez trouver la source ici vers la ligne 4684.
Jeroen Vannevel
C'est dommage que ce soit un non-op quand nullest passé, cependant.
droite
87

Vous utilisez une valeur intermédiaire (qui n'est pas encore présente dans la phrase).

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

En réponse à la critique: si vous utilisez une chaîne assez grande et peu commune comme zq515sqdqs5d5sq1dqs4d1q5dqqé "& é5d4sqjshsjddjhodfqsqc, nvùq ^ µù; d & € sdq: d:;) àçàçlala où il est peu probable que je débatte de ce point qu'un utilisateur entrera jamais ceci. La seule façon de savoir si un utilisateur le fera est de connaître le code source et à ce stade, vous êtes avec un tout autre niveau de soucis.

Oui, il existe peut-être des méthodes regex sophistiquées. Je préfère quelque chose de lisible dont je sais qu'il ne m'éclatera pas non plus.

Réitérant également l'excellent conseil donné par @David Conrad dans les commentaires :

N'utilisez pas une chaîne intelligemment (stupidement) choisie pour être improbable. Utilisez des caractères de la zone d'utilisation privée Unicode, U + E000..U + F8FF. Supprimez d'abord ces caractères, car ils ne devraient pas être légitimement dans l'entrée (ils n'ont qu'une signification spécifique à l'application dans certaines applications), puis utilisez-les comme espaces réservés lors du remplacement.

Jeroen Vannevel
la source
4
@arshajii Je suppose que cela dépend de votre définition de "meilleur" ... si cela fonctionne et est suffisamment performant, passer à la tâche de programmation suivante et l'améliorer plus tard lors de la refactorisation serait mon approche.
Matt Coubrough
24
De toute évidence, "lala" n'est qu'un exemple. En production, vous devez utiliser " zq515sqdqs5d5sq1dqs4d1q5dqqé" & é & € sdq: d:;) àçàçlala ".
Jeroen Vannevel
81
N'utilisez pas une chaîne intelligemment (stupidement) choisie pour être improbable. Utilisez des caractères de la zone d'utilisation privée Unicode, U + E000..U + F8FF. Supprimez d'abord ces caractères, car ils ne devraient pas être légitimement dans l'entrée (ils n'ont qu'une signification spécifique à l'application dans certaines applications), puis utilisez-les comme espaces réservés lors du remplacement.
David Conrad
22
En fait, après avoir lu la FAQ Unicode dessus , je pense que les non-caractères de la gamme U + FDD0..U + FDEF seraient un choix encore meilleur.
David Conrad
6
@Taemyr Bien sûr, mais quelqu'un doit nettoyer l'entrée, non? Je m'attendrais à ce qu'une fonction de remplacement de chaîne fonctionne sur toutes les chaînes, mais cette fonction est interrompue pour les entrées non sécurisées.
Navin
33

Vous pouvez essayer quelque chose comme ça, en utilisant Matcher#appendReplacementet 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());
Il était une fois un bar et un foo.
arshajii
la source
2
Cela fonctionne-t-il si foo, baret storytous ont des valeurs inconnues?
Stephen P
1
@StephenP J'ai essentiellement codé en dur les chaînes "foo"et "bar"les chaînes de remplacement que l'OP avait dans son code, mais le même type d'approche fonctionnerait bien même si ces valeurs ne sont pas connues (vous devrez utiliser if/ else ifau lieu d'un switchdans le while-boucle).
arshajii
6
Vous devrez être prudent lors de la création de l'expression régulière. Pattern.quoteserait utile, ou \Qet \E.
David Conrad
1
@arshajii - oui, je l'ai prouvé à moi-même comme une méthode "swapThese" en prenant word1, word2 et story comme paramètres. +1
Stephen P
4
Même plus propre serait d'utiliser le modèle (foo)|(bar)et de vérifier ensuite m.group(1) != null, pour éviter de répéter les mots pour correspondre.
Jörn Horstmann
32

Ce n'est pas un problème facile. Et plus vous avez de paramètres de remplacement de recherche, plus cela devient compliqué. Vous avez plusieurs options, dispersées sur la palette de moche-élégant, efficace-gaspillage:

  • Utilisation StringUtils.replaceEachdepuis Apache Commons comme @AlanHay recommandé. C'est une bonne option si vous êtes libre d'ajouter de nouvelles dépendances dans votre projet. Vous pourriez avoir de la chance: la dépendance peut déjà être incluse dans votre projet

  • Utilisez un espace réservé temporaire comme @Jeroen l'a suggéré et effectuez le remplacement en 2 étapes:

    1. Remplacez tous les modèles de recherche par une balise unique qui n'existe pas dans le texte d'origine
    2. Remplacez les espaces réservés par le véritable remplacement de la cible

    Ce n'est pas une bonne approche, pour plusieurs raisons: il faut s'assurer que les balises utilisées dans la première étape sont vraiment uniques; il effectue plus d'opérations de remplacement de chaîne que nécessaire

  • Construisez une expression régulière à partir de tous les modèles et utilisez la méthode avec MatcheretStringBuffer comme suggéré par @arshajii . Ce n'est pas terrible, mais pas terrible non plus, car la construction de l'expression régulière est un peu hackish, et cela implique StringBufferce qui s'est démodé il y a quelque temps en faveur de StringBuilder.

  • Utilisez une solution récursive proposée par @mjolka , en fractionnant la chaîne au niveau des modèles correspondants et en effectuant une récurrence sur les segments restants. C'est une solution fine, compacte et assez élégante. Sa faiblesse réside dans les opérations potentiellement nombreuses de sous-chaînes et de concaténation, et les limites de taille de pile qui s'appliquent à toutes les solutions récursives

  • Divisez le texte en mots et utilisez les flux Java 8 pour effectuer les remplacements avec élégance, comme @msandiford l'a suggéré, mais bien sûr, cela ne fonctionne que si vous êtes d'accord avec le fractionnement aux limites des mots, ce qui le rend inapproprié comme solution générale

Voici ma version, basée sur des idées empruntées à l'implémentation d' Apache . Ce n'est ni simple ni élégant, mais cela fonctionne et devrait être relativement efficace, sans étapes inutiles. En un mot, cela fonctionne comme ceci: recherchez à plusieurs reprises le modèle de recherche correspondant suivant dans le texte, et utilisez a StringBuilderpour accumuler les segments sans correspondance et les remplacements.

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

Tests unitaires:

@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
la source
21

Recherchez le premier mot à remplacer. Si c'est dans la chaîne, recurse sur la partie de la chaîne avant l'occurrence et sur la partie de la chaîne après l'occurrence.

Sinon, continuez avec le mot suivant à remplacer.

Une implémentation naïve pourrait ressembler à ceci

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

Exemple d'utilisation:

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

Production:

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

Une version moins naïve:

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

Malheureusement, Java Stringn'a pas de indexOf(String str, int fromIndex, int toIndex)méthode. J'ai omis l'implémentation d' indexOfici car je ne suis pas certain qu'elle soit correcte, mais elle peut être trouvée sur ideone , avec quelques timings approximatifs de diverses solutions publiées ici.

mjolka
la source
2
Bien que l'utilisation d'une bibliothèque existante comme apache commons pour des choses comme celle-ci soit sans doute le moyen le plus simple de résoudre ce problème assez courant, vous avez montré une implémentation qui fonctionne sur des parties de mots, sur des mots décidés à l'exécution et sans remplacer les sous-chaînes par des jetons magiques contrairement à (actuellement) des réponses aux votes plus élevés. +1
Buhb
Magnifique, mais touche le sol lorsqu'un fichier d'entrée de 100 Mo est fourni.
Christophe De Troyer
12

One-liner dans 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());
  • Rechercher des expressions régulières ( ?<=, ?=): http://www.regular-expressions.info/lookaround.html
  • Si les mots peuvent contenir des caractères regex spéciaux, utilisez Pattern.quote pour les échapper.
  • J'utilise goyava ImmutableMap pour plus de concision, mais évidemment toute autre carte fera également l'affaire.
Vitalii Fedorenko
la source
11

Voici une possibilité de flux Java 8 qui pourrait être intéressante pour certains:

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

Voici une approximation du même algorithme 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
la source
10
C'est une bonne suggestion lorsque les éléments que vous souhaitez remplacer sont des mots réels séparés par des espaces (ou similaires), mais cela ne fonctionnerait pas pour remplacer les sous-chaînes d'un mot.
Simon Forsberg
+1 pour les flux Java8. Dommage que cela nécessite un délimiteur.
Navin
6

Si vous souhaitez remplacer des mots dans une phrase qui sont séparés par un espace comme indiqué dans votre exemple, vous pouvez utiliser cet algorithme simple.

  1. Histoire partagée sur un espace blanc
  2. Remplacez chaque élément, si foo remplacez-le par bar et vice-varsa
  3. Rejoignez le tableau en une seule chaîne

Si le fractionnement sur l'espace n'est pas acceptable, on peut suivre cet algorithme alternatif. Vous devez d'abord utiliser la chaîne la plus longue. Si les chaînes sont foo et foo, vous devez d'abord utiliser foo, puis foo.

  1. Diviser sur le mot foo
  2. Remplacez bar par foo chaque élément du tableau
  3. Rejoignez ce tableau en ajoutant une barre après chaque élément sauf le dernier
fastcodejava
la source
1
C'est ce que je pensais suggérer aussi. Bien que cela ajoute une restriction selon laquelle le texte est composé de mots entourés d'espaces. :)
Développeur Marius Žilėnas
@ MariusŽilėnas J'ai ajouté un algorithme alternatif.
fastcodejava
5

Voici une réponse moins compliquée avec 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;
     }

Et la méthode s'appelle

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

Le résultat est: génial est Raffy, Raffy Raffy est génial génial

Volonté
la source
1
courir replaced.replaceAll("Raffy", "Barney");après cela le rendra legen ... attendez-le; Dary !!!
Keale
3

Si vous souhaitez pouvoir gérer plusieurs occurrences des chaînes de recherche à remplacer, vous pouvez le faire facilement en fractionnant la chaîne sur chaque terme de recherche, puis en la remplaçant. Voici un exemple:

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
la source
3

Vous pouvez atteindre votre objectif avec le bloc de code suivant:

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

Il remplace les mots quel que soit l'ordre. Vous pouvez étendre ce principe dans une méthode utilitaire, comme:

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

Qui serait consommé comme:

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" }));
Léonard Braga
la source
3

Cela fonctionne et est simple:

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

Vous l'utilisez comme ceci:

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

Remarque: cela compte sur les chaînes ne contenant pas de caractère \ufdd0, qui est un caractère réservé en permanence à un usage interne par Unicode (voir http://www.unicode.org/faq/private_use.html ):

Je ne pense pas que ce soit nécessaire, mais si vous voulez être absolument sûr, vous pouvez utiliser:

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
la source
3

Échange d'une seule occurrence

S'il n'y a qu'une seule occurrence de chacune des chaînes échangeables dans l'entrée, vous pouvez effectuer les opérations suivantes:

Avant de procéder à tout remplacement, récupérez les indices des occurrences des mots. Après cela, nous ne remplaçons que le mot trouvé à ces index, et non toutes les occurrences. Cette solution utilise StringBuilderet ne produit pas des intermédiaires Stringcomme String.replace().

Une chose à noter: si les mots échangeables ont des longueurs différentes, après le premier remplacement, le deuxième index peut changer (si le 1er mot se produit avant le 2ème) exactement avec la différence des 2 longueurs. Donc, aligner le deuxième index garantira que cela fonctionne même si nous échangeons des mots de différentes longueurs.

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

Permutation du nombre arbitraire d'occurrences

Comme dans le cas précédent, nous allons d'abord collecter les index (occurrences) des mots, mais dans ce cas, il s'agira d'une liste d'entiers pour chaque mot, pas seulement un int. Pour cela, nous utiliserons la méthode utilitaire suivante:

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

Et en utilisant cela, nous remplacerons les mots par l'autre en diminuant l'indice (ce qui peut nécessiter d'alterner entre les 2 mots échangeables) afin que nous n'ayons même pas à corriger les index après un remplacement:

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
la source
Je ne sais pas comment java gère l'unicode, mais l'équivalent C # de ce code serait incorrect. Le problème est que la sous-chaîne qui indexOfcorrespond peut ne pas avoir la même longueur que la chaîne de recherche grâce aux idiosyncrasies de l'équivalence de chaîne Unicode.
CodesInChaos
@CodesInChaos Cela fonctionne parfaitement en Java car un Java Stringest un tableau de caractères et non un tableau d'octets. Toutes les méthodes de Stringet StringBuilderfonctionnent sur des caractères non sur des octets, qui sont "sans encodage". Ainsi, les indexOfcorrespondances ont exactement la même longueur (en caractères) que les chaînes de recherche.
icza
En C # et en java, une chaîne est une séquence d'unités de code UTF-16. Le problème est qu'il existe différentes séquences de points de code que l'Unicode considère comme équivalentes. Par exemple, äpeut être codé comme un point de code unique ou comme asuivi d'une combinaison ¨. Il existe également des points de code qui sont ignorés, tels que les (non) joints de largeur zéro. Peu importe si la chaîne se compose d'octets, de caractères ou autre, mais quelles règles de comparaison indexOfutilisent. Il peut utiliser simplement unité de code par comparaison d'unité de code ("Ordinal") ou il peut implémenter l'équivalence unicode. Je ne sais pas lequel java a choisi.
CodesInChaos
Par exemple, "ab\u00ADc".IndexOf("bc")renvoie 1en .net faisant correspondre la chaîne bcde deux caractères à une chaîne de trois caractères.
CodesInChaos
1
@CodesInChaos Je vois ce que tu veux dire maintenant. En Java "ab\u00ADc".indexOf("bc")renvoie -1ce qui signifie "bc"n'a pas été trouvé dans "ab\u00ADc". Il reste donc qu'en Java, l'algorithme ci-dessus fonctionne, les indexOf()correspondances ont exactement la même longueur (en caractères) que les chaînes de recherche et indexOf()ne signale les correspondances que si les séquences de caractères (points de code) correspondent.
icza
2

Il est facile d'écrire une méthode pour faire cela en utilisant 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();
}

Essai:

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

Production:

Il y a trois chiens et deux perruches.

Ce n'est pas immédiatement évident, mais une fonction comme celle-ci peut toujours dépendre de l'ordre dans lequel les remplacements sont spécifiés. Considérer:

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

Production:

Java est à JavaScript comme Ham est à Hamster

Mais inversez les remplacements:

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

Production:

Java est à JavaScript comme Ham est à HamScript

Oups! :)

Par conséquent, il est parfois utile de s'assurer de rechercher la correspondance la plus longue (comme le fait la strtrfonction PHP , par exemple). Cette version de la méthode fera cela:

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

Notez que les méthodes ci-dessus sont sensibles à la casse. Si vous avez besoin d'une version insensible à la casse, il est facile de modifier ce qui précède car String.regionMatchespeut prendre un ignoreCaseparamètre.

Boann
la source
2

Si vous ne voulez pas de dépendances, vous pouvez simplement utiliser un tableau qui n'autorise qu'une seule modification. Ce n'est pas la solution la plus efficace, mais elle devrait fonctionner.

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

Ensuite, cela fonctionnerait.

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
la source
2

Vous effectuez plusieurs opérations de recherche-remplacement sur l'entrée. Cela produira des résultats indésirables lorsque les chaînes de remplacement contiennent des chaînes de recherche. Prenons l'exemple de foo-> bar, bar-foo, voici les résultats pour chaque itération:

  1. Il était une fois un foo et un bar. (contribution)
  2. Il était une fois un bar et un bar. (foo-> bar)
  3. Il était une fois un foo et un foo. (bar-> foo, sortie)

Vous devez effectuer le remplacement en une seule itération sans revenir en arrière. Une solution de force brute est la suivante:

  1. Rechercher l'entrée de la position actuelle à la fin pour plusieurs chaînes de recherche jusqu'à ce qu'une correspondance soit trouvée
  2. Remplacez la chaîne de recherche correspondante par la chaîne de remplacement correspondante
  3. Définit la position actuelle sur le caractère suivant après la chaîne remplacée
  4. Répéter

Une fonction telle que String.indexOfAny(String[]) -> int[]{index, whichString}serait utile. Voici un exemple (pas le plus efficace):

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

Quelques tests:

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

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

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

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

Démo sur IDEONE
Démo sur IDEONE, code alternatif

Salman A
la source
1

Vous pouvez toujours le remplacer par un mot dont vous êtes sûr qu'il n'apparaîtra nulle part ailleurs dans la chaîne, puis effectuer le deuxième remplacement plus tard:

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

Notez que cela ne fonctionnera pas correctement si "StringYouAreSureWillNeverOccur"cela se produit.

Pokechu22
la source
5
Utilisez des caractères de la zone d'utilisation privée Unicode, U + E000..U + F8FF, pour créer un StringThatCannotEverOccur. Vous pouvez les filtrer au préalable car ils ne devraient pas exister dans l'entrée.
David Conrad
Ou U + FDD0..U + FDEF, les "non-caractères", réservés à un usage interne.
David Conrad
1

Envisagez d'utiliser StringBuilder

Ensuite, stockez l'index à l'endroit où chaque chaîne doit commencer. Si vous utilisez un caractère d'espace réservé à chaque position, supprimez-le et insérez la chaîne d'utilisateurs. Vous pouvez ensuite mapper la position de fin en ajoutant la longueur de chaîne à la position de départ.

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
la source
1

Ce que je ne peux partager, c'est ma propre méthode.

Vous pouvez utiliser un temporaire String temp = "<?>";ouString.Format();

Ceci est mon exemple de code créé dans l'application console via - "Idée seulement, pas de réponse exacte" .

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 vous pouvez également utiliser le 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;
        }

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

Leonel Sarmiento
la source
C'est assez hacky. Que ferez-vous s'il veut remplacer "_"?
Pier-Alexandre Bouchard
@ Pier-AlexandreBouchard Dans les méthodes, je change la valeur de tempde "_"en <?>. Mais si nécessaire, ce qu'il peut faire est d'ajouter un autre paramètre à la méthode qui changera la température. - "il vaut mieux rester simple non?"
Leonel Sarmiento
Mon point est que vous ne pouvez pas garantir le résultat attendu car si le temp == remplace, votre chemin ne fonctionnera pas.
Pier-Alexandre Bouchard
1

Voici ma version, qui est basée sur des mots:

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

Petite manière délicate mais vous devez faire quelques vérifications supplémentaires.

1. convertir la chaîne en tableau de caractères

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

2. bouclez sur temp et remplacez foopar baret barpar foocar il n'y a aucune chance d'obtenir à nouveau une chaîne remplaçable.

Key_coder
la source
1

Eh bien, la réponse la plus courte est ...

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
la source
1

En utilisant la réponse trouvée ici, vous pouvez trouver toutes les occurrences des chaînes que vous souhaitez remplacer.

Ainsi, par exemple, vous exécutez le code dans la réponse SO ci-dessus. Créez deux tables d'index (disons que bar et foo n'apparaissent pas une seule fois dans votre chaîne) et vous pouvez travailler avec ces tables pour les remplacer dans votre chaîne.

Maintenant, pour remplacer sur des emplacements d'index spécifiques, vous pouvez utiliser:

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

Alors que posc'est l'index où vos chaînes commencent (à partir des tables d'index que j'ai citées ci-dessus). Disons que vous avez créé deux tables d'index pour chacun. Appelons-les indexBaret indexFoo.

Maintenant, en les remplaçant, vous pouvez simplement exécuter deux boucles, une pour chaque remplacement que vous souhaitez effectuer.

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

De même une autre boucle pour indexFoo.

Cela n'est peut-être pas aussi efficace que d'autres réponses ici, mais c'est plus simple à comprendre que Maps ou d'autres choses.

Cela vous donnerait toujours le résultat souhaité et pour plusieurs occurrences possibles de chaque chaîne. Tant que vous stockez l'index de chaque occurrence.

De plus, cette réponse ne nécessite aucune récursivité ni aucune dépendance externe. En ce qui concerne la complexité, elle est probablement O (n au carré), alors que n est la somme des occurrences des deux mots.

John Demetriou
la source
-1

J'ai développé ce code résoudra le problème:

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

Dans l'utilisation principale change(story,word2,word1).

Onur
la source
2
Cela ne fonctionnera que s'il y a exactement une apparition de chaque chaîne
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
la source