İki dizeyi, diğerini değiştirmeyecek şekilde nasıl değiştirebilirim?


162

Diyelim ki aşağıdaki kod var:

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

Bu kod çalıştırıldıktan sonra değeri storyolacak"Once upon a time, there was a foo and a foo."

Benzer bir sorun, onları ters sırayla değiştirirsem ortaya çıkar:

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

Değeri storyolacak"Once upon a time, there was a bar and a bar."

Amacım açmaktır storyiçine "Once upon a time, there was a bar and a foo."Bunu başarmak nasıl olabilir?


7
+1 kesinlikle ile swap(String s1, String s2, String s3)tüm oluşumlarını değiştiren bir işlev olmalıdır ve bunun tersi de geçerlidir. s2s3
Ryan

Girdideki değiştirilebilir kelimelerin her birinin tek bir örneği olduğunu varsayabilir miyiz?
icza

14
Köşe-kasa: "ab" ve "ba" kelimelerini "ababababababa" da değiştirirken çıktı olarak ne bekliyoruz?
Hagen von Eitzen

1
Aşağıda bazı iyi çözümleriniz var, ancak yaklaşımınızın neden işe yaramadığını anlıyor musunuz? İlk olarak, "bir foo ve bir bar vardı" var. İlk değiştirme ("foo" -> "bar") sonra "bir bar ve bir bar vardı" var. Artık 2 "bar" örneğiniz var, bu nedenle ikinci yedeklemeniz beklediğiniz şeyi yapmaz - yalnızca son kez değiştirmediğinizi değiştirmek istediğinizi bilmenin bir yolu yoktur. @HagenvonEitzen İlginç. Ben çalışan bir çözüm bulur ve bulduğu her iki dize ilkini değiştirmek ve daha sonra değiştirilen bölümün sonunda tekrar beklenir.
DeveloperInDevelopment

1
Jeroen'in çözümü, toplu yeniden adlandırma yapmam gerektiğinde metin editörlerinde sık kullandığım bir çözümdür. Basit, anlaşılması kolay, özel bir kütüphane gerektirmez ve bir düşünce tarzı ile kusursuz olabilir.
Hot Licks

Yanıtlar:


88

Apache Commons StringUtilsreplaceEach() yöntemini kullanın :

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

2
Herhangi bir fikir tam olarak ne değiştirirHer dahili olarak?
Marek

3
@Marek, işlevin bir arama yapması ve bulunan her öğeyi dizine eklemesi, ardından dizine eklendikten sonra hepsini değiştirmesi çok olasıdır.

16
Bunun için kaynak bulabilirsiniz burada etrafında 4684. hat
Jeroen Vannevel

Yine de null, geçtiğinde hayır-op olması üzücü .
sağ kanat

87

Ara değer kullanırsınız (cümle içinde henüz mevcut değildir).

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

Eleştiriye bir yanıt olarak: zq515sqdqs5d5sq1dqs4d1q5dqqé "& é5d4sqjshsjddjhodfqsqc, nvùq ^ µù; d & € sdq: d:; gibi bile yeterince büyük olmayan bir dize kullanırsanız bu Kullanıcının girip girmeyeceğini öğrenmenin tek yolu kaynak kodunu bilmektir ve bu noktada başka bir endişe seviyesine sahip olursunuz.

Evet, belki süslü regex yolları vardır. Benden okunamayacağını bildiğim okunabilir bir şeyi tercih ederim.

Ayrıca @David Conrad'ın yorumlarında verdiği mükemmel tavsiyeyi yinelemek :

Olası olmayacak şekilde seçilmiş akıllıca (aptalca) bir dizi kullanmayın. Unicode Özel Kullanım Alanı, U + E000..U + F8FF karakterlerini kullanın. İlk olarak bu tür karakterleri kaldırın, çünkü bunlar meşru bir şekilde girdide olmamalıdır (yalnızca bazı uygulamalarda uygulamaya özgü bir anlamı vardır), ardından değiştirirken yer tutucu olarak kullanın.


