¿Hay una alternativa a la cadena. Reemplazar que no distingue entre mayúsculas y minúsculas?

306

Necesito buscar una cadena y reemplazar todas las ocurrencias de %FirstName%y %PolicyAmount%con un valor extraído de una base de datos. El problema es que la capitalización de FirstName varía. Eso me impide usar el String.Replace()método. He visto páginas web sobre el tema que sugieren

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

Sin embargo, por alguna razón cuando intento y reemplazar %PolicyAmount%con $0, la sustitución no se lleva a cabo. Supongo que tiene algo que ver con que el signo de dólar sea un carácter reservado en regex.

¿Hay otro método que pueda usar que no implique desinfectar la entrada para tratar con caracteres especiales regex?

Aheho
fuente
1
Si "$ 0" es la variable que entra, eso no afecta la expresión regular en absoluto.
cfeduke

Respuestas:

132

Desde MSDN
$ 0 - "Sustituye la última subcadena que coincide con el número de número de grupo (decimal)".

En .NET Expresiones regulares, el grupo 0 es siempre la coincidencia completa. Por un $ literal necesitas

string value = Regex.Replace("%PolicyAmount%", "%PolicyAmount%", @"$$0", RegexOptions.IgnoreCase);
Todd White
fuente
16
en este caso particular, esto está muy bien, pero en los casos en que las cadenas se introducen desde fuera, uno no puede estar seguro de que no contienen caracteres que significan algo especial en las expresiones regulares
Allanrbo
23
Debe escapar caracteres especiales como este: valor de cadena = Regex.Replace ("% PolicyAmount%", Regex.Escape ("% PolicyAmount%"), Regex.Escape ("$ 0"), RegexOptions.IgnoreCase);
Helge Klein
8
Tenga cuidado cuando use Regex.Escape en Regex.Replace. Tendrás que escapar de las tres cadenas aprobadas y llamar a Regex. ¡No escapes en el resultado!
Holger Adam
44
Según msdn: "Los escapes de caracteres se reconocen en los patrones de expresión regular pero no en los patrones de reemplazo". ( msdn.microsoft.com/en-us/library/4edbef7e.aspx )
Bronek
1
Es mejor usar: valor de cadena = Regex.Replace ("% PolicyAmount%", Regex.Escape ("% PolicyAmount%"), "$ 0" .Replace ("$", "$$"), RegexOptions.IgnoreCase); como reemplazo reconoce solo signos dolares.
Skorek
295

Parece que string.Replace debería tener una sobrecarga que requiere un StringComparisonargumento. Como no es así, puedes probar algo como esto:

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. Dragon 76
fuente
99
Agradable. Me gustaría cambiar ReplaceStringa Replace.
AMissico
41
De acuerdo con los comentarios anteriores. Esto se puede convertir en un método de extensión con el mismo nombre de método. Simplemente colóquelo en una clase estática con la firma del método: cadena estática pública Reemplazar (esta cadena String, string oldValue, string newValue, StringComparison comparación)
Mark Robinson
8
@Helge, en general, eso puede estar bien, pero tengo que tomar cadenas arbitrarias del usuario y no puedo arriesgarme a que la entrada sea significativa para la expresión regular. Por supuesto, supongo que podría escribir un bucle y poner una barra diagonal inversa delante de cada personaje ... En ese punto, también podría hacer lo anterior (en mi humilde opinión).
Jim
99
Mientras la unidad probaba esto, me encontré con el caso en el que nunca volvería cuando oldValue == newValue == "".
Ismael
10
Esto tiene errores; ReplaceString("œ", "oe", "", StringComparison.InvariantCulture)lanza ArgumentOutOfRangeException.
Michael Liu
45

Una especie de grupo confuso de respuestas, en parte porque el título de la pregunta es en realidad mucho más grande que la pregunta específica que se hace. Después de leerlo, no estoy seguro de que alguna respuesta esté a unas pocas ediciones de asimilar todas las cosas buenas aquí, así que pensé que trataría de sumar.

Aquí hay un método de extensión que creo que evita las trampas mencionadas aquí y proporciona la solución más ampliamente aplicable.

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

Entonces...

