Как я могу заменить две строки таким образом, чтобы одна не заменяла другую?


162

Допустим, у меня есть следующий код:

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

После запуска этого кода значение storyбудет"Once upon a time, there was a foo and a foo."

Аналогичная проблема возникает, если я заменил их в обратном порядке:

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

Значение storyбудет"Once upon a time, there was a bar and a bar."

Моя цель - превратиться storyв "Once upon a time, there was a bar and a foo."Как я могу это сделать?


7
+1 определенно должна быть какая-то функция, swap(String s1, String s2, String s3)которая меняет все вхождения s2с s3, и наоборот.
Райан

Можем ли мы предположить, что есть только одно вхождение каждого из заменяемых слов во входных данных?
icza

14
Угловой пример: что мы ожидаем в качестве выхода при замене «ab» и «ba» на «ababababababa»?
Хаген фон Айцен

1
У вас есть несколько хороших решений ниже, но понимаете ли вы, почему ваш подход не сработал? Во-первых, у вас есть "был фу и бар". После первой замены («foo» -> «bar») у вас «появился бар и бар». Теперь у вас есть 2 вхождения «bar», поэтому ваша вторая замена не делает то, что вы ожидаете - она ​​не может знать, что вы хотите заменить только ту, которую вы не заменяли в прошлый раз. @HagenvonEitzen Интересно. Я ожидал бы, что рабочее решение совпадет и заменит первую из любой найденной строки, а затем повторится с конца замененного раздела.
DeveloperInDevelopment

1
Решение Jeroen - это то, которое я часто использую в текстовых редакторах, когда мне нужно сделать массовое переименование. Это просто, легко понять, не требует специальной библиотеки и может быть надежным с минимальным количеством мыслей.
Hot Licks

Ответы:


88

Используйте replaceEach()метод из Apache Commons StringUtils :

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

2
любая идея, что именно replaceEach делает внутренне?
Марек

3
@Marek очень вероятно, что функция выполняет поиск и индексирует каждый найденный элемент, а затем заменяет их все после того, как все они были проиндексированы.

16
Вы можете найти источник этого здесь в строке 4684.
Йерун Ванневел

Жаль только, что он не работает, когда nullего принимают.
Право

87

Вы используете промежуточное значение (которого еще нет в предложении).

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

В ответ на критику: если вы используете достаточно большую необычную строку, такую ​​как zq515sqdqs5d5sq1dqs4d1q5dqqé "& é5d4sqjshsjddjhodfqsqc, то nvùq ^ µù; d & € sdq: d:;) даже не спорят, даже если и не склонны даже использовать это, даже если и не будете спорить, даже если вы даже не будете это использовать что пользователь когда-либо введет это. Единственный способ узнать, будет ли это делать пользователь, зная исходный код, и в этот момент у вас будет совсем другой уровень беспокойства.

Да, может быть, есть причудливые способы регулярных выражений. Я предпочитаю что-то читаемое, что я знаю, не произойдет и на меня.

Также повторяя превосходный совет, данный @David Conrad в комментариях :

Не используйте какую-то строку, умно (тупо) выбранную, чтобы быть маловероятной. Используйте символы из области личного использования Unicode, U + E000..U + F8FF. Сначала удалите все такие символы, поскольку они не должны быть законным образом во входных данных (они имеют смысл только для конкретного приложения в каком-либо приложении), а затем используйте их в качестве заполнителей при замене.


4
@arshajii Я думаю, это зависит от вашего определения «лучше» ... если оно работает и приемлемо эффективно, перейдите к следующей задаче программирования и улучшите ее позже во время рефакторинга.
Мэтт Кубри

24
Очевидно, "ляла" - это всего лишь пример. В процессе работы вы должны использовать " zq515sqdqs5d5sq1dqs4d1q5dqqé" & é & € sdq: d:;) àçàçlala ".
Йероен Ванневел

81
Не используйте какую-то строку, умно (тупо) выбранную, чтобы быть маловероятной. Используйте символы из области личного использования Unicode, U + E000..U + F8FF. Сначала удалите все такие символы, поскольку они не должны быть законным образом во входных данных (они имеют смысл только для конкретного приложения в каком-либо приложении), а затем используйте их в качестве заполнителей при замене.
Дэвид Конрад

