Czy istnieje alternatywa dla string.Replace, która nie rozróżnia wielkości liter?

306

Muszę poszukać ciąg i zastąpić wszystkie wystąpienia %FirstName%i %PolicyAmount%o wartości pobierane z bazy danych. Problem polega na tym, że wielkość liter w FirstName jest różna. To uniemożliwia mi użycie tej String.Replace()metody. Widziałem strony internetowe na ten temat, które sugerują

Regex.Replace(strInput, strToken, strReplaceWith, RegexOptions.IgnoreCase);

Jednak z jakiegoś powodu, gdy próbuję i zastąpić %PolicyAmount%ze $0nigdy wymiana odbywa. Zakładam, że ma to coś wspólnego ze znakiem dolara będącym zarezerwowaną postacią w wyrażeniu regularnym.

Czy mogę zastosować inną metodę, która nie wymaga dezynfekcji danych wejściowych w celu radzenia sobie ze znakami specjalnymi wyrażeń regularnych?

Aheho
źródło
1
Jeśli „0” jest zmienną wchodzącą, nie ma to żadnego wpływu na wyrażenie regularne.
cfeduke

Odpowiedzi:

132

Od MSDN
0 USD - „Zastępuje ostatni podciąg zgodny z numerem grupy (dziesiętnym)”.

W .NET Wyrażenia regularne grupa 0 jest zawsze całym dopasowaniem. Aby uzyskać dosłowny $ musisz

string value = Regex.Replace("%PolicyAmount%", "%PolicyAmount%", @"$$0", RegexOptions.IgnoreCase);
Todd White
źródło
16
w tym konkretnym przypadku jest to w porządku, ale w przypadkach, gdy łańcuchy są wprowadzane z zewnątrz, nie można mieć pewności, że nie zawierają one znaków, które oznaczają coś specjalnego w wyrażeniach regularnych
Allanrbo
23
Powinieneś unikać znaków specjalnych, takich jak: wartość ciągu = Regex.Replace („% PolicyAmount%”, Regex.Escape („% PolicyAmount%”), Regex.Escape („0 $”), RegexOptions.IgnoreCase);
Helge Klein
8
Uważaj podczas korzystania z Regex.Escape w Regex.Replace. Będziesz musiał uciec od wszystkich trzech przekazanych ciągów i wywołać wynik Regex.Unescape!
Holger Adam,
4
Zgodnie z msdn: „znaki specjalne są rozpoznawane we wzorcach wyrażeń regularnych, ale nie we wzorcach zastępczych.” ( msdn.microsoft.com/en-us/library/4edbef7e.aspx )
Bronek
1
Najlepiej użyć: wartość ciągu = Regex.Replace („% PolicyAmount%”, Regex.Escape („% PolicyAmount%”), „$ 0” .Replace („$”, „$$”), RegexOptions.IgnoreCase); jako zamiennik rozpoznaje tylko znaki dolara.
Skorek
295

Wydaje się, że string.Replace powinien mieć przeciążenie, które wymaga StringComparisonargumentu. Ponieważ tak nie jest, możesz spróbować czegoś takiego:

public static string ReplaceString(string str, string oldValue, string newValue, StringComparison comparison)
{
    StringBuilder sb = new StringBuilder();

    int previousIndex = 0;
    int index = str.IndexOf(oldValue, comparison);
    while (index != -1)
    {
        sb.Append(str.Substring(previousIndex, index - previousIndex));
        sb.Append(newValue);
        index += oldValue.Length;

        previousIndex = index;
        index = str.IndexOf(oldValue, index, comparison);
    }
    sb.Append(str.Substring(previousIndex));

    return sb.ToString();
}
C. Smok 76
źródło
9
Miły. Zmieniłbym się ReplaceStringna Replace.
AMissico,
41
Zgadzam się z powyższymi komentarzami. Można to przekształcić w metodę rozszerzenia o tej samej nazwie. Po prostu umieść go w klasie statycznej z podpisem metody: publiczny ciąg statyczny Zamień (ten ciąg znaków, ciąg znaków oldValue, ciąg znaków newValue, porównanie StringComparison)
Mark Robinson
8
@Helge, ogólnie rzecz biorąc, może to być w porządku, ale muszę pobrać dowolne ciągi od użytkownika i nie mogę ryzykować, że dane wejściowe będą miały znaczenie dla wyrażenia regularnego. Oczywiście myślę, że mógłbym napisać pętlę i umieścić odwrotny ukośnik przed każdą postacią ... W tym momencie równie dobrze mogę zrobić powyższe (IMHO).
Jim
9
Podczas testów jednostkowych natknąłem się na przypadek, w którym nigdy nie wróci oldValue == newValue == "".
Ishmael
10
To jest błędne; ReplaceString("œ", "oe", "", StringComparison.InvariantCulture)rzuca ArgumentOutOfRangeException.
Michael Liu,
45

