163

Let's say that I have the following code:

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

After this code runs, the value of story will be "Once upon a time, there was a foo and a foo."

A similar issue occurs if I replaced them in the opposite order:

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

The value of story will be "Once upon a time, there was a bar and a bar."

My goal is to turn story into "Once upon a time, there was a bar and a foo." How could I accomplish that?

icza
  • 389,944
  • 63
  • 907
  • 827
Pikamander2
  • 7,332
  • 3
  • 48
  • 69
  • 7
    +1 there definitely should be some function `swap(String s1, String s2, String s3)` that swaps all occurrences of `s2` with `s3`, and vice versa. – Ryan Dougherty Nov 07 '14 at 03:14
  • Can we assume there is only one occurrence of each of the swappable words in the input? – icza Nov 07 '14 at 07:39
  • I would replace the values with an ASCII code that isn't likely to be in the input such as 1 and 2 and then replace those. – Mr. Mascaro Nov 07 '14 at 19:20
  • @Ryan even better: a varargs function in the String class which swaps the elements. – Justin Nov 07 '14 at 20:48
  • @Quincunx not sure if/how that would work with more than 2 args though. – Ryan Dougherty Nov 08 '14 at 02:39
  • @Ryan here's one option: do every other element in the varargs array. Or do first half to second half. – Justin Nov 08 '14 at 04:09
  • 14
    Corner-case: What do we expect as output when swapping "ab" and "ba" in "ababababababa"? – Hagen von Eitzen Nov 08 '14 at 19:15
  • 1
    You have some good solutions below, but do you understand why your approach didn't work? First, you have "there was a foo and a bar". After the first replace ("foo"->"bar") you have "there was a bar and a bar". You now have 2 occurrences of "bar", so your second replace doesn't do what you expect - it has no way of knowing you only want to replace the one you didn't already replace last time around. @HagenvonEitzen Interesting. I would expect a working solution to match and replace the first of either string it finds and then repeat from the end of the replaced section. – DeveloperInDevelopment Nov 09 '14 at 01:44
  • 1
    Jeroen's solution is one I frequently use in text editors, when I need to do bulk renaming. It's simple, easy to understand, requires no special library, and can be foolproof with a modicum of thought. – Hot Licks Nov 09 '14 at 14:40
  • I haven't tried this in Java but how I do it with php is to set 2 arrays with the new and old words. Would look like: story.replace(array1, array2). – TomFirth Nov 11 '14 at 21:57

25 Answers25

89

Use the replaceEach() method from Apache Commons StringUtils:

