Case insensitive replace och tillämpningar
Förord
Standard Replace i string-klassen är case sensitive. På (nästan ren) svenska betyder det att string-klassens ersättningsmetod är skriftlägeskänslig, dvs ser stor och liten bokstav som olika bokstäver. Detta kan givetvis bli ett problem om man har behov av att byta ut text oberoende av om texten är skriven med stora eller små bokstäver. På nätet kan man hitta ett flertal lösningar på detta problem. Här presenteras en lite annorlunda lösning som har ett antal fördelar jämfört med de andra lösningarna som diskuteras. Exempel på tillämpningar är att skydda ditt system mot potentiellt farliga ord och att byta ut vissa ord mot bilder.
Ordförklaring
För att göra artikeln lättare att läsa kommer jag använda mig av följande ord:oldValue – den delen av en string som du vill ersätta
newValue – den delen av en string som du vill ersätta oldValue med
expression – en string som du vill ersätta ut delar av
Replace
Att ersätta delar av en text (replace) är mycket enkelt i .NET. Eftersom .NET är objektorienterat kommer den vane objektorienteraren hitta replace-metoden precis där den hör hemma: I string-klassen. Ett exempel: C#
string myString = "Detta är en av dina strängar";
myString = myString.Replace("dina","mina");
VB.NET
Dim myString As String = "Detta är en av dina strängar"
myString = myString.Replace("dina", "mina")
Efter att replace-metoden körts kommer myString att innehålla texten "Detta är en av mina strängar". En intressant detalj i sammanhanget är att man måste skriva
...
C#
myString = myString.Replace("dina","mina");
VB.NET
myString = myString.Replace("dina","mina")
...
för att koden skall fungera. Detta beror på att en string i .NET inte kan förändras, den är sk Immutable. Om du vill förändra innehållet i en string-variabel måste du helt enkelt skapa en ny string som sedan sparas i din variabel. Replace-metoden skapar och returnerar alltså ett nytt string-objekt som innehåller samma text som myString, med vissa värden utbytta förstås.
Case insensitive replace
När man vill åstadkomma en skriftlägesokänslig ersättning blir det lite svårare. Anledningen till att denna artikel existerar är att skriftlägesokänslig ersättning inte finns implementerat i string-klassen per idag. För att illustrera problemet ytterligare har jag gjort en liten lista på vad vi måste göra för att åstadkomma önskat resultat i vårt exempel ovan oberoende av skriftläget i ordet ”dina”. Vi måste köra Replace på myString med alla de ord som jag skrivit in i listan nedan:Dina
dIna
diNa
dinA
DIna
DiNa
DinA
diNA
dInA
…(och några ord till)
Ärligt talat blir jag lite trött bara av att skriva en sådan lista. Jag tror att de flesta håller med om att det i praktiken är omöjligt att ta hand om alla fall av stora och små bokstäver genom att använda skriftlägesokänslig ersättning. Iallafall om man vill byta ut mer än ett ganska kort ord (vilket är rätt så tråkigt det också!). Så det vi behöver är en metod som byter ut en text - oberoende av dess kombination med stora och små bokstäver – med en annan text. Det kan väl inte vara så svårt att få till? Helt rätt, det är inte så svårt att få till, ett flertal lösningar finns runt på Internet på olika tutorials, forum, utvecklarsiter etc. Nedan tittar jag lite på några av dem.
Existerande lösningar
Jag vet inte om jag har sett alla lösningarna som har publicerats på nätet men här är iallafall några av dom:- Loop-lösningen
Tanken bakom denna lösningen är att man först tillverkar en version av sitt expression och oldValue som är helt och hållet skrivet med små (alternativt stora) bokstäver. Man har då fått ”skriftlägesokänsliga versioner” av expression och oldValue. Sedan gör man en loop med de skriftlägesokänsliga versionerna som för varje gång man hittar en oldValue i expression gör en ny version av expression som baseras på det orginala expression samt newValue. - Den rekursiva lösningen
Tanken bakom denna lösning är nästan exakt densamma som loop-lösningen. Om man hittar en skriftlägesokänslig version av oldValue i den skriftlägesokänsliga versionen av expression bygger man en ny version av expression och returnerar därefter ett metodanrop till samma metod (Replace) med den nya expression som parameter. När man inte hittar oldValue i expression har allt bytts ut och man returnerar expression. - Reguljärt uttryck mönster-lösningen
Tanken bakom denna lösning är rätt så annorlunda jämfört med de två andra. För att förstå lösningen krävs att man kan lite om Regular Expressions (reguljära uttryck). Enkelt sagt är ett reguljärt uttryck inte en vanlig text men ett mönster. I mönstret kan man ha bokstäver, siffror och andra tecken. Detta mönster kan användas till att hitta en teckenkombination i en text (och en massa andra saker…). För att lösa vårt problem konstruerar man ett mönster som kan hitta oldValue oberoende av skriftläge.
I reguljära uttryck finns något som kallas Character classes (bokstavsklasser). En bokstavsklass som matchar bokstaven ’A’ oberoende av skriftläge ser ut så här : ”[aA]”. Denna klassen kommer hitta alla ‘a’ och ‘A’ i en text. Ett reguljärt uttryck som hittar ”dina” oberoende av skriftläge ser ut så här: “[dD][iI][nN][aA]”. När man väl har skapat ett reguljärt uttryck skickar man med expression, det reguljära uttrycket och newValue till Replace-metoden i RegEx-klassen som sedan returnerar ett string-objekt.
Enligt min ödmjuka åsikt är Reguljärt uttryck mönster-lösningen den bästa av lösningarna ovan. Ytterligare en av mina ödmjuka åsikter är att ingen av dessa lösningnar är riktigt tillfredsställande. Jag har därför hittat på en annan lösning som är baserad på “Reguljärt uttryck mönster-lösningen”. Jag kallar lösningen Reguljärt uttryck-lösningen…
En ny lösning
I grund och botten är denna lösning densamma som reguljärt uttryck monster-lösningen, fast utan mönstret! En lite mera noggrann inspektion av Replace-metoden i RegEx-klassen visar att en av dess överlagrade versioner kan ta emot en parameter som kallas options och är av typen RegexOptions (en enum). Valet RegexOptions.IgnoreCase beskrivs så här på MSDN: ” Specifies case-insensitive matching.” Detta innebär att vi kan lämna med ett reguljärt uttryck och tala om för metoden att den skall hitta detta monster oberoende av skriftläge. Det reguljära uttrycket blir i vårt fall mycket enkelt: oldValue, utan några som helst ändringar! Kodsnutten nedan visar hur enkelt det är att implementera skriftlägesokänslig ersättning med den nya lösningen:
C#
expression = Regex.Replace(
expression,
oldValue,
newValue,
RegexOption.IgnoreCase);
VB.NET
expression = Regex.Replace( _
expression, _
oldValue, _
newValue, _
RegexOption.IgnoreCase)
Och det är allt! Inga loopar, inga rekursiva anrop, inga reguljära uttryck att bygga etc. Varför jag föredrar denna lösningen? Jag har i grund och botten tre orsaker:
- Mindre kod
Mindre kod betyder att risken för buggar reduceras (OK jag erkänner, jag har gjort ett misstag eller två i min kod förut :) - .NET Framework (plattformen .NET) utnyttjas i högre grad
Detta är en mycket stor fördel eftersom plattformen hela tiden utvecklas och förbättras av Microsoft. Detta innebär att kodens prestanda kan ökas när vi använder nyare versioner av .NET Framework. För att åstadkomma detta behöver vi varken ändra koden eller bygga/kompilera om projektet! - Prestanda
I vissa fall är det detta som betyder mest. I vårt fall handlar det om att utveckla en metod som skall återanvändas om och om igen, så i detta fall är prestanda rätt så betydelsefullt. Enligt ett enkelt test jag gjort med en text på ca 40.000 ord så är min lösning ca 3 gånger snabbare än den näst snabbaste metoden som är reguljärt uttryck mönster-lösningen. Den var i sin tur ca 17 gånger snabbare än loop-lösningen som igen var ca 5 gånger snabbare än den rekursiva lösningen.
En case insensitive Replace-metod
Hur skall vi då implementera en case insensitive Replace-metod? Det finns några saker att tänka på. Borde man göra en ny string-klass som har en överlagrad Replace-metod? Eller göra en klassmetod (static(C#)/Shared(VB.NET)) som tar emot inte bara oldValue och newValue men också expression? Borde vi göra en metod som kan hantera både skriftlägesokänslig och skriftlägeskänslig ersättning? Jag har några förslag:Att göra en ny string-klass blir något svårt eftersom man inte kan ärva från string-klassen. string-klassen har en hel del metoder som vi i så fall får implementera, och klassen kan komma att ändras i framtiden i nya .NET Framework-versioner. Det senare innebär att om vi vill att vår egna string-klass skall klara av allt som System.string klarar av kan vi behöva modifiera klassen om och om igen. Jag föreslår alltså att göra en static/Shared metod. Skall metoden klara av skriftlägesokänslig och skriftlägeskänslig ersättning? Programmeraren kan ju välja vilken metod han eller hon vill anropa. Även om vi har en metod som kan hantera båda fallen så kan ju programmeraren välja att anropa string-klassens metod för skriftlägeskänslig ersättning. Så varför inte ge programmeraren valet? Min implementation ser ut så här:
C#
public static string Replace(string expression,string oldValue, string newValue, bool caseSensitive) {
string replaced;
if (caseSensitive) {
replaced = expression.Replace(oldValue,newValue);
}
else {
replaced = Regex.Replace(expression, oldValue, newValue, RegexOptions.IgnoreCase);
}
return replaced;
}
VB.NET
Public Shared Function Replace(ByVal expression As String, ByVal oldValue As String, _
ByVal newValue As String, ByVal caseSensitive As Boolean) As String
Dim replaced As String
If caseSensitive Then
replaced = expression.Replace(oldValue, newValue)
Else
replaced = Regex.Replace(expression, oldValue, newValue, RegexOptions.IgnoreCase)
End If
Return replaced
End Function
Tillämpningar
Vad kan vi använda denna nya funktionalitet till? Jag har två exempel: Ta bort potentiellt farliga ord från en text och byta ut vissa ord i en text på en webbsida mot bilder. Båda dessa tillämpningarna kräver att man använder Replace på samma text ett antal gånger med olika oldValues och newValues. Jag har därför skrivit ytterligare en metod som tar emot många parametrar för oldValue och newValue. Vid anrop till denna metod skall parametern oldAndNewValues ha värden enligt detta mönstret: ”oldValue|newValue”, t.ex. ”dina|mina”. C#
public static string MultipleReplace(string expression,bool caseSensitive,params string[] oldAndNewValues) {
string replaced = expression;
string[] oldAndNew;
foreach(string oldAndNewValue in oldAndNewValues) {
oldAndNew = oldAndNewValue.Split('|');
replaced = Replace(replaced,oldAndNew[0],oldAndNew[1],caseSensitive);
}
return replaced;
}
VB.NET
Public Shared Function MultipleReplace(ByVal expression As String, ByVal caseSensitive As Boolean, ByVal ParamArray oldAndNewValues() As String) As String
Dim replaced As String = expression
Dim oldAndNewValue As String
Dim oldAndNew() As String
For Each oldAndNewValue In oldAndNewValues
oldAndNew = oldAndNewValue.Split("|")
replaced = Replace(replaced, oldAndNew(0), oldAndNew(1), caseSensitive)
Next
Return replaced
End Function
Nyckelordet params(C#)/ParamArray(VB.NET) gör att oldAndNewValues blir en parameter-array. Detta innebär att man antingen kan anropa metoden med en string[](C#)/String()(VB.NET) som innehåller ett antal string-objekt med oldValue|newValue, eller skriva in så många oldValue|newValue string man vill som parametrar till metoden. Utifrån detta har jag (mycket enkelt) skrivit en metod som eliminerar potentilelt farliga ord (rätt så många script- och programmeringsspråk är case insensitive) från en text :
C#
public static string ReplaceDangerousWords(string s) {
//add any number of potentially dangerous words to the parameter list
return MultipleReplace(s,false,
"object|object",
"script|script",
"applet|applet");
}
VB.NET
Public Shared Function ReplaceDangerousWords(ByVal s As String) As String
'add any number of potentially dangerous words to the parameter list
Return MultipleReplace(s, False, _
"object|object", _
"script|script", _
"applet|applet")
End Function
Lika enkelt är det att skriva en metod som lägger in bilder i en text (inte alla är lika noga när det gäller att använda små och stora bokstäver som programmerare SMiley):
C#
public static string InsertImagesInText(string s) {
//add any number of image replacements to the parameter list
return MultipleReplace(s,true,
" smiley | ",
" ulgy | ",
" .net | ");
//don't replace parts of a web-address:
//include a space before and after words
}
VB.NET
Public Shared Function InsertImagesInText(ByVal s As String) As String
'add any number of image replacements to the parameter list
Return MultipleReplace(s, False, _
" smiley | ", _
" ugly | ", _
" .net | ")
'don't replace parts of a web-address:
'include a space before and after words
End Function
Konklusion
Att kunna utföra case insensitive Replace på en text är ibland mycket viktigt. I .NET är case insensitive Replace inte implementerat per default i string-klassen, men är ändå rätt så lätt att åstadkomma.
Ladda ner koden
Klicka på bilden nedan om du vill ladda ner koden som jag skrivit i denna artikel. Både C# och VB.NET kod finns med. Tips: Om du vill använda klasserna i din programmering välj då att kompilera C# klasserna eftersom de har XML-kommentarer inlagt i koden (går inte att göra i VB.NET). Dessa kommentarer gör att det blir lättare för dig att använda klasserna om du arbetar i en editor med inbyggt stöd för XML-kommentarer, t.ex. Visual Studio .NET.
Resurser
- Replace-metoden i Regex-klassen
- RegexOption enum
- RegexOption enum
Lycka till!
Herbjörn
Mikael Johansson
Har man pysslat med PHP förut blir man frustrerad att inte detta redan finns i .Net-plattformen. Letade länge i .Net klasser efter funktionaliteten innan jag hittade denna artikel och insåg att det inte fanns någon enkel "standardfunktion"