Existe-t-il une alternative à la chaîne, remplacer qui ne respecte pas la casse?

306

J'ai besoin de rechercher une chaîne et de remplacer toutes les occurrences de %FirstName%et %PolicyAmount%par une valeur extraite d'une base de données. Le problème est que la capitalisation de FirstName varie. Cela m'empêche d'utiliser la String.Replace()méthode. J'ai vu des pages Web sur le sujet qui suggèrent

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

Cependant, pour une raison quelconque, lorsque j'essaie de remplacer %PolicyAmount%par $0, le remplacement n'a jamais lieu. Je suppose que cela a quelque chose à voir avec le signe dollar étant un caractère réservé dans l'expression régulière.

Y a-t-il une autre méthode que je peux utiliser qui n'implique pas de nettoyer l'entrée pour gérer les caractères spéciaux regex?

Aheho
la source
1
Si "$ 0" est la variable qui entre, cela n'a aucun impact sur l'expression régulière.
cfeduke

Réponses:

132

À partir de MSDN
$ 0 - "Remplace la dernière sous-chaîne correspondant au numéro de groupe (décimal)."

Dans .NET, le groupe d'expressions régulières 0 correspond toujours à l'intégralité de la correspondance. Pour un $ littéral, vous devez

string value = Regex.Replace("%PolicyAmount%", "%PolicyAmount%", @"$$0", RegexOptions.IgnoreCase);
Todd White
la source
16
dans ce cas particulier, c'est très bien, mais dans les cas où les chaînes sont entrées de l'extérieur, on ne peut pas être sûr qu'elles ne contiennent pas de caractères qui signifient quelque chose de spécial dans les expressions régulières
Allanrbo
23
Vous devez échapper les caractères spéciaux comme celui-ci: chaîne valeur = Regex.Replace ("% PolicyAmount%", Regex.Escape ("% PolicyAmount%"), Regex.Escape ("$ 0"), RegexOptions.IgnoreCase);
Helge Klein
8
Veuillez faire attention lorsque vous utilisez Regex.Escape dans Regex.Replace. Vous devrez échapper aux trois chaînes passées et appeler Regex.Unescape sur le résultat!
Holger Adam
4
Selon msdn: "Les caractères d'échappement sont reconnus dans les modèles d'expression régulière mais pas dans les modèles de remplacement." ( msdn.microsoft.com/en-us/library/4edbef7e.aspx )
Bronek
1
Il est préférable d'utiliser: string value = Regex.Replace ("% PolicyAmount%", Regex.Escape ("% PolicyAmount%"), "$ 0" .Replace ("$", "$$"), RegexOptions.IgnoreCase); comme remplacement ne reconnaît que les signes dolaires.
Skorek
295

On dirait que string.Replace devrait avoir une surcharge qui prend un StringComparisonargument. Comme ce n'est pas le cas, vous pouvez essayer quelque chose comme ceci:

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
la source
9
Agréable. Je changerais ReplaceStringpour Replace.
AMissico
41
Acceptez les commentaires ci-dessus. Cela peut être transformé en une méthode d'extension avec le même nom de méthode. Il suffit de l'insérer dans une classe statique avec la signature de la méthode: public static string Replace (this String str, string oldValue, string newValue, StringComparison comparaison)
Mark Robinson
8
@Helge, en général, cela peut être bien, mais je dois prendre des chaînes arbitraires de l'utilisateur et je ne peux pas risquer que l'entrée soit significative pour regex. Bien sûr, je suppose que je pourrais écrire une boucle et mettre une barre oblique inverse devant chaque personnage ... À ce stade, je ferais aussi bien de faire ce qui précède (à mon humble avis).
Jim
9
Pendant que je testais cela, je suis tombé sur le cas où il ne reviendrait jamais quand oldValue == newValue == "".
Ishmael
10
C'est buggy; ReplaceString("œ", "oe", "", StringComparison.InvariantCulture)jette ArgumentOutOfRangeException.
Michael Liu
45

Une sorte de groupe de réponses déroutant, en partie parce que le titre de la question est en réalité beaucoup plus large que la question spécifique posée. Après avoir lu, je ne suis pas sûr qu'une réponse soit à quelques modifications de l'assimilation de toutes les bonnes choses ici, alors j'ai pensé que j'essaierais de résumer.

Voici une méthode d'extension qui, je pense, évite les pièges mentionnés ici et fournit la solution la plus largement applicable.

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

Alors...

  • Ceci est une méthode d'extension @MarkRobinson
  • Cela n'essaie pas d'ignorer Regex @Helge (vous devez vraiment faire octet par octet si vous voulez chaîne sniff comme ça en dehors de Regex)
  • Laissez - passer de » @MichaelLiu excellente cas de test , "œ".ReplaceCaseInsensitiveFind("oe", "")bien qu'il peut avoir eu un comportement légèrement différent à l' esprit.