22
На самом деле, после прочтения FAQ по Unicode , я думаю, что нехарактеры в диапазоне U + FDD0..U + FDEF будут еще лучшим выбором.
Дэвид Конрад

6
@Taemyr Конечно, но кто-то должен очистить вход, верно? Я ожидаю, что функция замены строк работает на всех строках, но эта функция прерывается для небезопасных входных данных.
Навин

33

Вы можете попробовать что-то вроде этого, используя Matcher#appendReplacementи 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());
Когда-то был бар и фу.

2
Это работает, если foo, barи storyвсе имеют неизвестные значения?
Стивен П,

1
@StephenP Я по существу жестко закодировал "foo"и "bar"строку замены как OP имел в своем коде, но тот же тип подхода будет работать хорошо , даже если эти значения не известны (вы должны использовать if/ else ifвместо switchвнутри while-loop).
Аршаджи

6
Вы должны быть осторожны при создании регулярного выражения. Pattern.quoteпригодится, или \Qи \E.
Дэвид Конрад

1
@arshajii - да, я доказал это себе как метод "swapThese", принимающий в качестве параметров word1, word2 и story. +1
Стивен П

4
Еще чище будет использовать шаблон, (foo)|(bar)а затем проверить m.group(1) != null, чтобы избежать повторения слов, чтобы соответствовать.
Йорн Хорстманн

32

Это не простая проблема. И чем больше у вас параметров поиска-замены, тем сложнее становится. У вас есть несколько вариантов, разбросанных по палитре некрасиво-элегантно, эффективно-расточительно:

  • Используйте StringUtils.replaceEachот Apache Commons, как рекомендуется @AlanHay . Это хороший вариант, если вы можете добавлять новые зависимости в ваш проект. Возможно, вам повезет: зависимость может быть включена уже в ваш проект

  • Используйте временный заполнитель, как предложено @Jeroen , и выполните замену в 2 этапа:

    1. Замените все шаблоны поиска уникальным тегом, которого нет в исходном тексте.
    2. Замените заполнители реальной заменой цели

    Это не очень хороший подход по нескольким причинам: необходимо убедиться, что теги, используемые на первом этапе, действительно уникальны; он выполняет больше операций замены строк, чем необходимо

  • Создайте регулярное выражение из всех шаблонов и используйте метод с MatcherиStringBuffer как предложено @arshajii . Это не страшно, но и не так уж и здорово, так как построение регулярного выражения является своего рода хакерским, и оно включает StringBufferв себя то, что давно вышло из моды в пользу StringBuilder.

  • Используйте рекурсивное решение, предложенное @mjolka , разделив строку на совпавшие шаблоны и вернувшись к оставшимся сегментам. Это прекрасное решение, компактное и довольно элегантное. Его слабость заключается в потенциально большом количестве подстрок и операций конкатенации, а также в ограничениях размера стека, применимых ко всем рекурсивным решениям.

  • Разделите текст на слова и используйте потоки Java 8 для элегантного выполнения замен, как предложено @msandiford , но, конечно, это работает, только если вы хорошо разбиваете границы слов, что делает его непригодным в качестве общего решения

Вот моя версия, основанная на идеях, заимствованных из реализации Apache . Это не просто и не элегантно, но работает и должно быть относительно эффективным, без лишних шагов. В двух словах, это работает так: многократно находите следующий соответствующий шаблон поиска в тексте и используйте a StringBuilderдля накопления несопоставленных сегментов и замен.

public static String replaceEach(String text, String[] searchList, String[] replacementList) {
    // TODO: throw new IllegalArgumentException() if any param doesn't make sense
    //validateParams(text, searchList, replacementList);

    SearchTracker tracker = new SearchTracker(text, searchList, replacementList);
    if (!tracker.hasNextMatch(0)) {
        return text;
    }

    StringBuilder buf = new StringBuilder(text.length() * 2);
    int start = 0;

    do {
        SearchTracker.MatchInfo matchInfo = tracker.matchInfo;
        int textIndex = matchInfo.textIndex;
        String pattern = matchInfo.pattern;
        String replacement = matchInfo.replacement;

        buf.append(text.substring(start, textIndex));
        buf.append(replacement);

        start = textIndex + pattern.length();
    } while (tracker.hasNextMatch(start));

    return buf.append(text.substring(start)).toString();
}