Rodzaj mylącej grupy odpowiedzi, po części dlatego, że tytuł pytania jest w rzeczywistości znacznie większy niż zadawane pytanie szczegółowe. Po przeczytaniu nie jestem pewien, czy odpowiedź jest kilka zmian od przyswojenia wszystkich dobrych rzeczy tutaj, więc pomyślałem, że spróbuję podsumować.

Oto metoda rozszerzenia, która moim zdaniem pozwala uniknąć wspomnianych tutaj pułapek i zapewnia najszerzej stosowane rozwiązanie.

public static string ReplaceCaseInsensitiveFind(this string str, string findMe,
    string newValue)
{
    return Regex.Replace(str,
        Regex.Escape(findMe),
        Regex.Replace(newValue, "\\$[0-9]+", @"$$$0"),
        RegexOptions.IgnoreCase);
}

Więc...

Niestety, komentarz @HA, który masz do Escapewszystkich trzech, jest niepoprawny . Wartość początkowa i newValuenie musi być.

Uwaga: Musisz jednak uciec $s w nowej wartości, którą wstawiasz, jeśli są częścią czegoś, co wydaje się być znacznikiem „przechwyconej wartości” . Zatem trzy znaki dolara w Regex.Replace wewnątrz Regex.Replace [sic]. Bez tego coś takiego się psuje ...

"This is HIS fork, hIs spoon, hissssssss knife.".ReplaceCaseInsensitiveFind("his", @"he$0r")

Oto błąd:

An unhandled exception of type 'System.ArgumentException' occurred in System.dll

Additional information: parsing "The\hisr\ is\ he\HISr\ fork,\ he\hIsr\ spoon,\ he\hisrsssssss\ knife\." - Unrecognized escape sequence \h.

Powiem ci co, wiem, że ludzie, którzy czują się komfortowo z Regex, czują, że ich użycie pozwala uniknąć błędów, ale często wciąż jestem stronniczy w bajtowaniu ciągów wąchania (ale dopiero po przeczytaniu Spolskyego na temat kodowania ), aby mieć absolutną pewność, że otrzymujesz to, co masz przeznaczony do ważnych zastosowań. Trochę przypomina mi Crockforda o „ niepewnych wyrażeniach regularnych ”. Zbyt często piszemy wyrażenia regularne, które pozwalają na to, czego chcemy (jeśli mamy szczęście), ale przypadkowo dopuszczają więcej (np. Czy $10naprawdę jest prawidłowym ciągiem „wartość przechwytywania” w moim nowym wyrażeniu regularnym powyżej?), Ponieważ nie byliśmy wystarczająco rozważni . Obie metody mają wartość i obie zachęcają do różnego rodzaju niezamierzonych błędów. Często łatwo jest nie docenić złożoności.

Ta dziwna $ucieczka (i która Regex.Escapenie uciekła z przechwyconych wzorców wartości, tak $0jak bym się spodziewał po wartościach zastępczych) doprowadziła mnie na chwilę do szaleństwa. Programowanie jest trudne (c) 1842

bułka z masłem
źródło
32

Oto metoda rozszerzenia. Nie jestem pewien, gdzie to znalazłem.

public static class StringExtensions
{
    public static string Replace(this string originalString, string oldValue, string newValue, StringComparison comparisonType)
    {
        int startIndex = 0;
        while (true)
        {
            startIndex = originalString.IndexOf(oldValue, startIndex, comparisonType);
            if (startIndex == -1)
                break;

            originalString = originalString.Substring(0, startIndex) + newValue + originalString.Substring(startIndex + oldValue.Length);

            startIndex += newValue.Length;
        }

        return originalString;
    }

}
rboarman
źródło
Może być konieczne obsłużenie pustych / pustych przypadków ciągów.
Vad
2
Wielokrotne błędy w tym rozwiązaniu: 1. Sprawdź, czy wartość originalString, oldValue i newValue ma wartość null. 2. Nie zwracaj orginalString z powrotem (nie działa, proste typy nie są przekazywane przez referencję), ale najpierw przypisz wartość orginalValue do nowego łańcucha i zmodyfikuj go i zwróć.
RWC
31