StringUtils.replaceEach(story, new String[]{"foo", "bar"}, new String[]{"bar", "foo"})
ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152
Alan Hay
  • 22,665
  • 4
  • 56
  • 110
  • 3
    any idea what exactly replaceEach does internally? – Marek Nov 07 '14 at 15:51
  • 3
    @Marek it's very likely that the function does a search and indexes each item found, then replaces all of them once they have all been indexed. –  Nov 07 '14 at 16:21
  • 16
    You can find the source for this [here](http://commons.apache.org/proper/commons-lang/apidocs/src-html/org/apache/commons/lang3/StringUtils.html) around line 4684. – Jeroen Vannevel Nov 07 '14 at 18:09
  • It's a pity that it is a no-op when `null` is passed, though. –  Nov 09 '14 at 11:05
87

You use an intermediate value (which is not yet present in the sentence).

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

As a response to criticism: if you use a large enough uncommon string like zq515sqdqs5d5sq1dqs4d1q5dqqé"&é5d4sqjshsjddjhodfqsqc, nvùq^µù;d&€sdq: d: ;)àçàçlala and use that, it is unlikely to the point where I won't even debate it that a user will ever enter this. The only way to know whether a user will is by knowing the source code and at that point you're with a whole other level of worries.

Yes, maybe there are fancy regex ways. I prefer something readable that I know will not break out on me either.

Also reiterating the excellent advise given by @David Conrad in the comments:

Don't use some string cleverly (stupidly) chosen to be unlikely. Use characters from the Unicode Private Use Area, U+E000..U+F8FF. Remove any such characters first, since they shouldn't legitimately be in the input (they only have application-specific meaning within some application), then use them as placeholders when replacing.

Community
  • 1
  • 1
Jeroen Vannevel
  • 43,651
  • 22
  • 107
  • 170
  • 1
    You call it hacky, I call it very easy, virtually guaranteed to work and a pretty standard approach to switch two values. – Jeroen Vannevel Nov 06 '14 at 23:37
  • 4
    @arshajii I guess that depends on your definition of "better"... if it works and is acceptably performant, move on to the next programming task and improve it later during refactoring would be my approach. – Matt Coubrough Nov 06 '14 at 23:38
  • 4
    I thought about doing that, but in my actual program, `word1` and `word2` will be defined by the user. If the user were to enter `lala` for word1, the same problem would occur. – Pikamander2 Nov 06 '14 at 23:39
  • 1
    @Pikamander2 lala is just a placeholder, use something they can't or won't use, ASCII code 3 for example,... – Wranorn Nov 06 '14 at 23:41
  • 24
    Obviously "lala" is just an example. In production, you should use "*zq515sqdqs5d5sq1dqs4d1q5dqqé"&é&€sdq:d:;)àçàçlala*". – Jeroen Vannevel Nov 06 '14 at 23:42
  • Could always complicate it by exploding the string into an array and walking the array replacing one variable for another and then gluing the pieces back together. But WHY!? – Wranorn Nov 06 '14 at 23:43
  • 1
    Regarding your edit: The source code will be known. Assume that no particular string is safe, and assume that the user is overly curious and will enter whatever it takes to "break" the program. – Pikamander2 Nov 06 '14 at 23:52
  • 1
    I actually do this using `"~@~\u16A3~@~"` with the comment `// Uses the Runic letter "YR", which is unlikely to occur enclosed in tildes and at-signs in any sane usage.` – Stephen P Nov 06 '14 at 23:53
  • 2
    @Pikamander2: that's an important remark then. With that remark added, arshajii's answer is the best one. But for all other cases, this should be just fine. You could still randomly generate the characters though, instead of hardcoding a string. – Jeroen Vannevel Nov 06 '14 at 23:55
  • 81
    Don't use some string cleverly (stupidly) chosen to be unlikely. Use characters from the Unicode Private Use Area, U+E000..U+F8FF. Remove any such characters first, since they shouldn't legitimately be in the input (they only have application-specific meaning within some application), then use them as placeholders when replacing. – David Conrad Nov 07 '14 at 00:00
  • 22
    Actually, after reading the [Unicode FAQ on it](http://www.unicode.org/faq/private_use.html), I think the noncharacters in the range U+FDD0..U+FDEF would be an even better choice. – David Conrad Nov 07 '14 at 00:24
  • 3
    Also, they recommend replacing characters that your application uses internally, and thus could be confused by if they were in the input, with U+FFFD rather than simply deleting them. – David Conrad Nov 07 '14 at 00:30
  • 2
    Why would you do this? :( You now have to document which characters your method does not work on. – Navin Nov 07 '14 at 00:50
  • 1
    @arshajii why is this hacky? – Evorlor Nov 07 '14 at 00:52
  • 1
    @Evorlor Because it silently returns the wrong answer for some inputs (the ones that contain the sentinel string). You can make it break loudly but this is not a real solution. – Navin Nov 07 '14 at 08:22
  • 6
    @Taemyr Sure, but someone has to sanitize the input, right? I would expect that a string-replacement function works on all strings, but this function breaks for unsafe inputs. – Navin Nov 07 '14 at 08:26
  • 3
    The string that is guaranteed not to be in the input can only be generated with the input known: change the case of every character in the string is an obvious one. You end up with a string that is the same length as the input, and different. – Floris Nov 08 '14 at 22:33
  • @Floris Actually it's not as easy as it may seem. For once this even works in ASCII and doesn't rely on any fun unicode specials: What's the lower (or upper case) of "."? ;) – Voo Nov 09 '14 at 00:45
  • 1
    @Voo - actually I would just XOR the number with 64 - that actually gives you the conversion (including for non-alpha characters). See http://www.catonmat.net/blog/ascii-case-conversion-trick/ . But I see your point; I would probably try a little harder than brute force XOR to make sure the different character was not some undesirable control character... – Floris Nov 09 '14 at 02:09
  • Note that one can use any "illegal" characters as the temporary replacement string. Simplest would be to use the null character, alone or in combination with others. Null is only legit in strings in some extremely rare/odd cases, so it would be "safe" even in strings with "odd" Unicode characters. – Hot Licks Nov 09 '14 at 14:38
  • @arshajii Sure it is hacky, but I guess the Apache String Utils do it behind the scenes not differently. – lmaooooo Nov 12 '14 at 20:51
  • Surprised with the down votes on this one. This is a legitimate answer which is simple and does the job perfectly. +1 for you. – Krishnabhadra Nov 17 '14 at 11:53
33

You can try something like this, using Matcher#appendReplacement and 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());
Once upon a time, there was a bar and a foo.
arshajii
  • 127,459
  • 24
  • 238
  • 287
  • I'll admit, It's more elegant than Jeroen's answer, but also over complicated (IMO). – Wranorn Nov 06 '14 at 23:48
  • 2
    Does this work if `foo`, `bar`, and `story` all have unknown values? – Stephen P Nov 06 '14 at 23:55
  • 1
    @StephenP I've essentially hard-coded the `"foo"` and `"bar"` replacement strings as the OP had in his code, but the same type of approach would work fine even if those values are not known (you'd have to use `if`/`else if` instead of a `switch` within the `while`-loop). – arshajii Nov 06 '14 at 23:57
  • 6
    You'd have to be careful in creating the regex. `Pattern.quote` would come in handy, or `\Q` and `\E`. – David Conrad Nov 07 '14 at 00:06
  • 1
    @arshajii - yep, proved it to myself as a "swapThese" method taking word1, word2, and story as parameters. +1 – Stephen P Nov 07 '14 at 00:08
  • 4
    Even cleaner would be to use the pattern `(foo)|(bar)` and then check against `m.group(1) != null`, to avoid repeating the words to match. – Jörn Horstmann Nov 07 '14 at 17:12
  • Seems like the cleanest solution here to me. Also the least conservative memory-wise when long strings are involved. – Vic Nov 11 '14 at 22:15
32

This is not an easy problem. And the more search-replacement parameters you have, the trickier it gets. You have several options, scattered on the palette of ugly-elegant, efficient-wasteful:

  • Use StringUtils.replaceEach from Apache Commons as @AlanHay recommended. This is a good option if you're free to add new dependencies in your project. You might get lucky: the dependency might be included already in your project

  • Use a temporary placeholder as @Jeroen suggested, and perform the replacement in 2 steps:

    1. Replace all search patterns with a unique tag that doesn't exist in the original text
    2. Replace the placeholders with the real target replacement

    This is not a great approach, for several reasons: it needs to ensure that the tags used in the first step are really unique; it performs more string replacement operations than really necessary

  • Build a regex from all the patterns and use the method with Matcher and StringBuffer as suggested by @arshajii. This is not terrible, but not that great either, as building the regex is kind of hackish, and it involves StringBuffer which went out of fashion a while ago in favor of StringBuilder.

  • Use a recursive solution proposed by @mjolka, by splitting the string at the matched patterns, and recursing on the remaining segments. This is a fine solution, compact and quite elegant. Its weakness is the potentially many substring and concatenation operations, and the stack size limits that apply to all recursive solutions

  • Split the text to words and use Java 8 streams to perform the replacements elegantly as @msandiford suggested, but of course that only works if you are ok with splitting at word boundaries, which makes it not suitable as a general solution

Here's my version, based on ideas borrowed from Apache's implementation. It's neither simple nor elegant, but it works, and should be relatively efficient, without unnecessary steps. In a nutshell, it works like this: repeatedly find the next matching search pattern in the text, and use a StringBuilder to accumulate the unmatched segments and the replacements.

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

Unit tests:

@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"})
    );
}
Community
  • 1
  • 1
