Esiste un'alternativa alla stringa. Sostituisci maiuscole e minuscole?


306

Ho bisogno di cercare una stringa e sostituire tutte le occorrenze di %FirstName%e %PolicyAmount%con un valore estratto da un database. Il problema è che la capitalizzazione di FirstName varia. Questo mi impedisce di usare il String.Replace()metodo. Ho visto pagine web sull'argomento che suggeriscono

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

Tuttavia, per qualche motivo, quando provo a sostituire %PolicyAmount%con $0, la sostituzione non ha mai luogo. Presumo che abbia qualcosa a che fare con il simbolo del dollaro che è un personaggio riservato in regex.

Esiste un altro metodo che posso usare che non comporta la sanificazione dell'input per gestire i caratteri speciali regex?


1
Se "$ 0" è la variabile presente che non ha alcun impatto sulla regex.
cfeduke,

Risposte:


132

Da MSDN
$ 0 - "Sostituisce l'ultima sottostringa corrispondente al numero del numero di gruppo (decimale)."

Nel gruppo di espressioni regolari .NET il gruppo 0 è sempre l'intera corrispondenza. Per $ letterali è necessario

string value = Regex.Replace("%PolicyAmount%", "%PolicyAmount%", @"$$0", RegexOptions.IgnoreCase);

16
in questo caso particolare va bene, ma nei casi in cui le stringhe sono inserite dall'esterno, non si può essere sicuri che non contengano caratteri che significano qualcosa di speciale nelle espressioni regolari
Allanrbo

23
Dovresti evitare caratteri speciali come questo: string value = Regex.Replace ("% PolicyAmount%", Regex.Escape ("% PolicyAmount%"), Regex.Escape ("$ 0"), RegexOptions.IgnoreCase);
Helge Klein,

8
Fare attenzione quando si utilizza Regex.Escape in Regex.Replace. Dovrai sfuggire a tutte e tre le stringhe passate e chiamare Regex.Unescape sul risultato!
Holger Adam,

4
Secondo msdn: "Gli escape di caratteri sono riconosciuti in schemi di espressioni regolari ma non in schemi di sostituzione". ( msdn.microsoft.com/en-us/library/4edbef7e.aspx )
Bronek,

1
È meglio usare: string value = Regex.Replace ("% PolicyAmount%", Regex.Escape ("% PolicyAmount%"), "$ 0" .Replace ("$", "$$"), RegexOptions.IgnoreCase); poiché la sostituzione riconosce solo i segni dolar.
Skorek,

295

Sembra che string.Replace dovrebbe avere un sovraccarico che prende una StringComparisondiscussione. Dal momento che non lo è, potresti provare qualcosa del genere:

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

9
Bello. Vorrei cambiare ReplaceStringin Replace.
AMissico,

41
Accetto i commenti sopra. Questo può essere trasformato in un metodo di estensione con lo stesso nome di metodo. Inseriscilo semplicemente in una classe statica con la firma del metodo: stringa statica pubblica Sostituisci (questa stringa str, stringa oldValue, stringa newValue, confronto StringComparison)
Mark Robinson

8
@Helge, in generale, potrebbe andare bene, ma devo prendere stringhe arbitrarie dall'utente e non posso rischiare che l'input sia significativo per regex. Certo, immagino di poter scrivere un ciclo e mettere una barra rovesciata davanti a ogni personaggio ... A quel punto, potrei anche fare quanto sopra (IMHO).
Jim,

9
Durante questo test unitario mi sono imbattuto nel caso in cui non sarebbe mai tornato quando oldValue == newValue == "".
Ismaele,

10
Questo è un bug; ReplaceString("œ", "oe", "", StringComparison.InvariantCulture)genera ArgumentOutOfRangeException.
Michael Liu,

45

Tipo di un gruppo confuso di risposte, in parte perché il titolo della domanda è in realtà molto più grande della domanda specifica che viene posta. Dopo aver letto, non sono sicuro che una risposta sia a poche modifiche dall'assimilazione di tutte le cose buone qui, quindi ho pensato di provare a riassumere.

Ecco un metodo di estensione che penso evita le insidie ​​menzionate qui e fornisce la soluzione più ampiamente applicabile.

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

Così...

Sfortunatamente, il commento di @HA che hai a Escapetutti e tre non è corretto . Il valore iniziale e newValuenon deve essere.