Wydaje się, że najłatwiejszą metodą jest po prostu użycie metody Zamień, która jest dostarczana z .Net i jest dostępna od .Net 1.0:

string res = Microsoft.VisualBasic.Strings.Replace(res, 
                                   "%PolicyAmount%", 
                                   "$0", 
                                   Compare: Microsoft.VisualBasic.CompareMethod.Text);

Aby użyć tej metody, musisz dodać odwołanie do zestawu Microsoft.VisualBasic. Ten zestaw jest standardową częścią środowiska wykonawczego .Net, nie jest dodatkowym plikiem do pobrania ani oznaczony jako przestarzały.

CleverPatrick
źródło
4
To działa. Musisz dodać odwołanie do zestawu Microsoft.VisualBasic.
CleverPatrick
Dziwne, że ta metoda miała pewne problemy, kiedy jej użyłem (znaki na początku linii zaginęły). Najpopularniejsza odpowiedź tutaj C. Dragon 76działała zgodnie z oczekiwaniami.
Jeremy Thompson
1
Problem polega na tym, że zwraca NOWY ciąg, nawet jeśli nie dokonano zamiany, gdzie string.replace () zwraca wskaźnik do tego samego ciągu. Może stać się nieefektywny, jeśli robisz coś jak scalanie listów formularzy.
Brain2000
4
Brain2000, mylisz się. Wszystkie ciągi w .NET są niezmienne.
Der_Meister
Der_Meister, podczas gdy to, co mówisz jest poprawne, nie czyni tego, co Brain2000 powiedział źle.
Simon Hewitt
11
    /// <summary>
    /// A case insenstive replace function.
    /// </summary>
    /// <param name="originalString">The string to examine.(HayStack)</param>
    /// <param name="oldValue">The value to replace.(Needle)</param>
    /// <param name="newValue">The new value to be inserted</param>
    /// <returns>A string</returns>
    public static string CaseInsenstiveReplace(string originalString, string oldValue, string newValue)
    {
        Regex regEx = new Regex(oldValue,
           RegexOptions.IgnoreCase | RegexOptions.Multiline);
        return regEx.Replace(originalString, newValue);
    }
Karl Glennon
źródło
Który lepszy sposób? co jest z stackoverflow.com/a/244933/206730 ? lepsza wydajność?
Kiquenet,
8

Zainspirowany odpowiedzią cfeduke, stworzyłem tę funkcję, która używa IndexOf do znalezienia starej wartości w ciągu, a następnie zastępuje ją nową wartością. Użyłem tego w skrypcie SSIS przetwarzającym miliony wierszy, a metoda wyrażenia regularnego była znacznie wolniejsza niż ta.

public static string ReplaceCaseInsensitive(this string str, string oldValue, string newValue)
{
    int prevPos = 0;
    string retval = str;
    // find the first occurence of oldValue
    int pos = retval.IndexOf(oldValue, StringComparison.InvariantCultureIgnoreCase);

    while (pos > -1)
    {
        // remove oldValue from the string
        retval = retval.Remove(pos, oldValue.Length);

        // insert newValue in it's place
        retval = retval.Insert(pos, newValue);

        // check if oldValue is found further down
        prevPos = pos + newValue.Length;
        pos = retval.IndexOf(oldValue, prevPos, StringComparison.InvariantCultureIgnoreCase);
    }

    return retval;
}
JeroenV
źródło
+1 za nieużywanie wyrażenia regularnego, gdy nie jest to konieczne. Jasne, używasz jeszcze kilku wierszy kodu, ale jest on znacznie bardziej wydajny niż zastępowanie oparte na wyrażeniach regularnych, chyba że potrzebujesz funkcji $.
ChrisG
6

Rozwijając popularną odpowiedź C. Dragon 76 , zmieniając jego kod w rozszerzenie, które przeciąża domyślną Replacemetodę.

public static class StringExtensions
{
    public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
    {
        StringBuilder sb = new StringBuilder();

        int previousIndex = 0;
        int index = str.IndexOf(oldValue, comparison);
        while (index != -1)
        {
            sb.Append(str.Substring(previousIndex, index - previousIndex));
            sb.Append(newValue);
            index += oldValue.Length;

            previousIndex = index;
            index = str.IndexOf(oldValue, index, comparison);
        }
        sb.Append(str.Substring(previousIndex));
        return sb.ToString();
     }
}
Chad Kuehn
źródło
3