janos
  • 120,954
  • 29
  • 226
  • 236
21

Search for the first word to be replaced. If it's in the string, recurse on the the part of the string before the occurrence, and on the part of the string after the occurrence.

Otherwise, continue with the next word to be replaced.

A naive implementation might look like this

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

Sample usage:

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

Output:

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

A less-naive version:

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

Unfortunately, Java's String has no indexOf(String str, int fromIndex, int toIndex) method. I've omitted the implementation of indexOf here as I'm not certain it's correct, but it can be found on ideone, along with some rough timings of various solutions posted here.

mjolka
  • 781
  • 1
  • 6
  • 13
  • 2
    Although using an existing library like apache commons for things like this is doubtless the easiest way of solving this fairly common problem, you've shown an implementation that works on parts of words, on words decided in runtime and without replacing substrings with magic tokens unlike (currently) higher voted answers. +1 – Buhb Nov 07 '14 at 14:13
  • Beautiful, but hits the ground when an input file of 100 mb is supplied. – Christophe De Troyer Nov 10 '14 at 20:44
12

One-liner in Java 8:

    story = Pattern
        .compile(String.format("(?<=%1$s)|(?=%1$s)", "foo|bar"))
        .splitAsStream(story)
        .map(w -> ImmutableMap.of("bar", "foo", "foo", "bar").getOrDefault(w, w))
        .collect(Collectors.joining());