private static class SearchTracker {

    private final String text;

    private final Map<String, String> patternToReplacement = new HashMap<>();
    private final Set<String> pendingPatterns = new HashSet<>();

    private MatchInfo matchInfo = null;

    private static class MatchInfo {
        private final String pattern;
        private final String replacement;
        private final int textIndex;

        private MatchInfo(String pattern, String replacement, int textIndex) {
            this.pattern = pattern;
            this.replacement = replacement;
            this.textIndex = textIndex;
        }
    }

    private SearchTracker(String text, String[] searchList, String[] replacementList) {
        this.text = text;
        for (int i = 0; i < searchList.length; ++i) {
            String pattern = searchList[i];
            patternToReplacement.put(pattern, replacementList[i]);
            pendingPatterns.add(pattern);
        }
    }

    boolean hasNextMatch(int start) {
        int textIndex = -1;
        String nextPattern = null;

        for (String pattern : new ArrayList<>(pendingPatterns)) {
            int matchIndex = text.indexOf(pattern, start);
            if (matchIndex == -1) {
                pendingPatterns.remove(pattern);
            } else {
                if (textIndex == -1 || matchIndex < textIndex) {
                    textIndex = matchIndex;
                    nextPattern = pattern;
                }
            }
        }

        if (nextPattern != null) {
            matchInfo = new MatchInfo(nextPattern, patternToReplacement.get(nextPattern), textIndex);
            return true;
        }
        return false;
    }
}

Модульные тесты:

@Test
public void testSingleExact() {
    assertEquals("bar", StringUtils.replaceEach("foo", new String[]{"foo"}, new String[]{"bar"}));
}

@Test
public void testReplaceTwice() {
    assertEquals("barbar", StringUtils.replaceEach("foofoo", new String[]{"foo"}, new String[]{"bar"}));
}

@Test
public void testReplaceTwoPatterns() {
    assertEquals("barbaz", StringUtils.replaceEach("foobar",
            new String[]{"foo", "bar"},
            new String[]{"bar", "baz"}));
}

@Test
public void testReplaceNone() {
    assertEquals("foofoo", StringUtils.replaceEach("foofoo", new String[]{"x"}, new String[]{"bar"}));
}

@Test
public void testStory() {
    assertEquals("Once upon a foo, there was a bar and a baz, and another bar and a cat.",
            StringUtils.replaceEach("Once upon a baz, there was a foo and a bar, and another foo and a cat.",
                    new String[]{"foo", "bar", "baz"},
                    new String[]{"bar", "baz", "foo"})
    );
}

21

Найдите первое слово для замены. Если он находится в строке, выполните рекурсивный анализ в части строки до вхождения и в части строки после вхождения.

В противном случае перейдите к следующему слову, подлежащему замене.

Наивная реализация может выглядеть так

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

Пример использования:

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

Вывод:

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

Менее наивная версия:

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

К сожалению, у Java Stringнет indexOf(String str, int fromIndex, int toIndex)метода. Я пропустил реализацию indexOfздесь, так как не уверен, что это правильно, но его можно найти на ideone , а также некоторые грубые временные рамки различных решений, размещенных здесь.


2
Хотя использование существующей библиотеки, такой как Apache Commons, для подобных вещей, несомненно, является самым простым способом решения этой довольно распространенной проблемы, вы продемонстрировали реализацию, которая работает над частями слов, над словами, определенными во время выполнения, и не заменяет подстроки магическими токенами в отличие от (в настоящее время) ответы с более высоким рейтингом. +1
Бухб

Прекрасно, но поражает, когда входной файл размером 100 МБ.
Кристоф Де Тройер

12

Однострочник в 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());
  • Регулярные выражения Lookaround ( ?<=, ?=): http://www.regular-expressions.info/lookaround.html
  • Если слова могут содержать специальные символы регулярного выражения, используйте Pattern.quote, чтобы избежать их.
  • Я использую guava ImmutableMap для краткости, но, очевидно, любая другая карта тоже подойдет.

11

Вот возможность потоков Java 8, которая может быть интересна для некоторых:

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

Вот пример того же алгоритма в Java 7:

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

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

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

