Existe uma alternativa para string.Replace que não diferencia maiúsculas de minúsculas?

306

Preciso pesquisar uma string e substituir todas as ocorrências de %FirstName%e %PolicyAmount%com um valor extraído de um banco de dados. O problema é que a capitalização do Nome varia. Isso me impede de usar o String.Replace()método Eu vi páginas da web sobre o assunto que sugerem

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

No entanto, por algum motivo, quando eu tentar substituir %PolicyAmount%com $0, a substituição não ocorre. Suponho que isso tenha algo a ver com o cifrão sendo um caractere reservado na regex.

Existe outro método que eu possa usar que não envolva a limpeza da entrada para lidar com caracteres especiais de expressão regular?

Aheho
fonte
1
Se "$ 0" for a variável que entra, isso não afeta a regex.
cfeduke 28/10/08

Respostas:

132

Do MSDN
$ 0 - "Substitui a última substring correspondente ao número do número do grupo (decimal)."

No .NET, o grupo 0 de expressões regulares é sempre a correspondência inteira. Para um $ literal, você precisa

string value = Regex.Replace("%PolicyAmount%", "%PolicyAmount%", @"$$0", RegexOptions.IgnoreCase);
Todd White
fonte
16
neste caso particular isso é bom, mas nos casos em que as cordas são de entrada do lado de fora, não se pode ter certeza que eles não contêm caracteres que significam algo especial em expressões regulares
Allanrbo
23
Você deve escapar de caracteres especiais como este: string value = Regex.Replace ("% PolicyAmount%", Regex.Escape ("% PolicyAmount%"), Regex.Escape ("$ 0"), RegexOptions.IgnoreCase);
Helge Klein
8
Cuidado ao usar o Regex.Escape no Regex.Replace. Você terá que escapar de todas as três seqüências passadas e chamar Regex.Unescape no resultado!
precisa
4
De acordo com o msdn: "Escapes de caracteres são reconhecidos em padrões de expressão regular, mas não em padrões de substituição". ( msdn.microsoft.com/en-us/library/4edbef7e.aspx )
Bronek
1
É melhor usar: string value = Regex.Replace ("% PolicyAmount%", Regex.Escape ("% PolicyAmount%"), "$ 0" .Replace ("$", "$$"), RegexOptions.IgnoreCase); como substituição reconhece apenas sinais dolar.
Skorek
295

Parece que string.Replace deve ter uma sobrecarga que requer um StringComparisonargumento. Como não, você pode tentar algo como isto:

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. Dragão 76
fonte
9
Agradável. Eu mudaria ReplaceStringpara Replace.
AMissico
41
Concorde com os comentários acima. Isso pode ser transformado em um método de extensão com o mesmo nome de método. Basta colocá-la em uma classe estático com a assinatura do método: public static string Substituir (este str String, cordas oldValue, corda newValue, comparação StringComparison)
Mark Robinson
8
@ Helge, em geral, isso pode ser bom, mas eu tenho que pegar seqüências arbitrárias do usuário e não posso arriscar que a entrada seja significativa para regex. Claro, acho que eu poderia escrever um loop e colocar uma barra invertida na frente de todo e qualquer personagem ... Nesse ponto, eu também poderia fazer o acima (IMHO).
Jim
9
Durante o teste de unidade, deparei-me com o caso em que nunca retornaria quando oldValue == newValue == "".
Ishmael
10
Isso é buggy; ReplaceString("œ", "oe", "", StringComparison.InvariantCulture)joga ArgumentOutOfRangeException.
Michael Liu
45

Um tipo de grupo confuso de respostas, em parte porque o título da pergunta é realmente muito maior do que a pergunta específica que está sendo feita. Depois de ler, não tenho certeza se alguma resposta está a algumas edições de assimilar todas as coisas boas aqui, então imaginei que tentaria resumir.

Aqui está um método de extensão que eu acho que evita as armadilhas mencionadas aqui e fornece a solução mais amplamente aplicável.

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

Assim...

Infelizmente, o comentário do @HA de que você tem Escapetodos os três não está correto . O valor inicial enewValue não precisa ser.