Vitalii Fedorenko
  • 110,878
  • 29
  • 149
  • 111
11

Here is a Java 8 streams possibility that might be interesting for some:

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

Here is an approximation of the same algorithm in Java 7:

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

// Map is from untranslated word to translated word
Map<String, String> wordMap = new HashMap<>();
wordMap.put(word1, word2);
wordMap.put(word2, word1);

// Split on word boundaries so we retain whitespace.
StringBuilder translated = new StringBuilder();
for (String w : story.split("\\b"))
{
  String tw = wordMap.get(w);
  translated.append(tw != null ? tw : w);
}

System.out.println(translated);
clstrfsck
  • 14,715
  • 4
  • 44
  • 59
  • 10
    This is a nice suggestion when the stuff you want replaced are actual *words* separated by spaces (or similar), but this would not work for replacing substrings of a word. – Simon Forsberg Nov 07 '14 at 00:52
  • +1 for Java8 streams. Too bad this requires a delimiter. – Navin Nov 07 '14 at 08:29
6

​If you want to replace words in a sentence which are separated by white space as shown in your example you can use this simple algorithm.

  1. Split story on white space
  2. Replace each elements, if foo replace it to bar and vice varsa
  3. Join the array back into one string

​If Splitting on space is not acceptable one can follow this alternate algorithm. ​You need to use the longer string first. If the stringes are foo and fool, you need to use fool first and then foo.

  1. Split on the word foo
  2. Replace bar with foo each element of the array
  3. Join that array back adding bar after each element except the last
fastcodejava
  • 39,895
  • 28
  • 133
  • 186
5

Here's a less complicated answer using 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;
     }

And method is called

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

Output is: awesome is Raffy, Raffy Raffy is awesome awesome

WillingLearner
  • 739
  • 6
  • 10
  • 1
    running `replaced.replaceAll("Raffy", "Barney");` after this will make it legen... wait for it; Dary!!! – Keale Nov 10 '14 at 03:03
3

If you want to be able to handle multiple occurrences of the search strings to be replaced, you can do that easily by splitting the string on each search term, then replacing it. Here is an example:

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
  • 3,316
  • 3
  • 27
  • 49
3

Swapping Only One Occurrence

If there is only one occurrence of each of the swapable strings in the input, you can do the following:

Before proceeding to any replace, get the indices of the occurrences of the words. After that we only replace the word found at these indexes, and not all occurrences. This solution uses StringBuilder and does not produce intermediate Strings like String.replace().

One thing to note: if the swapable words have different lengths, after the first replace the second index might change (if the 1st word occurs before the 2nd) exactly with the difference of the 2 lengths. So aligning the second index will ensure this works even if we're swapping words with different lengths.

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