Na podstawie odpowiedzi Jeffa Reddy'ego, z pewnymi optymalizacjami i walidacjami:

public static string Replace(string str, string oldValue, string newValue, StringComparison comparison)
{
    if (oldValue == null)
        throw new ArgumentNullException("oldValue");
    if (oldValue.Length == 0)
        throw new ArgumentException("String cannot be of zero length.", "oldValue");

    StringBuilder sb = null;

    int startIndex = 0;
    int foundIndex = str.IndexOf(oldValue, comparison);
    while (foundIndex != -1)
    {
        if (sb == null)
            sb = new StringBuilder(str.Length + (newValue != null ? Math.Max(0, 5 * (newValue.Length - oldValue.Length)) : 0));
        sb.Append(str, startIndex, foundIndex - startIndex);
        sb.Append(newValue);

        startIndex = foundIndex + oldValue.Length;
        foundIndex = str.IndexOf(oldValue, startIndex, comparison);
    }

    if (startIndex == 0)
        return str;
    sb.Append(str, startIndex, str.Length - startIndex);
    return sb.ToString();
}
Mark Cranness
źródło
2

wersja podobna do C. Dragon's, ale jeśli potrzebujesz tylko jednego zamiennika:

int n = myText.IndexOf(oldValue, System.StringComparison.InvariantCultureIgnoreCase);
if (n >= 0)
{
    myText = myText.Substring(0, n)
        + newValue
        + myText.Substring(n + oldValue.Length);
}
Allanrbo
źródło
1

Oto kolejna opcja wykonania zamiany Regex, ponieważ wydaje się, że niewiele osób zauważa, że ​​dopasowania zawierają lokalizację w ciągu:

    public static string ReplaceCaseInsensative( this string s, string oldValue, string newValue ) {
        var sb = new StringBuilder(s);
        int offset = oldValue.Length - newValue.Length;
        int matchNo = 0;
        foreach (Match match in Regex.Matches(s, Regex.Escape(oldValue), RegexOptions.IgnoreCase))
        {
            sb.Remove(match.Index - (offset * matchNo), match.Length).Insert(match.Index - (offset * matchNo), newValue);
            matchNo++;
        }
        return sb.ToString();
    }
Brandon
źródło
Czy możesz wyjaśnić, dlaczego pomnażasz przez MatchNo?
Aheho,
Jeśli istnieje różnica w długości między oldValue i newValue, ciąg będzie dłuższy lub krótszy podczas zamiany wartości. match.Index odnosi się do oryginalnej lokalizacji w ciągu, musimy skorygować ruch tej pozycji z powodu naszej zamiany. Innym podejściem byłoby wykonanie operacji Usuń / Wstaw od prawej do lewej.
Brandon,
Rozumiem. Do tego służy zmienna „offset”. Nie rozumiem, dlaczego mnożymy przez matchNo. Moja intuicja mówi mi, że lokalizacja dopasowania w ciągu nie miałaby związku z faktyczną liczbą wcześniejszych wystąpień.
Aheho,
Nieważne, rozumiem teraz. Przesunięcie należy skalować na podstawie liczby wystąpień. Jeśli tracisz 2 znaki za każdym razem, gdy musisz dokonać zamiany, musisz to uwzględnić przy obliczaniu parametrów metody usuwania
Aheho
0
Regex.Replace(strInput, strToken.Replace("$", "[$]"), strReplaceWith, RegexOptions.IgnoreCase);
Joel Coehoorn
źródło
3
To nie działa $ Nie znajduje się w tokenie. Jest w strReplace With string.
Aheho
9
I nie możesz tego do tego przystosować?
Joel Coehoorn
18
Ta strona ma być repozytorium poprawnych odpowiedzi. Brak odpowiedzi, które są prawie poprawne.
Aheho
0

Metoda wyrażeń regularnych powinna działać. Jednak to, co możesz zrobić, to ciąg znaków z bazy danych małymi literami, małe zmienne%, które masz, a następnie zlokalizuj pozycje i długości w łańcuchu małych liter z bazy danych. Pamiętaj, że pozycje w ciągu nie zmieniają się tylko dlatego, że są małe.