System.out.println(translated);

10
Это хорошее предложение, когда вещи, которые вы хотите заменить, являются реальными словами, разделенными пробелами (или подобными), но это не будет работать для замены подстрок слова.
Саймон Форсберг

+1 для потоков Java8. Жаль, что для этого требуется разделитель.
Навин

6

Если вы хотите заменить слова в предложении, разделенные пробелами, как показано в вашем примере, вы можете использовать этот простой алгоритм.

  1. Сплит история на пустом месте
  2. Замените каждый элемент, если foo замените его на bar и наоборот.
  3. Соединить массив обратно в одну строку

Если расщепление по пространству неприемлемо, можно следовать этому альтернативному алгоритму. Вы должны использовать более длинную строку в первую очередь. Если строки foo и fool, вам нужно сначала использовать fool, а затем foo.

  1. Сплит на слово фу
  2. Замените bar на foo для каждого элемента массива
  3. Присоедините этот массив обратно, добавляя строку после каждого элемента, кроме последнего

1
Это то, что я тоже хотел предложить. Хотя это добавляет ограничение, что текст - это слова, заключенные в пробелы. :)
Разработчик Marius Žilėnas

@ MariusŽilėnas Я добавил альтернативный алгоритм.
fastcodejava

5

Вот менее сложный ответ с использованием карты.

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

И метод называется

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

Вывод: круто Раффи, Раффи Раффи круто


1
бег replaced.replaceAll("Raffy", "Barney");за этим сделает его леген ... подождать; Дарий !!!
Кил

3

Если вы хотите иметь возможность обрабатывать несколько вхождений искомых строк, которые вы хотите заменить, вы можете легко это сделать, разделив строку по каждому поисковому запросу, а затем заменив ее. Вот пример:

String regex = word1 + "|" + word2;
String[] values = Pattern.compile(regex).split(story);

String result;
foreach subStr in values
{
   subStr = subStr.replace(word1, word2);
   subStr = subStr.replace(word2, word1);
   result += subStr;
}

3

Вы можете достичь своей цели с помощью следующего блока кода:

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

Он заменяет слова независимо от порядка. Вы можете расширить этот принцип в служебный метод, например:

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

Который будет потребляться как:

String story = "Once upon a time, in a foo, there was a foo and a bar.";
story = replace(story, new String[] { "bar", "foo" },
    new String[] { "foo", "bar" }));

3

Это работает и просто:

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

Вы используете это так:

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