4
@arshajii Sanırım bu "daha iyi" tanımınıza bağlıdır ... eğer çalışıyor ve kabul edilebilir bir performans sergiliyorsa, bir sonraki programlama görevine geçin ve yeniden düzenleme sırasında daha sonra geliştirmek benim yaklaşımım olacaktır.
Matt Coubrough

24
Açıkçası "lala" sadece bir örnektir. Üretimde " zq515sqdqs5d5sq1dqs4d1q5dqqé" & é & € sdq: d:;) àçàçlala " yı kullanmalısınız .
Jeroen Vannevel

81
Olası olmayacak şekilde seçilmiş akıllıca (aptalca) bir dizi kullanmayın. Unicode Özel Kullanım Alanı, U + E000..U + F8FF karakterlerini kullanın. İlk olarak bu tür karakterleri kaldırın, çünkü bunlar meşru bir şekilde girdide olmamalıdır (yalnızca bazı uygulamalarda uygulamaya özgü bir anlamı vardır), ardından değiştirirken yer tutucu olarak kullanın.
David Conrad

22
Aslında, üzerinde Unicode SSS okuduktan sonra , U + FDD0..U + FDEF aralığındaki karakter olmayanların daha iyi bir seçim olacağını düşünüyorum.
David Conrad

6
@Taemyr Elbette, ama biri girdiyi dezenfekte etmek zorunda, değil mi? Dize değiştirme işlevinin tüm dizelerde çalışmasını beklerim, ancak bu işlev güvenli olmayan girişler için kesilir.
Navin

33

Sen kullanarak, böyle bir şey deneyebilir Matcher#appendReplacementve 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());
Bir zamanlar bir bar ve bir foo vardı.

2
Bu eser eğer mı foo, barve storytüm bilinmeyen değerlere sahip?
Stephen P

1
Ben aslında sabit kodlanmış ettik @StephenP "foo"ve "bar"OP onun kodda olduğu gibi yedek dizeleri, ancak yaklaşımın aynı tip ince bu değerler bilinen olmasa bile çalışacak (eğer kullanım olurdu if/ else ifyerine ait switcholan while-loop).
arshajii

6
Normal ifadeyi oluştururken dikkatli olmalısınız. Pattern.quotekullanışlı olur \Qve ve \E.
David Conrad

1
@arshajii - yep, word1, word2 ve hikayeyi parametre olarak alarak "swapThese" yöntemi olarak bunu kendime kanıtladı. +1
Stephen P

4
Temizleyici bile , kelimeyi kullanmak (foo)|(bar)ve daha sonra m.group(1) != null, kelimeleri eşleştirmek için tekrar etmemek için kontrol etmek olacaktır.
Jörn Horstmann

32

Bu kolay bir sorun değil. Ve ne kadar çok arama-değiştirme parametresi varsa, o kadar zor olur. Çirkin zarif, verimli ve savurgan palete dağılmış birkaç seçeneğiniz var:

  • @AlanHay önerildiği StringUtils.replaceEachgibi Apache Commons'tan kullanın . Projenize yeni bağımlılıklar eklemekte özgürseniz, bu iyi bir seçenektir. Şanslı olabilirsiniz: bağımlılık projenize zaten dahil edilmiş olabilir

  • @Jeroen'in önerdiği gibi geçici bir yer tutucu kullanın ve değiştirme işlemini 2 adımda gerçekleştirin:

    1. Tüm arama kalıplarını orijinal metinde bulunmayan benzersiz bir etiketle değiştirin
    2. Yer tutucuları gerçek hedef değiştirme ile değiştirin

    Bu, pek çok nedenden ötürü harika bir yaklaşım değildir: ilk adımda kullanılan etiketlerin gerçekten benzersiz olmasını sağlamak gerekir; gerçekten gerekenden daha fazla dize değiştirme işlemi gerçekleştirir

  • Tüm kalıplardan bir regex kurmak ve birlikte yöntemini kullanın MatcherveStringBuffer olarak önerdiği @arshajii . Bu korkunç değil, ama o kadar da büyük değil, regex'i inşa etmek bir tür haksızlıktır ve StringBufferbir süre önce lehine modası geçmeyi içerir StringBuilder.

  • Dizeyi eşleşen desenlere bölerek ve kalan segmentler üzerinde yineleyerek , @mjolka tarafından önerilen özyinelemeli bir çözüm kullanın . Bu iyi bir çözümdür, kompakt ve oldukça zariftir. Zayıflığı potansiyel olarak birçok alt dize ve birleştirme işlemi ve tüm özyinelemeli çözümlere uygulanan yığın boyutu sınırlarıdır

  • Metni kelimelere ayırın ve değiştirmeleri @msandiford'un önerdiği gibi zarif bir şekilde gerçekleştirmek için Java 8 akışlarını kullanın , ancak elbette yalnızca kelime sınırlarında bölme konusunda sorun yaşıyorsanız işe yarar, bu da genel bir çözüm olarak uygun değildir