Następnie za pomocą pętli, która idzie w odwrotnym kierunku (jest łatwiej, jeśli nie musisz, musisz mieć bieżącą liczbę miejsc, do których przenoszą się późniejsze punkty), usuń z bazy danych ciąg znaków innych niż małe litery z bazy danych zmienne% według ich pozycji i długość i wstaw wartości zastępcze.

cfeduke
źródło
Przez odwrotność mam na myśli przetwarzanie znalezionych lokalizacji w odwrotnej kolejności od najdalszego do najkrótszego, a nie przechodzenie przez ciąg znaków z bazy danych w odwrotnej kolejności.
cfeduke
Możesz, lub możesz po prostu skorzystać z Regexu :)
Ray
0

(Ponieważ wszyscy to robią). Oto moja wersja (z zerowymi testami oraz poprawnym wprowadzaniem i zastępowaniem znaków zastępczych) ** Inspirowane z internetu i innych wersji:

using System;
using System.Text.RegularExpressions;

public static class MyExtensions {
    public static string ReplaceIgnoreCase(this string search, string find, string replace) {
        return Regex.Replace(search ?? "", Regex.Escape(find ?? ""), (replace ?? "").Replace("$", "$$"), RegexOptions.IgnoreCase);          
    }
}

Stosowanie:

var result = "This is a test".ReplaceIgnoreCase("IS", "was");
Fredrik Johansson
źródło
0

Pozwól, że przedstawię moją sprawę, a jeśli chcesz, możesz mnie rozerwać na strzępy.

Regex nie jest odpowiedzią na ten problem - relatywnie zbyt wolny i głodny pamięci.

StringBuilder jest znacznie lepszy niż string-string.

Ponieważ będzie to metoda rozszerzenia w celu uzupełnienia string.Replace, uważam, że ważne jest dopasowanie sposobu, w jaki to działa - dlatego ważne jest zgłaszanie wyjątków dla tych samych problemów argumentów, podobnie jak zwracanie oryginalnego ciągu, jeśli nie dokonano zamiany.

Uważam, że posiadanie parametru StringComparison nie jest dobrym pomysłem. Próbowałem, ale przypadek testowy pierwotnie wspomniany przez Michaela-Liu wykazał problem:

[TestCase("œ", "oe", "", StringComparison.InvariantCultureIgnoreCase, Result = "")]

Podczas gdy IndexOf będzie pasować, istnieje niedopasowanie między długością dopasowania w ciągu źródłowym (1) a oldValue.Length (2). Przejawiało się to przez spowodowanie IndexOutOfRange w niektórych innych rozwiązaniach, gdy oldValue.Length został dodany do bieżącej pozycji dopasowania i nie mogłem znaleźć sposobu na obejście tego. Regex i tak nie pasuje do przypadku, więc wybrałem pragmatyczne rozwiązanie polegające na użyciu tylko StringComparison.OrdinalIgnoreCasedla mojego rozwiązania.

Mój kod jest podobny do innych odpowiedzi, ale moim zdziwieniem jest to, że szukam dopasowania, zanim podejmę trud tworzenia StringBuilder. Jeśli nie zostanie znaleziony, można uniknąć potencjalnie dużej alokacji. Kod staje się następnie do{...}whilezamiastwhile{...}

Zrobiłem kilka obszernych testów w stosunku do innych odpowiedzi, które pojawiły się ułamkowo szybciej i zużyły nieco mniej pamięci.

    public static string ReplaceCaseInsensitive(this string str, string oldValue, string newValue)
    {
        if (str == null) throw new ArgumentNullException(nameof(str));
        if (oldValue == null) throw new ArgumentNullException(nameof(oldValue));
        if (oldValue.Length == 0) throw new ArgumentException("String cannot be of zero length.", nameof(oldValue));

        var position = str.IndexOf(oldValue, 0, StringComparison.OrdinalIgnoreCase);
        if (position == -1) return str;

        var sb = new StringBuilder(str.Length);

        var lastPosition = 0;

        do
        {
            sb.Append(str, lastPosition, position - lastPosition);

            sb.Append(newValue);

        } while ((position = str.IndexOf(oldValue, lastPosition = position + oldValue.Length, StringComparison.OrdinalIgnoreCase)) != -1);

        sb.Append(str, lastPosition, str.Length - lastPosition);

        return sb.ToString();
    }
Simon Hewitt
źródło