[java] 한 줄이 다른 줄을 바꾸지 않는 방법으로 두 줄을 어떻게 바꿀 수 있습니까?

다음 코드가 있다고 가정 해 봅시다.

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."나는 그것을 달성 할 수 있는가?



답변

Apache Commons StringUtilsreplaceEach()메소드를 사용하십시오 .

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


답변

문장에 아직없는 중간 값을 사용합니다.

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가 제공 한 훌륭한 조언을 반복합니다 .

가능성이없는 것으로 현명하게 (멍청하게) 선택된 문자열을 사용하지 마십시오. 유니 코드 개인 사용 영역 (U + E000..U + F8FF)의 문자를 사용하십시오. 이러한 문자는 합법적으로 입력에 포함되어서는 안되므로 (일부 응용 프로그램 내에서 응용 프로그램 특정 의미 만 있음) 해당 문자를 먼저 제거한 다음 교체 할 때 자리 표시 자로 사용하십시오.


답변

Matcher#appendReplacementand를 사용하여 이와 같은 것을 시도 할 수 있습니다 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());
옛날 옛적에 술집과 foo가있었습니다.


답변

이것은 쉬운 문제가 아닙니다. 검색 대체 매개 변수가 많을수록 더 까다로워집니다. 추악하고 우아하고 효율적으로 낭비되는 팔레트에 여러 가지 옵션이 있습니다.

  • @AlanHay가 권장 되는대로StringUtils.replaceEach Apache Commons에서 사용하십시오 . 프로젝트에 새로운 의존성을 추가 할 수 있다면 좋은 옵션입니다. 운이 좋을 수도 있습니다 : 종속성이 이미 프로젝트에 포함될 수 있습니다

  • @Jeroen이 제안한 대로 임시 자리 표시자를 사용하고 2 단계로 교체를 수행하십시오.

    1. 모든 검색 패턴을 원본 텍스트에없는 고유 한 태그로 바꿉니다.
    2. 자리 표시자를 실제 대상 교체로 교체

    이는 여러 가지 이유로 큰 접근 방식이 아닙니다. 첫 번째 단계에서 사용 된 태그가 실제로 고유해야합니다. 실제로 필요한 것보다 더 많은 문자열 교체 작업을 수행합니다.

  • 모든 패턴에서 정규식을 구축하고 함께 방법을 사용 Matcher하고StringBuffer 등이 제안 @arshajii . 이 끔찍한되지 않습니다,하지만 그 큰 중 하나, 정규식을 구축하는 것은 일종의 hackish의, 그리고이 포함로 StringBuffer찬성 얼마 전 패션 A의 나갔다한다 StringBuilder.

  • @mjolka가 제안한 재귀 솔루션을 사용 하여 일치하는 패턴으로 문자열을 분할하고 나머지 세그먼트를 반복하십시오 . 이것은 작고 매우 우아한 훌륭한 솔루션입니다. 약점은 잠재적으로 많은 부분 문자열 및 연결 작업이며 모든 재귀 솔루션에 적용되는 스택 크기 제한입니다.

  • @msandiford가 제안한 것처럼 텍스트를 단어로 나누고 Java 8 스트림을 사용하여 교체를 우아하게 수행 하지만 단어 경계에서 분할해도 괜찮은 경우에만 작동하므로 일반적인 솔루션으로 적합하지 않습니다.

다음은 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"})
    );
}


답변

대체 할 첫 단어를 검색하십시오. 문자열에 있으면 발생 전 문자열 부분과 발생 후 문자열 부분에서 되풀이하십시오.

그렇지 않으면 다음 단어를 계속 바꾸십시오.

순진한 구현은 다음과 같습니다.

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 에서 찾을 수 있습니다 .


답변

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 를 사용 하여 이스케이프 처리하십시오.
  • 간결함을 위해 구아바 ImmutableMap을 사용하지만 분명히 다른 모든지도 잘 작동합니다.

답변

다음은 일부 사용자에게 흥미로운 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);