İşte Apache'nin uygulamasından ödünç alınan fikirlere dayanan versiyonum . Ne basit ne de zariftir, ancak işe yaramaz ve gereksiz adımlar olmadan nispeten verimli olmalıdır. Özetle, şu şekilde çalışır: metinde bir sonraki eşleşen arama desenini tekrar tekrar bulun StringBuilderve eşsiz segmentleri ve değiştirmeleri biriktirmek için a kullanın.

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

Birim testleri:

@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

Değiştirilecek ilk kelimeyi arayın. Dize içindeyse, dizenin oluşumdan önceki kısmında ve dizenin oluşumundan sonraki kısmında yinelenir.

Aksi takdirde, değiştirilecek bir sonraki kelimeyle devam edin.

Saf bir uygulama şöyle görünebilir

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

Örnek kullanım:

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

Çıktı:

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

Daha az saf bir versiyon:

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

Ne yazık ki, Java'nın yöntemi Stringyoktur indexOf(String str, int fromIndex, int toIndex). indexOfBurada uygulanmasını atladım , çünkü doğru olduğundan emin değilim, ama burada yayınlanan çeşitli çözümlerin kaba zamanlamaları ile birlikte ideone'de bulunabilir .


2
Bu tür şeyler için apache commons gibi mevcut bir kütüphaneyi kullanmak şüphesiz bu oldukça yaygın sorunu çözmenin en kolay yolu olsa da, kelimelerin bölümlerinde, çalışma zamanında karar verilen kelimelerde ve alt dizeleri aksine sihirli jetonlarla değiştirmeden çalışan bir uygulama gösterdiniz. (şu anda) daha yüksek oy kullanan cevaplar. +1
Buhb

Güzel, ancak 100 mb'lik bir giriş dosyası sağlandığında yere çarpıyor.
Christophe De Troyer

12

Java 8'de tek astar:

    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 düzenli ifadeleri ( ?<=, ?=): http://www.regular-expressions.info/lookaround.html
  • Sözcükler özel regex karakterleri içeriyorsa, Pattern.quote kullanın bunlardan kaçmak için kullanın.
  • Kısaca özlü olması için guava ImmutableMap kullanıyorum, ancak açıkçası başka herhangi bir Harita da işi yapacak.

11

İşte bazıları için ilginç olabilecek bir Java 8 akış olasılığı:

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'de aynı algoritmanın bir tahmini:

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
Değiştirmek istediğiniz şeyler boşluklarla (veya benzerleriyle) ayrılmış gerçek kelimeler olduğunda , ancak bu, bir kelimenin alt dizelerini değiştirmek için işe yaramazsa güzel bir öneridir .
Simon Forsberg

Java8 akışları için +1. Çok kötü bir sınırlayıcı gerektirir.
Navin

6

Örneğinizde gösterildiği gibi boşlukla ayrılmış bir cümledeki sözcükleri değiştirmek istiyorsanız, bu basit algoritmayı kullanabilirsiniz.

  1. Beyaz boşluk üzerinde split hikaye
  2. Her elemanı değiştirin, eğer foo çubuk ile değiştirin ve varsa
  3. Diziye bir dizeye tekrar katılın

Alanda bölme kabul edilemezse, bu alternatif algoritmayı takip edebilirsiniz. Önce daha uzun dizeyi kullanmanız gerekir. Eğer ipler foo ve aptalsa, önce aptal ve sonra foo kullanmanız gerekir.

  1. Foo kelimesi üzerinde bölün
  2. Çubuğu dizinin her öğesini foo ile değiştir
  3. Sonuncu hariç her öğeden sonra bu dizi geri ekleme çubuğuna katılın