Nota: você precisa, no entanto, escapar $s no novo valor que está inserindo se eles fizerem parte do que pareceria ser um marcador de "valor capturado" . Assim, os três cifrões no Regex.Replace dentro do Regex.Replace [sic]. Sem isso, algo assim quebra ...

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

Aqui está o erro:

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.

Para dizer a você, sei que as pessoas que se sentem confortáveis ​​com o Regex sentem que seu uso evita erros, mas muitas vezes ainda pareço de byte farejando seqüências de caracteres (mas só depois de ler Spolsky nas codificações ) para ter certeza absoluta de que está conseguindo o que quer. destinado a casos de uso importantes. Lembra-me de Crockford em " um pouco expressões regulares inseguras ". Com muita freqüência, escrevemos regexps que permitem o que queremos (se tivermos sorte), mas involuntariamente permitimos mais entradas (por exemplo, é $10realmente uma cadeia de "valor de captura" válida no meu novo regexp newValue, acima?) Porque não fomos atenciosos o suficiente . Ambos os métodos têm valor e encorajam diferentes tipos de erros não intencionais. Geralmente é fácil subestimar a complexidade.

Essa $fuga estranha (e que Regex.Escapenão escapou dos padrões de valores capturados $0como eu esperaria em valores de substituição) me deixou louca por um tempo. A programação é difícil (c) 1842

ruffin
fonte
32

Aqui está um método de extensão. Não sei onde o encontrei.

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
fonte
Pode ser necessário lidar com casos de seqüência de caracteres vazios / nulos.
Vad
2
Erros múltiplos na solução: 1. Verifique se originalString, oldValue e newValue são nulos. 2. Não retorne orginalString (não funciona, tipos simples não são passados ​​por referência), mas atribua o valor de orginalValue primeiro a uma nova string, modifique-a e devolva-a.
RWC 07/01
31

Parece que o método mais fácil é simplesmente usar o método Replace que acompanha o .NET e existe desde o .NET 1.0:

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

Para usar esse método, você deve adicionar uma Referência ao Microsoft.VisualBasic em conjunto. Este assembly é uma parte padrão do tempo de execução .Net, não é um download extra ou está marcado como obsoleto.

CleverPatrick
fonte
4
Funciona. Você precisa adicionar uma referência ao assembly Microsoft.VisualBasic.
CleverPatrick
Estranho que esse método tenha tido alguns problemas quando o usei (caracteres no início da linha desapareceram). A resposta mais popular aqui C. Dragon 76funcionou como esperado.
21815 Jeremy Thompson
1
O problema é que ele retorna uma NOVA string, mesmo que não seja feita uma substituição, onde string.replace () retorna um ponteiro para a mesma string. Pode ficar ineficiente se você estiver fazendo algo como uma mesclagem de cartas.
precisa saber é o seguinte
4
Brain2000, você está errado. Todas as strings no .NET são imutáveis.
Der_Meister
Der_Meister, enquanto o que você diz está correto, não faz o que o Brain2000 disse errado.
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
fonte
Qual é o melhor caminho? O que há sobre stackoverflow.com/a/244933/206730 ? melhor performance?
Kiquenet
8

Inspirado pela resposta de cfeduke, criei esta função que usa IndexOf para encontrar o valor antigo na string e depois o substitui pelo novo valor. Eu usei isso em um script SSIS processando milhões de linhas, e o método regex era muito mais lento que isso.

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
fonte
+1 por não usar regex quando não for necessário. Claro, você usa mais algumas linhas de código, mas é muito mais eficiente que a substituição baseada em regex, a menos que você precise da funcionalidade $.
ChrisG
6

Expandindo a resposta popular de C. Dragon 76 , transformando seu código em uma extensão que sobrecarrega o Replacemétodo padrão .

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
fonte
3

Com base na resposta de Jeff Reddy, com algumas otimizações e validações:

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
fonte
2

uma versão semelhante à C. Dragon's, mas se você precisar apenas de uma única substituição:

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

Aqui está outra opção para executar substituições de Regex, pois poucas pessoas parecem perceber que as correspondências contêm o local dentro da cadeia:

    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