Desafortunadamente, el comentario de @HA de que tienes que hacer Escapelas tres cosas no es correcto . El valor inicial y newValueno necesita ser.

Nota: Sin embargo, debe escapar de $s en el nuevo valor que está insertando si son parte de lo que parece ser un marcador de "valor capturado" . Así, los tres signos de dólar en Regex.Replace dentro de Regex.Replace [sic]. Sin eso, algo así se rompe ...

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

Aquí está el error:

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.

Te digo qué, sé que las personas que se sienten cómodas con Regex sienten que su uso evita errores, pero a menudo sigo siendo parcial para detectar cadenas de bytes (pero solo después de haber leído Spolsky en las codificaciones ) para estar absolutamente seguro de que estás obteniendo lo que destinado a casos de uso importantes. Me recuerda un poco a Crockford sobre " expresiones regulares inseguras ". Con demasiada frecuencia escribimos expresiones regulares que permiten lo que queremos (si tenemos suerte), pero involuntariamente permitimos más (por ejemplo, ¿es $10realmente una cadena válida de "valor de captura" en mi nueva expresión de valor, arriba?) Porque no pensamos lo suficiente . Ambos métodos tienen valor, y ambos fomentan diferentes tipos de errores no intencionales. A menudo es fácil subestimar la complejidad.

Ese $escape extraño (y que Regex.Escapeno escapó de los patrones de valores capturados como $0esperaba en los valores de reemplazo) me volvió loco por un tiempo. La programación es difícil (c) 1842

ruffin
fuente
32

Aquí hay un método de extensión. No estoy seguro de dónde lo encontré.

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
fuente
Es posible que necesite manejar casos de cadenas vacías / nulas.
Vad
2
Múltiples errores en esta solución: 1. Verifique originalString, oldValue y newValue para ver si son nulos. 2. No devuelva orginalString (no funciona, los tipos simples no se pasan por referencia), pero asigne el valor de orginalValue primero a una nueva cadena y modifíquelo y devuélvalo.
RWC
31

Parece que el método más fácil es simplemente usar el método Reemplazar que se incluye con .Net y ha existido desde .Net 1.0:

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

Para utilizar este método, debe agregar una referencia al ensamblado Microsoft.VisualBasic. Este ensamblaje es una parte estándar del tiempo de ejecución de .Net, no es una descarga adicional ni está marcado como obsoleto.

CleverPatrick
fuente
44
Funciona. Debe agregar una referencia al ensamblado Microsoft.VisualBasic.
CleverPatrick
Es extraño que este método tuviera algunos problemas cuando lo usé (los caracteres al principio de la línea desaparecieron). La respuesta más popular aquí fue de C. Dragon 76como se esperaba.
Jeremy Thompson
1
El problema con esto es que devuelve una NUEVA cadena incluso si no se realiza un reemplazo, donde string.replace () devuelve un puntero a la misma cadena. Puede volverse ineficiente si está haciendo algo como una combinación de carta de formulario.
Brain2000
44
Brain2000, estás equivocado. Todas las cadenas en .NET son inmutables.
Der_Meister
Der_Meister, aunque lo que dices es correcto, eso no hace que lo que Brain2000 dijo mal.
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
fuente
¿Cuál es mejor manera? ¿Qué pasa con stackoverflow.com/a/244933/206730 ? ¿mejor presentación?
Kiquenet
8

Inspirado por la respuesta de cfeduke, hice esta función que usa IndexOf para encontrar el valor anterior en la cadena y luego la reemplaza con el nuevo valor. Utilicé esto en un script SSIS que procesaba millones de filas, y el método regex fue mucho más lento que esto.

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
fuente
+1 por no usar expresiones regulares cuando no es necesario. Claro, usa algunas líneas más de código, pero es mucho más eficiente que el reemplazo basado en expresiones regulares a menos que necesite la funcionalidad $.
ChrisG
6

Ampliando la respuesta popular de C. Dragon 76 al convertir su código en una extensión que sobrecarga el Replacemétodo predeterminado .

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

Basado en la respuesta de Jeff Reddy, con algunas optimizaciones y validaciones:

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

una versión similar a la de C. Dragon, pero si solo necesita un reemplazo:

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