1
Ben de bunu önermeyi düşünüyordum. Yine de metnin boşluklarla çevrili kelimeler olduğu konusunda bir kısıtlama getirmektedir. :)
Geliştirici Marius Žilėnas

@ MariusŽilėnas Alternatif bir algoritma ekledim.
fastcodejava

5

Harita'yı kullanarak daha az karmaşık bir cevap.

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

Ve yöntem denir

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

Çıktı: harika Raffy, Raffy Raffy harika harika


1
replaced.replaceAll("Raffy", "Barney");bundan sonra koşmak onu efsane yapacaktır ... bekleyin; Dary !!!
Keale

3

Değiştirilecek arama dizelerinin birden çok örneğini işleyebilmek istiyorsanız, dizeyi her arama teriminde bölerek ve sonra değiştirerek bunu kolayca yapabilirsiniz. İşte bir örnek:

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

Hedefinize aşağıdaki kod bloğuyla ulaşabilirsiniz:

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

Sıradan bağımsız olarak kelimelerin yerini alır. Bu ilkeyi aşağıdaki gibi bir yardımcı program yöntemine genişletebilirsiniz:

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

Hangi olarak tüketilecek:

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

Bu işe yarar ve basit:

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

Bu şekilde kullanırsınız:

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

Not: Bu \ufdd0, Unicode tarafından dahili kullanım için kalıcı olarak ayrılmış bir karakter olan karakter içermeyen Dizeler için geçerlidir (Bkz. Http://www.unicode.org/faq/private_use.html ):

Bunun gerekli olduğunu düşünmüyorum, ancak kesinlikle güvende olmak istiyorsanız şunları kullanabilirsiniz:

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

Yalnızca Bir Oluşumu Değiştirme

Girişteki değiştirilebilir dizelerin her birinin yalnızca bir örneği varsa, aşağıdakileri yapabilirsiniz:

Herhangi bir değiştirmeye geçmeden önce, kelimelerin oluşum endekslerini alın. Bundan sonra, sadece bu dizinlerde bulunan kelimeyi değiştiririz, tüm oluşumları değil. Bu çözüm, StringBuilderara ürünler Stringgibi kullanır ve üretmez String.replace().

Dikkat edilmesi gereken bir şey: takas edilebilir kelimelerin farklı uzunlukları varsa, ilk değiştirme işleminden sonra ikinci dizin (2. kelime 2.'den önce gerçekleşirse) tam olarak 2 uzunluk farkı ile değişebilir. Bu nedenle, ikinci dizini hizalamak, farklı uzunluklardaki kelimeleri değiştirsek bile bunun çalışmasını sağlayacaktır.

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

Değişen Keyfi Geçiş Sayısı

Önceki duruma benzer şekilde, ilk olarak kelimelerin indekslerini (oluşumlarını) toplayacağız, ancak bu durumda, sadece bir tane değil, her kelime için bir tamsayı listesi olacak int . Bunun için aşağıdaki yardımcı programı kullanacağız:

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

Ve bunu kullanarak, indeksi azaltarak kelimeleri değiştirin (2 değiştirilebilir kelime arasında geçiş yapması gerekebilir), böylece bir değiştirmeden sonra indeksleri düzeltmemiz bile gerekmez:

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 nasıl unicode işler emin değilim, ama bu kodun C # eşdeğeri yanlış olur. Sorun, indexOfunicode dize eşdeğerinin kendine özgü ifadeleri sayesinde eşleşen alt dizenin arama dizesiyle aynı uzunlukta olmaması olabilir.
CodesInChaos

@CodesInChaos Java Stringbir bayt dizisi değil, bir karakter dizisi olduğu için Java'da kusursuz çalışır . Tüm yöntemlerStringStringBuilder"Kodlamasız" karakterlerin ve baytlarda olmayan karakterler üzerinde çalışır. Böylece indexOfeşleşmeler, arama dizeleriyle tam olarak aynı (karakter) uzunluğa sahiptir.
icza

Hem C # hem de java'da bir dize, UTF-16 kod birimlerinin bir dizisidir. Sorun, unicode'un eşdeğer olduğunu düşündüğü farklı kod noktaları dizileri olmasıdır. Örneğin ä, tek bir kod noktası olarak veya aardından bir birleştirme olarak kodlanabilir ¨. Sıfır genişlikli (birleştirici olmayan) birleştiriciler gibi yok sayılan bazı kod noktaları da vardır. Dizenin bayt, karakter veya herhangi bir şey içermesi önemli değildir, ancak hangi karşılaştırma kurallarının indexOfkullandığı. Kod-birimi karşılaştırması ("Sıralı") ile basitçe kod-birimini kullanabilir ya da unicode denkliği uygulayabilir. Hangisini seçtiğimi bilmiyorum.
CodesInChaos

Örneğin , iki karakter dizesini üç karakter dizesiyle eşleştiren .net öğesini "ab\u00ADc".IndexOf("bc")döndürür . 1bc
CodesInChaos

1
@CodesInChaos Şimdi ne demek istediğini anlıyorum. Java ile "ab\u00ADc".indexOf("bc")döner-1 hangi araçlar "bc"bulunamadı bulundu "ab\u00ADc". Bu yüzden hala Java'da yukarıdaki algoritmanın çalıştığı, indexOf()eşleşmelerin arama dizeleriyle tam olarak aynı (karakter) uzunluğa sahip olduğunu ve indexOf()yalnızca charsequences (kod noktaları) eşleştiğinde eşleşmeleri bildirdiğini gösterir.
icza

2

Bunu yapmak için bir yöntem yazmak kolaydır 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();
}

Test yapmak:

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

Çıktı:

Üç köpek ve iki muhabbet kuşu var.

Hemen belli değildir, ancak böyle bir işlev, değiştirmelerin belirtildiği sıraya bağlı olabilir. Düşünmek:

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

Çıktı:

Ham, Hamster olduğu için Java JavaScript'tir

Ancak değiştirmeleri tersine çevirin:

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

Çıktı:

Ham, HamScript'e olduğu için Java JavaScript'e aittir

Hata! :)