fonte
Você poderia explicar por que está multiplicando pelo MatchNo?
Aheho 15/08/14
Se houver uma diferença de comprimento entre o oldValue e o newValue, a cadeia ficará mais longa ou mais curta à medida que você substitui os valores. match.Index refere-se ao local original dentro da string, precisamos ajustar o movimento dessa posição devido à nossa substituição. Outra abordagem seria executar a opção Remover / Inserir da direita para a esquerda.
Brandon
Entendi. É para isso que serve a variável "offset". O que não entendo é por que você está multiplicando por matchNo. Minha intuição me diz que a localização de uma correspondência dentro de uma sequência não teria relação com a contagem real de ocorrências anteriores.
Aheho 15/08/14
Não importa, eu entendi agora. O deslocamento precisa ser escalado com base no número de ocorrências. Se você está perdendo 2 caracteres cada vez que você precisa fazer uma substituição, você precisa de conta para que ao calcular os parâmetros para o método remove
Aheho
0
Regex.Replace(strInput, strToken.Replace("$", "[$]"), strReplaceWith, RegexOptions.IgnoreCase);
Joel Coehoorn
fonte
3
Isso não funciona. O $ não está no token. Está na string strReplace With.
Aheho 28/10/08
9
E você não pode adaptá-lo para isso?
Joel Coehoorn
18
Este site deve ser um repositório para respostas corretas. Não são respostas quase corretas.
Aheho 28/10/08
0

O método de expressão regular deve funcionar. No entanto, o que você também pode fazer é minúscula a sequência do banco de dados, minúscula a% de variáveis% que você possui e, em seguida, localize as posições e comprimentos na sequência minúscula do banco de dados. Lembre-se, as posições em uma corda não mudam apenas porque é mais baixa.

Em seguida, usando um loop que é inverso (é mais fácil, se você não tiver, você terá que manter uma contagem contínua de onde os pontos posteriores se deslocam) remova da sua seqüência de caracteres não inferior do banco de dados as variáveis%% por sua posição e comprimento e insira os valores de substituição.

cfeduke
fonte
Por reverso, refiro-me a processar os locais encontrados no sentido inverso, do mais extremo ao mais curto, e não atravessar a string do banco de dados em sentido inverso.
cfeduke 28/10/08
Você poderia, ou você pode simplesmente usar o Regex :)
Ray
0

(Já que todo mundo está tentando fazer isso). Aqui está minha versão (com verificações nulas e escape correto de entrada e substituição) ** Inspirada na Internet e em outras versões:

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");
Fredrik Johansson
fonte
0

Deixe-me fazer o meu caso e então você pode me rasgar em pedaços, se quiser.

Regex não é a resposta para esse problema - muito lento e com fome de memória, relativamente falando.

StringBuilder é muito melhor que manipular string.

Como esse será um método de extensão para complementar string.Replace , acredito que é importante combinar como isso funciona - portanto, lançar exceções para os mesmos problemas de argumento é importante, assim como retornar a string original se uma substituição não tiver sido feita.

Acredito que ter um parâmetro StringComparison não é uma boa ideia. Eu tentei, mas o caso de teste mencionado originalmente por michael-liu mostrou um problema: -

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

Enquanto IndexOf corresponderá, há uma incompatibilidade entre o comprimento da correspondência na cadeia de origem (1) e no oldValue.Length (2). Isso se manifestou causando IndexOutOfRange em algumas outras soluções quando oldValue.Length foi adicionado à posição de correspondência atual e não consegui encontrar uma maneira de contornar isso. O Regex falha em corresponder ao caso, então tomei a solução pragmática de usar apenasStringComparison.OrdinalIgnoreCase para a minha solução.

Meu código é semelhante a outras respostas, mas minha opinião é que procuro uma correspondência antes de me dar ao trabalho de criar a StringBuilder. Se nenhum for encontrado, uma alocação potencialmente grande será evitada. O código então se torna um do{...}whilee não umwhile{...}

Fiz alguns testes extensivos com outras respostas e isso saiu um pouco mais rápido e usou um pouco menos de memória.

    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
fonte