Примечание: это рассчитывает на струнном , не содержащий символ \ufdd0, который представляет собой символ , постоянно зарезервирован для внутреннего использования Unicode (см http://www.unicode.org/faq/private_use.html ):

Я не думаю, что это необходимо, но если вы хотите быть в полной безопасности, вы можете использовать:

public String replaceBoth(String text, String token1, String token2) {
    if (text.contains("\ufdd0") || token1.contains("\ufdd0") || token2.contains("\ufdd0")) throw new IllegalArgumentException("Invalid character.");
    return text.replace(token1, "\ufdd0").replace(token2, token1).replace("\ufdd0", token2);
    }

3

Обмен только одним вхождением

Если во входных данных есть только одно вхождение каждой из заменяемых строк, вы можете сделать следующее:

Прежде чем приступить к любой замене, получите индексы вхождений слов. После этого мы заменяем только слова, найденные по этим индексам, а не все вхождения. Это решение использует StringBuilderи не производит промежуточные Stringсорта String.replace().

Следует отметить одну вещь: если заменяемые слова имеют разную длину, после первой замены второй индекс может измениться (если 1-е слово встречается до 2-го) точно с разницей в 2 длины. Таким образом, выравнивание второго индекса гарантирует, что это работает, даже если мы меняем слова различной длины.

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

Обмен произвольным числом вхождений

По аналогии с предыдущим случаем мы сначала соберем индексы (вхождения) слов, но в этом случае это будет список целых чисел для каждого слова, а не только для одного int. Для этого мы будем использовать следующий служебный метод:

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

И используя это, мы заменим слова другим, уменьшив индекс (что может потребовать чередования двух заменяемых слов), так что нам даже не придется исправлять индексы после замены:

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

Я не уверен, как Java обрабатывает Unicode, но эквивалент C # этого кода будет неверным. Проблема заключается в том, что indexOfсовпадающая подстрока может не иметь такую ​​же длину, что и строка поиска, благодаря особенностям эквивалентности строк Юникода.
CodesInChaos

@CodesInChaos Это работает безупречно в Java, потому что Java Stringявляется массивом символов, а не байтовым массивом. Все методы Stringи StringBuilderработают с символами, а не с байтами, которые не кодируются. Таким образом, indexOfсовпадения имеют точно такую ​​же (символьную) длину, что и строки поиска.
icza

И в C #, и в Java строка представляет собой последовательность кодовых единиц UTF-16. Проблема в том, что существуют разные последовательности кодовых точек, которые Юникод считает эквивалентными. Например, äможет быть закодирован как одна кодовая точка или как aпоследующее объединение ¨. Есть также некоторые кодовые точки, которые игнорируются, такие как соединения нулевой ширины (не). Не имеет значения, состоит ли строка из байтов, символов и т. Д., Но какие правила сравнения indexOfиспользуются. Он может использовать просто кодовую единицу путем сравнения кодовых единиц («Порядковый») или может реализовывать эквивалентность Юникода. Я не знаю, какую именно Java выбрал.
CodesInChaos

Например, "ab\u00ADc".IndexOf("bc")возвращает 1в .net соответствие строки из двух символов строке bcиз трех символов.
CodesInChaos

1
@CodesInChaos Теперь я понимаю, что вы имеете в виду. В Java "ab\u00ADc".indexOf("bc")возвращается, -1значит "bc"не было найдено в "ab\u00ADc". Таким образом, все еще стоит отметить, что в Java работает вышеупомянутый алгоритм, indexOf()совпадения имеют точно такую ​​же (символьную) длину, что и строки поиска, и indexOf()сообщают о совпадениях только в том случае, если совпадают символы последовательности (кодовые точки).
icza

2

Легко написать метод для этого, используя 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();
}

Тестирование:

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

Вывод:

Есть три собаки и две волнистые попугайчики.

Это не сразу очевидно, но такая функция все еще может зависеть от порядка, в котором указаны замены. Рассматривать:

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

Вывод:

Ява для JavaScript, как Хэм для Хомяка

Но поменяйте местами замены:

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

Вывод:

Ява для JavaScript, как Хэм для HamScript

К сожалению! :)

Поэтому иногда полезно убедиться, что найдено наибольшее совпадение (как strtr, например , функция PHP ). Эта версия метода сделает это:

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

Обратите внимание, что вышеупомянутые методы чувствительны к регистру. Если вам нужна версия без учета регистра, легко изменить вышеприведенное, поскольку она String.regionMatchesможет принимать ignoreCaseпараметр.


2

Если вы не хотите никаких зависимостей, вы можете просто использовать массив, который допускает только одноразовое изменение. Это не самое эффективное решение, но оно должно работать.

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

Тогда это будет работать.

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

String[] a = {"foo", "bar"};
String[] b = {"bar", "foo"};
String[] c = {"there", "Pocahontas"};
story = replace(story, a, b, c);

System.out.println(story); // Once upon a time, Pocahontas was a bar and a foo.

2

Вы выполняете несколько операций поиска-замены на входе. Это приведет к нежелательным результатам, когда строки замены содержат строки поиска. Рассмотрим пример foo-> bar, bar-foo, вот результаты для каждой итерации:

  1. Когда-то давно был фу и бар. (Вход)
  2. Когда-то был бар и бар. (Foo-> бар)
  3. Когда-то давно был фу и фу. (bar-> foo, вывод)

Вы должны выполнить замену за одну итерацию, не возвращаясь. Решение о грубой силе следующее:

  1. Поиск ввода от текущей позиции до конца для нескольких строк поиска, пока не будет найдено соответствие
  2. Заменить соответствующую строку поиска соответствующей строкой замены
  3. Установить текущую позицию для следующего символа после замененной строки
  4. Повторение

Такая функция, как String.indexOfAny(String[]) -> int[]{index, whichString}было бы полезно. Вот пример (не самый эффективный):

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

Некоторые тесты:

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

Демо на IDEONE
Демо на IDEONE, альтернативный код


1