Swapping Arbitrary Number of Occurrences

Analogous to the previous case we will first collect the indexes (occurrences) of the words, but in this case it will a list of integers for each word, not just one int. For this we will use the following utility method:

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

And using this we will replace the words with the other one by decreasing index (which might require to alternate between the 2 swapable words) so that we won't even have to correct the indices after a replace:

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
  • 389,944
  • 63
  • 907
  • 827
  • I'm not sure how java handles unicode, but the C# equivalent of this code would be incorrect. The issue is that the substring that `indexOf` matches might not have the same length as the searchstring thanks to the idiosyncrasies of unicode string equivalence. – CodesInChaos Nov 07 '14 at 16:44
  • @CodesInChaos It works flawlessly in Java because a Java `String` is a character array and not a byte array. All methods of `String` and `StringBuilder` operate on characters not on bytes, which are "encoding-free". Thus `indexOf` matches have exactly the same (character)length as the search strings. – icza Nov 07 '14 at 17:14
  • In both C# and java a string is a sequence of UTF-16 code units. The issue is that there are different sequences of codepoints which unicode considers equivalent. For example `ä` can be encoded as a single codepoint or as an `a` followed by a combining `¨`. There are also some codepoints which are ignored, such as zero-width (non) joiners. It doesn't matter if the string consists of bytes, chars or whatever, but which comparison rules `indexOf` uses. It might use simply code-unit by code-unit comparison ("Ordinal") or it might implement unicode equivalence. I don't know which one java chose. – CodesInChaos Nov 07 '14 at 17:25
  • For example `"ab\u00ADc".IndexOf("bc")` returns `1` in .net matching the two character string `bc` to a three character string. – CodesInChaos Nov 07 '14 at 17:32
  • 1
    @CodesInChaos I see what you mean now. In Java `"ab\u00ADc".indexOf("bc")` returns `-1` which means `"bc"` was not found in `"ab\u00ADc"`. So it still stands that in Java the above algorithm works, `indexOf()` matches have exactly the same (character)length as the search strings, and `indexOf()` only reports matches if the charsequences (codepoints) match. – icza Nov 07 '14 at 18:39
3

You can accomplish your goal with the following code block:

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

It replaces the words regardless of the order. You can extend this principle into an utility method, like:

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

Which would be consumed as:

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
  • 477
  • 4
  • 9
3

This works and is simple:

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

You use it like this:

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

Note: this counts on Strings not containing character \ufdd0, which is a character permanently reserved for internal use by Unicode (See http://www.unicode.org/faq/private_use.html):

I don't think it's necessary, but If you want to be absolutely safe you can use:

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);
    }
Marcelo Glasberg
  • 29,013
  • 23
  • 109
  • 133
2

It's easy to write a method to do this using 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();
}

Testing:

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

Output:

There are three dogs and two budgies.

It is not immediately obvious, but a function like this can still be dependent on the order in which the replacements are specified. Consider:

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

Output:

Java is to JavaScript as Ham is to Hamster

But reverse the replacements:

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

Output:

Java is to JavaScript as Ham is to HamScript

Oops! :)

