Як я можу замінити два рядки таким чином, щоб одна не закінчилася заміною іншої?


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 випадки "бар", тож ваша друга заміна не робить те, що ви очікуєте - це не може знати, що ви хочете замінити лише те, якого ви вже не замінювали минулого разу. @HagenvonEitzen Цікаво. Я б очікував, що робоче рішення відповідатиме та замінить перший з будь-яких рядків, які він знайде, а потім повторить із кінця заміненого розділу.
DeveloperInDevelopment

1
Рішення Jeroen - це те, що я часто використовую в текстових редакторах, коли мені потрібно зробити групове перейменування. Це просто, легко зрозуміти, не вимагає спеціальної бібліотеки, і може бути безглуздим з ходом думки.
Гарячі лизання

Відповіді:


88

Скористайтеся replaceEach()методом від Apache Commons StringUtils :

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

2
будь-яка ідея, що саме замінює кожен робить внутрішньо?
Марек

3
@Marek дуже ймовірно, що функція здійснює пошук та індексує кожен знайдений елемент, а потім замінює їх усі, коли всі вони були індексовані.

16
Ви можете знайти джерело для цього тут навколо лінії 4684.
Йерун Vannevel

Шкода, що це не-оп, коли nullйого передають.
праворуч

87

Ви використовуєте проміжне значення (якого ще немає у реченні).

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

У відповідь на критику: якщо ви використовуєте досить великий неабияке рядок як zq515sqdqs5d5sq1dqs4d1q5dqqé "і é5d4sqjshsjddjhodfqsqc, nvùq ^ мкЕД; d & € SDQ: d :;) àçàçlala і використовувати це, то навряд чи момент , коли я навіть не буду дебатувати його що користувач коли-небудь введе це. Єдиний спосіб дізнатися, чи буде користувач, знаючи вихідний код, і в цей момент у вас є цілий інший рівень турбот.

Так, можливо, існують химерні способи регексу. Я вважаю за краще щось читабельне, яке, я знаю, теж не вибухне на мені.

Також підтверджуючи чудову пораду, яку дав @David Conrad у коментарях :

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


4
@arshajii Я думаю, що це залежить від вашого визначення "кращого" ... якщо воно працює і є сприйнятливим виконавцем, перейдіть до наступного завдання програмування та вдосконалюйте його пізніше під час рефакторингу - це мій підхід.
Метт Coubrough

24
Очевидно, що "ляля" - це лише приклад. У виробництві слід використовувати " zq515sqdqs5d5sq1dqs4d1q5dqqé" & é & € sdq: d:;) àçàçlala ".
Jeroen Vannevel

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

22
Насправді, прочитавши відповіді про 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-лоп).
Аршаджій

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

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

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

32

Це непроста проблема. І чим більше у вас параметрів пошуку-заміни, тим складніше він отримує. У вас є кілька варіантів, розкиданих на палітрі потворно-елегантних, ефективно-марнотратних:

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

  • Використовуйте тимчасовий заповнювач, як запропонував @Jeroen , і виконайте заміну в два етапи:

    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());
  • Орієнтовні регулярні вирази ( ?<=, ?=): 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 замінить його на бар і віс-варса
  3. Об’єднайте масив назад в один рядок

Якщо розділення на простір неприйнятне, можна дотримуватися цього альтернативного алгоритму. Спочатку потрібно скористатися довшою строкою. Якщо рядки foo і дурні, вам потрібно спочатку скористатися дурнем, а потім foo.

  1. Розділити на слово foo
  2. Замініть рядок на foo кожен елемент масиву
  3. Приєднуйтесь до цього масиву назад, додаючи панель після кожного елемента, крім останнього

1
Це я думав також запропонувати. Хоча це додає обмеження, що текст - це слова, оточені пробілами. :)
Розробник Marius Žilėnas

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

5

Ось менш складна відповідь за допомогою 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;
     }

І метод називається

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().

Варто зазначити одне: якщо слова, що змінюються, мають різну довжину, після першої заміни другий індекс може змінитися (якщо перше слово відбудеться перед другим) саме з різницею двох довжин. Тож вирівнювання другого індексу забезпечить це, навіть якщо ми підміняємо слова різною довжиною.

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. Проблема полягає в тому, що існують різні послідовності кодових точок, які unicode вважає еквівалентними. Наприклад, äможе бути закодована у вигляді однієї кодової точки або як aнаступна комбінація ¨. Існують і деякі кодові точки, які ігноруються, такі як нульові ширини (не) столяри. Не має значення, чи рядок складається з байтів, знаків чи іншого, але які параметри порівняння indexOfвикористовують. Він може використовувати просто кодову одиницю за допомогою порівняння кодової одиниці ("Звичайна"), або він може реалізувати еквівалентність унікоду. Я не знаю, яку з них вибрав.
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);

Вихід:

Java - це JavaScript, як Хам - Hamster

Але скасуйте заміни:

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

Вихід:

Java - це JavaScript, як Ham - це 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-> bar)
  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. Включіть темп і замініть 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

Використовуючи відповідь, знайдений тут, ви можете знайти всі виникнення рядків, які ви хочете замінити.

Так, наприклад, ви запускаєте код у вищевказаній відповіді ТАК. Створіть дві таблиці індексів (скажімо, рядок і стоп не відображаються лише один раз у рядку), і ви можете працювати з цими таблицями щодо їх заміни у рядку.

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

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
Він працюватиме лише за наявності у кожної строки рівно одного вигляду
Вік

-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.