Bu nedenle en uzun eşleşmeyi aradığınızdan emin olmak bazen yararlı olur (PHP gibistrtr işlevi . Yöntemin bu sürümü şunları yapacaktır:

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

Yukarıdaki yöntemlerin büyük / küçük harfe duyarlı olduğunu unutmayın. Büyük / küçük harfe duyarlı olmayan bir sürüme ihtiyacınız varsa, String.regionMatchesbir ignoreCaseparametreyi alabileceğinden yukarıdakileri değiştirmek kolaydır .


2

Herhangi bir bağımlılık istemiyorsanız, yalnızca bir kerelik bir değişikliğe izin veren bir dizi kullanabilirsiniz. Bu en verimli çözüm değil, ama işe yaramalı.

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

Sonra işe yarayabilirdi.

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

Girişte birden çok arama değiştirme işlemi gerçekleştiriyorsunuz. Bu, değiştirme dizeleri arama dizeleri içerdiğinde istenmeyen sonuçlar üretir. Foo-> bar, bar-foo örneğini düşünün, her yineleme için sonuçlar şunlardır:

  1. Bir zamanlar bir foo ve bir bar vardı. (giriş)
  2. Bir zamanlar bir bar ve bir bar vardı. (Foo-> bar)
  3. Bir zamanlar bir foo ve bir foo vardı. (bar-> foo, çıktı)

Değiştirme işlemini geri dönmeden tek bir yinelemede yapmanız gerekir. Bir kaba kuvvet çözümü aşağıdaki gibidir:

  1. Bir eşleşme bulunana kadar mevcut konumdan sona kadar girişi arayın
  2. Eşleşen arama dizesini karşılık gelen değiştirme dizesiyle değiştirin
  3. Geçerli konumu, değiştirilen dizeden sonraki karaktere ayarla
  4. Tekrar et

Gibi bir işlev String.indexOfAny(String[]) -> int[]{index, whichString}yararlı olacaktır. İşte bir örnek (en verimli olanı değil):

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

Bazı testler:

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 tanıtımı IDEONE
tanıtımı, alternatif kod


1

Her zaman dizede başka hiçbir yerde görünmeyeceğinden emin olduğunuz bir sözcükle değiştirebilir ve daha sonra ikinci değişikliği yapabilirsiniz:

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

Bu gerçekleşirse bunun doğru çalışmadığını unutmayın "StringYouAreSureWillNeverOccur".


5
Bir StringThatCannotEverOccur oluşturarak Unicode Özel Kullanım Alanı, U + E000..U + F8FF karakterlerini kullanın. Girişte bulunmamaları gerektiğinden bunları önceden filtreleyebilirsiniz.
David Conrad

Veya dahili kullanım için ayrılmış olan "Karakter olmayan" U + FDD0..U + FDEF.
David Conrad

1

StringBuilder kullanmayı düşünün

Ardından dizini her dizenin başlayacağı yerde saklayın. Her konumda bir yer tutucu karakter kullanıyorsanız, karakteri kaldırın ve users dizesini ekleyin. Daha sonra, dize uzunluğunu başlangıç ​​konumuna ekleyerek bitiş konumunu eşleyebilirsiniz.

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

Sadece paylaşabileceğim kendi yöntemim.

Geçici String temp = "<?>";veyaString.Format();

Bu, konsol uygulamasında oluşturulan örnek kodumdur. - "Sadece Fikir, Tam Cevap Değil" .

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

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

Çıktı: time upon a Once, there was a bar and a foo.


Oldukça hileli. "_" Yerine geçmek istiyorsa ne yapacaksınız?
Pier-Alexandre Bouchard

İskele-AlexandreBouchard yöntemlerde @ ı değerini değiştirmek temparasından "_"içine <?>. Ancak gerekirse, yapabileceği şey sıcaklığı değiştirecek yönteme başka bir parametre eklemektir. - "Basit tutmak doğru mu?"
Leonel Sarmiento

Benim fikrim yon beklenen sonucu garanti edemez çünkü temp == yerine, yol çalışmaz.
Pier-Alexandre Bouchard

1

İşte kelime tabanlı benim sürüm:

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

Biraz zor bir yol ama biraz daha kontrol yapmanız gerekiyor.

1. dizeyi karakter dizisine dönüştür

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

Temp 2.loop ve değiştirme fooile barve barile footekrar değiştirilebilen dize alma şansının söz konusu olmadığını olarak.


1

Kısa cevap ...

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

Burada bulunan cevabı kullanarak değiştirmek istediğiniz dizelerin tüm tekrarlarını bulabilirsiniz.

Örneğin, yukarıdaki SO yanıtında kodu çalıştırın. İki dizin tablosu oluşturun (diyelim ki bar ve foo dizenizde yalnızca bir kez görünmüyor) ve bu tablolarla dizenizde yer değiştirerek çalışabilirsiniz.

Şimdi belirli dizin konumlarını değiştirmek için şunları kullanabilirsiniz:

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

Oysa posdizelerinizin başladığı dizin (yukarıda alıntıladığım dizin tablolarından). Diyelim ki her biri için iki dizin tablosu oluşturdunuz. Onları arayalım indexBarve indexFoo.

Şimdi onları değiştirirken, yapmak istediğiniz her değiştirme için bir tane olmak üzere iki döngü çalıştırabilirsiniz.

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

Benzer şekilde başka bir döngü indexFoo.

Bu, buradaki diğer cevaplar kadar etkili olmayabilir, ancak Haritalar'dan veya diğer şeylerden daha kolay anlaşılır.

Bu, her zaman istediğiniz sonucu ve her dizenin birden çok olası tekrarını verir. Her oluşumun dizinini sakladığınız sürece.

Ayrıca bu cevabın özyineleme veya dış bağımlılık gerektirmez. Karmaşıklık söz konusu olduğunda, muhtemelen O (n kare) iken, n her iki kelimenin oluşumlarının toplamıdır.


-1

Bu kodu sorunu çözecek geliştirdim:

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

Ana kullanımda change(story,word2,word1).


2
Sadece her bir dizenin tam olarak bir görünümü varsa işe yarayacaktır
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);
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.