Aquí hay otra opción para ejecutar reemplazos de Regex, ya que no mucha gente parece notar que las coincidencias contienen la ubicación dentro de la cadena:

    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
fuente
¿Podría explicar por qué está multiplicando por MatchNo?
Aheho
Si hay una diferencia de longitud entre oldValue y newValue, la cadena se alargará o acortará a medida que reemplace los valores. match.Index se refiere a la ubicación original dentro de la cadena, tenemos que ajustar para ese movimiento de posiciones debido a nuestro reemplazo. Otro enfoque sería ejecutar Eliminar / Insertar de derecha a izquierda.
Brandon
Lo entiendo. Para eso está la variable "offset". Lo que no entiendo es por qué estás multiplicando por matchNo. Mi intuición me dice que la ubicación de una coincidencia dentro de una cadena no tendría relación con el recuento real de ocurrencias anteriores.
Aheho
No importa, lo entiendo ahora. El desplazamiento debe escalarse según el número de ocurrencias. Si está perdiendo 2 caracteres cada vez que necesita hacer un reemplazo, debe
tenerlo
0
Regex.Replace(strInput, strToken.Replace("$", "[$]"), strReplaceWith, RegexOptions.IgnoreCase);
Joel Coehoorn
fuente
3
Esto no funciona El $ no está en el token. Está en strReplace With string.
Aheho
99
¿Y no puedes adaptarlo para eso?
Joel Coehoorn
18
Se supone que este sitio es un repositorio de respuestas correctas. No respuestas que son casi correctas.
Aheho
0

El método de expresión regular debería funcionar. Sin embargo, lo que también puede hacer es poner en minúscula la cadena de la base de datos, poner en minúscula el% de variables% que tiene y luego ubicar las posiciones y longitudes en la cadena en minúscula de la base de datos. Recuerde, las posiciones en una cadena no cambian solo porque está en minúscula.

Luego, usando un bucle que va en reversa (es más fácil, si no lo hace, tendrá que llevar un recuento continuo de hacia dónde se mueven los puntos posteriores) elimine de su cadena en caja no inferior de la base de datos las% variables% por su posición y longitud e inserte los valores de reemplazo.

cfeduke
fuente
Por inverso, me refiero a procesar las ubicaciones encontradas en sentido inverso del más lejano al más corto, no atravesar la cadena de la base de datos en sentido inverso.
cfeduke
Podrías, o podrías usar el Regex :)
Ray
0

(Ya que todos están intentando esto). Aquí está mi versión (con comprobaciones nulas, y entrada correcta y escape de reemplazo) ** Inspirado en Internet y otras versiones:

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
fuente
0

Permíteme presentar mi caso y luego puedes hacerme pedazos si quieres.

Regex no es la respuesta para este problema: demasiado lento y memoria hambrienta, en términos relativos.

StringBuilder es mucho mejor que la secuencia de cadenas.

Dado que este será un método de extensión para complementar string.Replace, creo que es importante hacer coincidir cómo funciona, por lo tanto, lanzar excepciones para los mismos problemas de argumento es importante, ya que es devolver la cadena original si no se realizó un reemplazo.

Creo que tener un parámetro StringComparison no es una buena idea. Lo intenté pero el caso de prueba mencionado originalmente por michael-liu mostró un problema:

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

Si bien IndexOf coincidirá, hay una falta de coincidencia entre la longitud de la coincidencia en la cadena de origen (1) y oldValue.Length (2). Esto se manifestó al causar IndexOutOfRange en algunas otras soluciones cuando oldValue.Length se agregó a la posición de coincidencia actual y no pude encontrar una forma de evitar esto. Regex no coincide con el caso de todos modos, así que tomé la solución pragmática de usar solo StringComparison.OrdinalIgnoreCasepara mi solución.

Mi código es similar a otras respuestas, pero mi giro es que busco una coincidencia antes de tomar la molestia de crear un StringBuilder. Si no se encuentra ninguno, se evita una asignación potencialmente grande. El código se convierte en un en do{...}whilelugar de unwhile{...}

He realizado algunas pruebas exhaustivas con otras respuestas y esto salió fraccionalmente más rápido y usó un poco menos de 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();
    }
Simon Hewitt
fuente