Nota: tuttavia, è necessario sfuggire a $s nel nuovo valore che si sta inserendo se fanno parte di quello che sembrerebbe essere un marcatore di "valore acquisito" . Quindi i tre simboli del dollaro nel Regex.Replace all'interno del Regex.Replace [sic]. Senza quello, qualcosa di simile si rompe ...

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

Ecco l'errore:

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.

Ti dico una cosa, so che le persone che si sentono a proprio agio con Regex sentono che il loro uso evita errori, ma spesso sono ancora parziale alle stringhe di sniffer di byte (ma solo dopo aver letto Spolsky sulle codifiche ) per essere assolutamente sicuro di ottenere ciò che destinato a casi d'uso importanti. Mi ricorda un po 'Crockford su " espressioni regolari insicure ". Troppo spesso scriviamo regexps che consentono ciò che vogliamo (se siamo fortunati), ma consentiamo involontariamente di più in (ad esempio, è $10davvero una stringa "valore di acquisizione" valida nel mio nuovo valore regexp, sopra?) Perché non eravamo abbastanza premurosi . Entrambi i metodi hanno un valore ed entrambi incoraggiano diversi tipi di errori involontari. Spesso è facile sottovalutare la complessità.

Quella strana $fuga (e che Regex.Escapenon sfuggiva a modelli di valori catturati $0come mi sarei aspettato in valori di sostituzione) mi ha fatto impazzire per un po '. La programmazione è difficile (c) 1842


32

Ecco un metodo di estensione. Non sono sicuro di dove l'ho trovato.

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

}

Potrebbe essere necessario gestire i casi di stringa vuota / nulla.
Vad

2
Errori multipli in questa soluzione: 1. Controllare nullString, oldValue e newValue. 2. Non restituire orginalString (non funziona, i tipi semplici non vengono passati per riferimento), ma assegnare prima il valore di orginalValue a una nuova stringa e modificarlo e restituirlo.
RWC,

31

Sembra che il metodo più semplice sia semplicemente usare il metodo Sostituisci fornito con .Net ed è in circolazione da .Net 1.0:

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

Per utilizzare questo metodo, è necessario aggiungere un riferimento al gruppo Microsoft.VisualBasic. Questo assembly è una parte standard del runtime .Net, non è un download aggiuntivo o contrassegnato come obsoleto.


4
Funziona. È necessario aggiungere un riferimento all'assembly Microsoft.VisualBasic.
CleverPatrick,

Strano che questo metodo abbia avuto dei problemi quando l'ho usato (mancavano i caratteri all'inizio della riga). La risposta più popolare qui da ha C. Dragon 76funzionato come previsto.
Jeremy Thompson,

1
Il problema è che restituisce una NUOVA stringa anche se non viene effettuata una sostituzione, dove string.replace () restituisce un puntatore alla stessa stringa. Può diventare inefficiente se stai facendo qualcosa come una fusione di lettere di modulo.
Brain2000,

4
Brain2000, ti sbagli. Tutte le stringhe in .NET sono immutabili.
Der_Meister,

Der_Meister, sebbene ciò che dici sia corretto, ciò non rende sbagliato ciò che Brain2000 ha detto.
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);
    }

Qual è il modo migliore? che dire di stackoverflow.com/a/244933/206730 ? prestazioni migliori?
Kiquenet,

8

Ispirato dalla risposta di cfeduke, ho creato questa funzione che utilizza IndexOf per trovare il vecchio valore nella stringa e poi lo sostituisce con il nuovo valore. L'ho usato in uno script SSIS che elabora milioni di righe e il metodo regex era molto più lento di così.

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

+1 per non usare regex quando non è necessario. Certo, usi qualche altra riga di codice, ma è molto più efficiente della sostituzione basata su regex a meno che tu non abbia bisogno della funzionalità $.
ChrisG,

6

Espandendo la risposta popolare di C. Dragon 76 trasformando il suo codice in un'estensione che sovraccarica il Replacemetodo predefinito .

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

3

Basato sulla risposta di Jeff Reddy, con alcune ottimizzazioni e convalide:

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

2

una versione simile a quella di C. Dragon, ma se hai bisogno di una sola sostituzione:

int n = myText.IndexOf(oldValue, System.StringComparison.InvariantCultureIgnoreCase);
if (n >= 0)
{
    myText = myText.Substring(0, n)
        + newValue
        + myText.Substring(n + oldValue.Length);
}

1