Malheureusement, le commentaire de @HA que vous devez faire aux Escapetrois n'est pas correct . La valeur initiale et newValuen'a pas besoin d'être.

Remarque: Cependant, vous devez échapper les $s dans la nouvelle valeur que vous insérez s'ils font partie de ce qui semble être un marqueur de "valeur capturée" . Ainsi, les trois signes du dollar dans le Regex.Replace à l'intérieur du Regex.Replace [sic]. Sans cela, quelque chose comme ça se casse ...

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

Voici l'erreur:

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.

Dites-vous quoi, je sais que les gens qui sont à l'aise avec Regex ont l'impression que leur utilisation évite les erreurs, mais je reste souvent partial pour les chaînes de reniflement d'octets (mais seulement après avoir lu Spolsky sur les encodages ) pour être absolument sûr que vous obtenez ce que vous destiné aux cas d'utilisation importants. Ça me rappelle un peu Crockford sur les " expressions régulières peu sûres ". Trop souvent, nous écrivons des expressions rationnelles qui permettent ce que nous voulons (si nous avons de la chance), mais en autorisons involontairement plus (par exemple, est-ce $10vraiment une chaîne de "valeur de capture" valide dans mon expression rationnelle newValue, ci-dessus?) Parce que nous n'étions pas assez réfléchis . Les deux méthodes ont de la valeur et encouragent toutes deux différents types d'erreurs involontaires. Il est souvent facile de sous-estimer la complexité.

Cette $fuite étrange (et qui Regex.Escapen'a pas échappé aux modèles de valeur capturés $0comme je m'y attendais dans les valeurs de remplacement) m'a rendu fou pendant un certain temps. La programmation est difficile (c) 1842

ruffin
la source
32

Voici une méthode d'extension. Je ne sais pas où je l'ai trouvé.

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
la source
Vous devrez peut-être gérer les cas de chaîne vide / nulle.
Vad
2
Erreurs multiples dans cette solution: 1. Vérifiez nullString originalString, oldValue et newValue. 2. Ne restituez pas orginalString (ne fonctionne pas, les types simples ne sont pas transmis par référence), mais affectez d'abord la valeur de orginalValue à une nouvelle chaîne, modifiez-la et restituez-la.
RWC
31

Il semble que la méthode la plus simple consiste simplement à utiliser la méthode Replace livrée avec .Net et utilisée depuis .Net 1.0:

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

Pour utiliser cette méthode, vous devez ajouter une référence à l'assembly Microsoft.VisualBasic. Cet assemblage est une partie standard du runtime .Net, ce n'est pas un téléchargement supplémentaire ou marqué comme obsolète.

CleverPatrick
la source
4
Ça marche. Vous devez ajouter une référence à l'assembly Microsoft.VisualBasic.
CleverPatrick
Étrange que cette méthode ait eu quelques problèmes quand je l'ai utilisée (les caractères au début de la ligne ont disparu). La réponse la plus populaire ici a C. Dragon 76fonctionné comme prévu.
Jeremy Thompson
1
Le problème avec cela est qu'il retourne une NOUVELLE chaîne même si aucun remplacement n'est effectué, où string.replace () renvoie un pointeur sur la même chaîne. Peut devenir inefficace si vous faites quelque chose comme une fusion de lettres types.
Brain2000
4
Brain2000, vous vous trompez. Toutes les chaînes dans .NET sont immuables.
Der_Meister
Der_Meister, bien que ce que vous dites soit correct, cela ne fait pas mal ce que Brain2000 a dit de 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
la source
Quelle est la meilleure façon? qu'en est-il de stackoverflow.com/a/244933/206730 ? meilleure performance?
Kiquenet
8

Inspiré par la réponse de cfeduke, j'ai créé cette fonction qui utilise IndexOf pour trouver l'ancienne valeur dans la chaîne, puis la remplace par la nouvelle valeur. Je l'ai utilisé dans un script SSIS traitant des millions de lignes, et la méthode regex était beaucoup plus lente que cela.

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
la source
+1 pour ne pas utiliser l'expression régulière lorsque ce n'est pas nécessaire. Bien sûr, vous utilisez quelques lignes de code supplémentaires, mais c'est beaucoup plus efficace que le remplacement basé sur des expressions rationnelles, sauf si vous avez besoin de la fonctionnalité $.
ChrisG
6

Développant la réponse populaire de C. Dragon 76 en faisant de son code une extension qui surcharge la Replaceméthode par défaut .

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
la source
3

Basé sur la réponse de Jeff Reddy, avec quelques optimisations et validations:

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
la source
2

une version similaire à C. Dragon's, mais si vous n'avez besoin que d'un seul remplacement:

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