Therefore it is sometimes useful to make sure to look for the longest match (as PHP's strtr function does, for example). This version of the method will do that:

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

Note that the above methods are case-sensitive. If you need a case-insensitive version it is easy to modify the above because String.regionMatches can take an ignoreCase parameter.

Boann
  • 48,794
  • 16
  • 117
  • 146
2

If you don't want any dependencies, you could simply use an array which allows a one-time change only. This is not the most efficient solution, but it should work.

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

Then, it whould work.

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
  • 5,135
  • 5
  • 37
  • 72
2

You are performing multiple search-replace operations on the input. This will produce undesired results when the replacement strings contain search strings. Consider the foo->bar, bar-foo example, here are the results for each iteration:

  1. Once upon a time, there was a foo and a bar. (input)
  2. Once upon a time, there was a bar and a bar. (foo->bar)
  3. Once upon a time, there was a foo and a foo. (bar->foo, output)

You need to perform the replacement in one iteration without going back. A brute-force solution is as follows:

  1. Search the input from current position to end for multiple search strings until a match is found
  2. Replace the matched search string with corresponding replace string
  3. Set current position to the next character after the replaced string
  4. Repeat

A function such as String.indexOfAny(String[]) -> int[]{index, whichString} would be useful. Here is an example (not the most efficient one):

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

Some 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

Demo on IDEONE
Demo on IDEONE, alternate code

Salman A
  • 262,204
  • 82
  • 430
  • 521
1

You can always replace it with a word you are sure will appear nowhere else in the string, and then do the second replace later:

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

Note that this will not work right if "StringYouAreSureWillNeverOccur" does occur.

Pokechu22
  • 4,984
  • 9
  • 37
  • 62
  • 5
    Use characters from the Unicode Private Use Area, U+E000..U+F8FF, creating a StringThatCannotEverOccur. You can filter them out beforehand since they shouldn't exist in the input. – David Conrad Nov 07 '14 at 00:03
  • Or U+FDD0..U+FDEF, the "Noncharacters", which are reserved for internal use. – David Conrad Nov 07 '14 at 00:26
1

Using the answer found here you can find all occurrences of the strings you wish to replace with.

So for example you run the code in the above SO answer. Create two tables of indexes (let's say bar and foo do not appear only once in your string) and you can work with those tables on replacing them in your string.

Now for replacing on specific index locations you can use:

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

Whereas pos is the index where your strings start (from the index tables I quoted above). So let's say you created two tables of indexes for each one. Let's call them indexBar and indexFoo.

Now in replacing them you could simply run two loops, one for each replacements you wish to make.

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

Similarly another loop for indexFoo.

This may not be as efficient as other answers here but it's simpler to understand than Maps or other stuff.

This would always give you the result you wish and for multiple possible occurrences of each string. As long as you store the index of each occurrence.

Also this answer needs no recursion nor any external dependencies. As far as complexity goes it propably is O(n squared), whereas n is the sum of occurences of both words.

Community
  • 1
  • 1
John Demetriou
  • 4,093
  • 6
  • 52
  • 88
1

Consider using StringBuilder

Then store the index where each string should start. If you use a place holder character at each position, then remove it, and insert the users string. You can then map the end position by adding the string length to the start position.

String firstString = "???";
String secondString  = "???"

StringBuilder story = new StringBuilder("One upon a time, there was a " 
    + firstString
    + " and a "
    + secondString);

int  firstWord = 30;
int  secondWord = firstWord + firstString.length() + 7;

story.replace(firstWord, firstWord + firstString.length(), userStringOne);
story.replace(secondWord, secondWord + secondString.length(), userStringTwo);

firstString = userStringOne;
secondString = userStringTwo;

return story;
1

What I can only share is my own method.

You can use a temporary String temp = "<?>"; or String.Format();

This is my example code created in console application via -"Idea Only, Not Exact Answer".

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

Or you can also use the 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;
        }

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

Onel Sarmiento
  • 1,608
  • 3
  • 20
  • 46
1

Here's my version, which is word-based:

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

Little tricky way but you need to do some more checks.

1.convert string to character array

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

2.loop on temp and replace foo with bar and bar with foo as there are no chances of getting replaceable string again.

Key_coder
  • 534
  • 3
  • 27
1

Well, the shorter answer is...

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
  • 134
  • 5
-1

I developed this code will solve problem:

public static String change(String s,String s1, String s2) {
   int length = s.length();
   int x1 = s1.length();
   int x2 = s2.length();
   int x12 = s.indexOf(s1);
   int x22 = s.indexOf(s2);
   String s3=s.substring(0, x12);
   String s4 =s.substring(x12+3, x22);
   s=s3+s2+s4+s1;
   return s;
}

In the main use change(story,word2,word1).

Pokechu22
  • 4,984
  • 9
  • 37
  • 62
Onur
  • 42
  • 3
-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
  • 13,014
  • 20
  • 92
  • 137