Вы всегда можете заменить его словом, которое наверняка больше нигде не появится в строке, а затем выполнить вторую замену позже:

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

Обратите внимание, что это не будет работать правильно, если "StringYouAreSureWillNeverOccur"это произойдет.


5
Используйте символы из области частного использования Unicode, U + E000..U + F8FF, создавая StringThatCannotEverOccur. Вы можете отфильтровать их заранее, так как они не должны существовать на входе.
Дэвид Конрад

Или U + FDD0..U + FDEF, «нехарактерные символы», которые зарезервированы для внутреннего использования.
Дэвид Конрад

1

Рассмотрите возможность использования StringBuilder

Затем сохраните индекс, где каждая строка должна начинаться. Если в каждой позиции используется символ-заполнитель, удалите его и вставьте строку пользователя. Затем вы можете отобразить конечную позицию, добавив длину строки к начальной позиции.

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

То, что я могу только поделиться, это мой собственный метод.

Вы можете использовать временный String temp = "<?>";илиString.Format();

Это мой пример кода, созданного в консольном приложении через - «Только идея, а не точный ответ» .

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

Или вы также можете использовать 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;
        }

Вывод: time upon a Once, there was a bar and a foo.


Это довольно забавно. Что вы будете делать, если он захочет заменить "_"?
Пьер-Александр Бушар

@ Pier-AlexandreBouchard В методах я меняю значение tempс "_"на <?>. Но при необходимости он может добавить в метод еще один параметр, который изменит темп. - "Лучше быть проще, не так ли?"
Леонель Сармьенто

Я хочу сказать, что вы не можете гарантировать ожидаемый результат, потому что если temp == заменить, ваш путь не будет работать.
Пьер-Александр Бушар

1

Вот моя версия, которая основана на словах:

class TextReplace
{

    public static void replaceAll (String text, String [] lookup,
                                   String [] replacement, String delimiter)
    {

        String [] words = text.split(delimiter);

        for (int i = 0; i < words.length; i++)
        {

            int j = find(lookup, words[i]);

            if (j >= 0) words[i] = replacement[j];

        }

        text = StringUtils.join(words, delimiter);

    }

    public static  int find (String [] array, String key)
    {

        for (int i = 0; i < array.length; i++)
            if (array[i].equals(key))
                return i;

        return (-1);

    }

}

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

Немного сложно, но вам нужно сделать еще несколько проверок.

1. преобразовать строку в массив символов

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

2. зацикливайтесь на temp и заменяйте fooна barи barс, так fooкак нет шансов получить заменяемую строку снова.


1

Ну, короче ответ ...

String word1 = "bar";
String word2 = "foo";
String story = "Once upon a time, there was a foo and a bar.";
story = story.replace("foo", "@"+ word1).replace("bar", word2).replace("@" + word2, word1);
System.out.println(story);

1

Используя найденный здесь ответ, вы можете найти все вхождения строк, которыми хотите заменить.

Так, например, вы запускаете код в ответе выше SO. Создайте две таблицы индексов (скажем, bar и foo не появляются в вашей строке только один раз), и вы можете работать с этими таблицами, заменяя их в вашей строке.

Теперь для замены в определенных местах индекса вы можете использовать:

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

Принимая во внимание, posчто это индекс, с которого начинаются ваши строки (из таблиц индексов, которые я цитировал выше). Допустим, вы создали две таблицы индексов для каждой. Давайте назовем их indexBarи indexFoo.

Теперь, заменяя их, вы можете просто запустить два цикла, по одному для каждой замены, которую вы хотите сделать.

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

Точно так же еще один цикл для indexFoo.

Это может быть не так эффективно, как другие ответы здесь, но это проще понять, чем Карты или другие вещи.

Это всегда даст вам желаемый результат и для нескольких возможных вхождений каждой строки. Пока вы храните индекс каждого вхождения.

Также этот ответ не нуждается ни в рекурсии, ни в каких-либо внешних зависимостях. Что касается сложности, то, вероятно, это O (n в квадрате), тогда как n - сумма совпадений обоих слов.


-1

Я разработал этот код, чтобы решить проблему:

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

В основном использовании change(story,word2,word1).


2
Это будет работать только в том случае, если в каждой строке будет ровно одно появление
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);
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.