Voici une autre option pour exécuter les remplacements Regex, car peu de gens semblent remarquer que les correspondances contiennent l'emplacement dans la chaîne:

    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
la source
Pourriez-vous expliquer pourquoi vous multipliez par MatchNo?
Aheho
S'il existe une différence de longueur entre oldValue et newValue, la chaîne devient plus longue ou plus courte lorsque vous remplacez des valeurs. match.Index se réfère à l'emplacement d'origine dans la chaîne, nous devons ajuster ce mouvement de position en raison de notre remplacement. Une autre approche consisterait à exécuter la suppression / insertion de droite à gauche.
Brandon
Je comprends ça. C'est à cela que sert la variable "offset". Ce que je ne comprends pas, c'est pourquoi vous multipliez par matchNo. Mon intuition me dit que l'emplacement d'une correspondance dans une chaîne n'aurait aucun rapport avec le nombre réel d'occurrences précédentes.
Aheho
Tant pis, je comprends maintenant. Le décalage doit être mis à l'échelle en fonction du nombre d'occurrences. Si vous perdez 2 caractères à chaque fois que vous devez effectuer un remplacement, vous devez en tenir compte lors du calcul des paramètres de la méthode de suppression
Aheho
0
Regex.Replace(strInput, strToken.Replace("$", "[$]"), strReplaceWith, RegexOptions.IgnoreCase);
Joel Coehoorn
la source
3
Ça ne marche pas. Le $ n'est pas dans le jeton. C'est dans le strReplace With string.
Aheho
9
Et vous ne pouvez pas l'adapter pour ça?
Joel Coehoorn
18
Ce site est censé être un référentiel pour les bonnes réponses. Pas des réponses presque correctes.
Aheho
0

La méthode d'expression régulière devrait fonctionner. Cependant, ce que vous pouvez également faire est de mettre en minuscule la chaîne de la base de données, de mettre en minuscule les% variables% que vous avez, puis de localiser les positions et les longueurs dans la chaîne en minuscules de la base de données. N'oubliez pas que les positions dans une chaîne ne changent pas simplement parce qu'elle est en bas de casse.

Ensuite, en utilisant une boucle qui va dans le sens inverse (c'est plus facile, si vous ne le faites pas, vous devrez garder un compte courant de l'endroit où les points ultérieurs se déplacent) supprimez de votre chaîne non inférieure de la base de données les% variables% par leur position et longueur et insérez les valeurs de remplacement.

cfeduke
la source
Par inverse, je veux dire traiter les emplacements trouvés en sens inverse du plus éloigné au plus court, pas traverser la chaîne de la base de données en sens inverse.
cfeduke
Vous pourriez, ou vous pourriez simplement utiliser le Regex :)
Ray
0

(Puisque tout le monde prend une photo à ce sujet). Voici ma version (avec des contrôles nuls, et une entrée correcte et un échappement de remplacement) ** Inspiré d'Internet et d'autres versions:

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

Usage:

var result = "This is a test".ReplaceIgnoreCase("IS", "was");
Fredrik Johansson
la source
0

Laissez-moi faire mon cas et vous pourrez me déchirer si vous le souhaitez.

Regex n'est pas la réponse à ce problème - trop lent et gourmand en mémoire, relativement parlant.

StringBuilder est bien meilleur que la manipulation de chaînes.

Étant donné que ce sera une méthode d'extension à compléter string.Replace, je pense qu'il est important de faire correspondre comment cela fonctionne - il est donc important de lever des exceptions pour les mêmes problèmes d'argument, tout comme le retour de la chaîne d'origine si aucun remplacement n'a été effectué.

Je pense qu'avoir un paramètre StringComparison n'est pas une bonne idée. Je l'ai essayé mais le cas de test mentionné à l'origine par michael-liu a montré un problème: -

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

Bien qu'IndexOf corresponde, il existe un décalage entre la longueur de la correspondance dans la chaîne source (1) et oldValue.Length (2). Cela s'est manifesté en provoquant IndexOutOfRange dans certaines autres solutions lorsque oldValue.Length a été ajouté à la position de correspondance actuelle et je n'ai pas pu trouver un moyen de contourner cela. Regex ne parvient pas à faire correspondre le cas de toute façon, j'ai donc pris la solution pragmatique de n'utiliser que StringComparison.OrdinalIgnoreCasepour ma solution.

Mon code est similaire à d'autres réponses mais mon torsion est que je cherche une correspondance avant de me donner la peine de créer un StringBuilder. Si aucun n'est trouvé, une allocation potentiellement importante est évitée. Le code devient alors un do{...}whileplutôt qu'unwhile{...}

J'ai fait des tests approfondis contre d'autres réponses et cela est sorti beaucoup plus rapidement et j'ai utilisé un peu moins de mémoire.

    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
la source