Ecco un'altra opzione per eseguire sostituzioni Regex, poiché non molte persone sembrano notare che le corrispondenze contengono la posizione all'interno della stringa:

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

Potresti spiegare perché stai moltiplicando per MatchNo?
Aheho,

Se c'è una differenza di lunghezza tra oldValue e newValue, la stringa diventerà più lunga o più corta quando si sostituiscono i valori. match.Index si riferisce alla posizione originale all'interno della stringa, dobbiamo regolare il movimento delle posizioni a causa della nostra sostituzione. Un altro approccio sarebbe quello di eseguire il Rimuovi / Inserisci da destra a sinistra.
Brandon,

Capisco quello. Ecco a cosa serve la variabile "offset". Quello che non capisco è perché stai moltiplicando per matchNo. La mia intuizione mi dice che la posizione di una corrispondenza all'interno di una stringa non avrebbe alcuna relazione con il conteggio effettivo delle occorrenze precedenti.
Aheho,

Non importa, ora capisco. L'offset deve essere ridimensionato in base al numero di occorrenze. Se stai perdendo 2 caratteri ogni volta che devi effettuare una sostituzione, devi tenerne conto quando calcoli i parametri con il metodo di rimozione
Aheho

0
Regex.Replace(strInput, strToken.Replace("$", "[$]"), strReplaceWith, RegexOptions.IgnoreCase);

3
Questo non funziona $ Non è nel token. È nello strReplace With string.
Aheho,

9
E non puoi adattarlo per quello?
Joel Coehoorn,

18
Questo sito dovrebbe essere un repository per risposte corrette. Non risposte quasi corrette.
Aheho,

0

Il metodo di espressione regolare dovrebbe funzionare. Tuttavia, ciò che è possibile fare è anche la stringa minuscola dal database, in minuscolo le variabili%% disponibili, quindi individuare le posizioni e le lunghezze nella stringa con lettere minuscole dal database. Ricorda, le posizioni in una stringa non cambiano solo perché ha il case inferiore.

Quindi usando un ciclo che va al contrario (è più facile, in caso contrario dovrete tenere un conteggio progressivo di dove si spostano i punti successivi) rimuovere dalla vostra stringa non inferiore dal database le variabili%% dalla loro posizione e lunghezza e inserire i valori di sostituzione.


Al contrario, intendo elaborare le posizioni trovate al contrario dal più lontano al più breve, non attraversare la stringa dal database al contrario.
cfeduke,

Potresti, o potresti semplicemente usare il Regex :)
Ray,

0

(Dal momento che tutti ci stanno provando). Ecco la mia versione (con controlli nulli e fuga di input e sostituzione corretti) ** Ispirato da Internet e da altre versioni:

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

Uso:

var result = "This is a test".ReplaceIgnoreCase("IS", "was");

0

Lasciami fare il mio caso e poi puoi farmi a brandelli se vuoi.

Regex non è la risposta a questo problema: troppo lento e affamato di memoria, relativamente parlando.

StringBuilder è molto meglio della manipolazione delle stringhe.

Dal momento che questo sarà un metodo di estensione da integrare string.Replace, ritengo sia importante abbinare il modo in cui funziona, pertanto è importante generare eccezioni per gli stessi argomenti, in quanto è necessario restituire la stringa originale se non è stata effettuata una sostituzione.

Credo che avere un parametro StringComparison non sia una buona idea. L'ho provato ma il test case originariamente menzionato da michael-liu ha mostrato un problema:

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

Sebbene IndexOf corrisponderà, esiste una discrepanza tra la lunghezza della corrispondenza nella stringa di origine (1) e oldValue.Length (2). Ciò si è manifestato causando IndexOutOfRange in alcune altre soluzioni quando oldValue.Length è stato aggiunto alla posizione di corrispondenza corrente e non sono riuscito a trovare un modo per aggirare questo. Regex non riesce a eguagliare il caso, quindi ho preso la soluzione pragmatica di usare solo StringComparison.OrdinalIgnoreCaseper la mia soluzione.

Il mio codice è simile ad altre risposte, ma la mia svolta è che cerco una corrispondenza prima di andare nel guaio di creare un StringBuilder. Se non viene trovato nessuno, viene evitata un'allocazione potenzialmente grande. Il codice diventa quindi un do{...}whileanziché unwhile{...}

Ho fatto alcuni test approfonditi contro altre Risposte e questo è risultato molto più veloce e ho usato un po 'meno memoria.

    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();
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.