ECMAScript 2018 har nogle udvidelser til brugen af RegEx, som ikke mindst er interessant for danskere og andre folkeførd, der er sprogligt udfordret med ÅØÆ, accenter, umlaut og hvad der ellers findes af slige varianter.
Regulære udtryk – Regular Expressions – som den amerikanske matematiker Stephen Cole Kleene tilbage i 1950’erne lagde grundstenen for – er vistnok både elsket og hadet. Elsket, fordi de er nyttige og hadet, fordi de ser dumme og langhårede ud.
I javascript kan en simpel søgning se således ud:
console.log("Halli, halli, hallo".match(/hall./gi)); // ["Halli", "halli", "hallo"]
Indholdet mellem skråstregerne er det mønster, der søges på i teksten. Punktum betyder et enkelt af en hvilken som helst karakter (altså både i og o). gi efter sidste skråstreg er flag, der tilkendegiver, at søgning skal gentages og ikke stoppe efter første match og ignorer store/små bogstaver.
RegEx kan hurtigt blive en nyttig lille ting. Den følgende linje udtrykker, at vi ønsker enhver form for “katarsis” i teksten udskiftet med “nonsens”, hvad enten katarsis staves med c eller k:
console.log("Catarsis og katarsis".replace(/[kc]atarsis/gi,"Nonsens"));// Nonsens og Nonsens
På tilsvarende måde kan med parentes oprettes grupperede udtryk:
console.log("Christian og Kristian men ikke Tristian".match(/(Ch|K)ristian/g)); // ["Christian", "Kristian"]
Problemet
At leve i et land med æ, ø og å giver i mange sammenhænge muligheder for nogle ekstra kompetenceudviklinger, for at udtrykke det på den positive måde. I RegEx kommer det f.eks. til udtryk ved brug af indbyggede “shortcuts” eller stenografiske notationer. \w er en forkortelse for bogstaver og tegn, der kan være del af et ord. Således skulle denne simple søgning ideelt set give alle ord i teksten, men det går ikke som forventet:
let test = "Åh nej, ny sætning med en eller to prøvelser, Øv!"; console.log(test.match(/\w+/gi)); // ["h", "nej", "ny", "s", "tning", "med", "en", "eller", "to", "pr", "velser", "v"]
Det går helt galt. Årsagen er, at \w i virkeligheden blot er en forkortelse for udtrykket [a-zA-Z0-9_]. Find alle bogstaver fra a til z, store som små, talcifre fra 0 til 9 og _ (underscore). En hurtig dansk løsning ville således være at udvide til
[a-zA-Z0-9_æøåÆØÅ] :
let test = "Åh nej, ny sætning med en eller to prøvelser, Øv!"; console.log(test.match(/[a-zA-Z_0-9æøåÆØÅ]+/gi)); // ["Åh", "nej", "ny", "sætning", "med", "en", "eller", "to", "prøvelser", "Øv"]
Det går bedre indtil den solbeskinnede dag snart oprinder, hvor man får lov til at søge på “Böse Früchte und Gemüse“. Så knækker filmen igen.
Man har altså hidtil været nødsaget til at hive ASCII-tabellen frem og tilføje de yderligere bogstaver og tegn, som er nødvendige. Med RegExs \u notation, hvor bogstavets fircifrede hexadecimale værdi tilføjes, ville en hurtig pakkeløsning kunne se således ud:
let test = "Åh nej. Nur mit Böse Früchte und Gemüse. Øv!"; console.log(test.match(/[a-zA-Z_0-9\u00C0-\u00FF]+/gi)); // ["Åh", "nej", "Nur", "mit", "Böse", "Früchte", "und", "Gemüse", "Øv"]
Den kan forkortes ved at bruge hvad der ligger i \w og angive karatererne for \u00C0 og \u00FF:
let test = "Åh nej. Nur mit Böse Früchte und Gemüse. Øv!"; console.log(test.match(/[\wÀ-ÿ]+/gi)); // ["Åh", "nej", "Nur", "mit", "Böse", "Früchte", "und", "Gemüse", "Øv"]
så en udvidelse af \w til \wÀ-ÿ vil tage åæøÅÆØ med og tilfredsstille tyskernes behov for umlaut – og svenskerne, der som bekendt har et noget halvhjertet forhold til ÆØ og Å.
Løsningen vil således holde et stykke af vejen, men det er en lappeløsning. Dels medtager vi revl og krat af tegn mellem C0 og FF, hvilket ikke nødvendigvis er ideelt, dels er det ikke til at vide, hvilke sproglige udfordringer søgningen fremadrettet kan blive udsat for. Lidt græsk og urdu, så knækker filmen med garanti igen. Den optimale løsning har derfor krævet en kedsommelig og tidrøvende botanisering i sprog og tegn.
u-flag og \p notation
Med ECMAScript 2018 er der kommet understøttelse af Unicode, hvilket gør tingene meget lettere. Det aktiveres ved at tilføje u-flag: /whatever/u. Så er der en \p notation til rådighed, så ovenstående søgning f.eks. kan hedde:
console.log(test.match(/[\p{L}]+/giu)); // ["Åh", "nej", "Nur", "mit", "Böse", "Früchte", "und", "Gemüse", "Øv"]
\p{L} der også kunne skrives \p{Letter} dækker alle sprogvarianter fra serbokroatisk til hebræisk og mandarin. Bag gardinet er det i virkeligheden en forkortelse for:
[A-Za-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]
Det er et pænt stykke arbejde at slippe for og koden mildest talt mere læsevenlig. Der er også mulighed for at cherry-picke specifikke sprogtegnsæt. Følgende giver samme resultat som sidste eksempel:
console.log(test.match(/[\p{sc=Latn}]+/giu)); // ["Åh", "nej", "Nur", "mit", "Böse", "Früchte", "und", "Gemüse", "Øv"]
\p{sc=Latn} er den korte version af \p{Script=Latin} som altså indskrænker sig til kun at dække dette tegnsæt. Som ved \w og \d findes også negationen ved stort P: eksempelvis \P{L} vil give hvad ikke er givet med \p{L}.
\p har en lang række andre options end L. Det gælder f.eks. \p{N} eller \p{Number} der ikke overraskende giver tal. Forskellen mellem \d og \p{N} er, at sidstnævnte ikke bare fanger 0-9, men også Unicode varianter af tal, f.eks:
① ② ③ ⒈ ⒉ ⒊ ➊ ➋ ➌ ⓵ ⓶ ⓷ ⑴ ⑵ ⑶ ㉈ ㉉ ㉊
Look behind
Et andet problem opstår ved brug af notationen \b , der matcher begyndelsen eller enden på et ord, hvad enten det udgøres af et mellemrum, punktuation (.;, etc) eller begyndelse/afslutningen på tekst eller linje. Hvis jeg f.eks. søger på alle ord af to bogstavers længde, så kunne en mulighed være
let test = "Åh nej, en ny sætning med en eller to prøvelser, Øv!"; console.log(test.match(/\b[\p{L}]{2}\b/gui)); // ["en", "ny", "sæ", "en", "to", "pr"]
Det går jo rivegalt igen. Notationen \b henregner det udvidede karaktersæt som tegn der ikke hører til et normalt ord, men derimod som tegn på linje med punktum og komma, der adskiller ord.
\b er altså ikke nyttig med ÆØÅ-holdige tekster. Heldigvis kan en anden ny feature i EcmaScript 2018 komme til undsætning: (?<=…) der indikerer positiv lookbehind (?<!…) for negativ lookbehind. Det er en nyttig ting, der handler om det mønster, der ligger lige før det mønster, der matcher.
(?<=mit) vil eksempelvis matche tilbage i teksten indtil bogstaverne mit optræder:
let test = "Åh nej. I Nur mit Böse Früchte und Gemüse. Øv!"; console.log(test.match(/(?<=mit)[^]+/gi)); // [" Böse Früchte und Gemüse. Øv"]
Konceptet kan også bruges lidt mere nyttige ting end frugter og grøntsager, f.eks. til at udtrække links og scripts fra HTML:
<div id=markup><script async defer src="https://www.example.com/default.js"></script> <script src="/www.microsoft.com/test.js"></script> <a class="linkclass" href="https://euromind.com/default.html"></a> <a href="http://www.euromind.com"></a></div> <script> const urlPattern = /(?<=\<a[^>]* href=")([^"]*)/ig; console.log(html.match(urlPattern)); // ["https://lindsoe.net/default.html", "http://euromind.com"] const scriptPattern = /(?<=\<script[^>]* src=")([^"]*)/ig; console.log(html.match(scriptPattern)); // ["https://www.example.com/default.js", "/www.microsoft.com/test.js"] </script>
I længere tid har lookahead (?=) notationen været tilgængelig i javascript. Dens funktion i verden er at kigge fremad fra den givne position og matche til en forudsætning er opfyldt. Tegnet ^ der hedder circumfleks – men i virkeligheden bare er en klaphat – betyder i RegEx-sprog begyndelsen af en streng eller linje. På samme måde betyder $ slutningen på en streng eller linje. Vi kan derfor nu erstatte \b på følgende måde:
- Fra den nuværende position kig tilbage i teksten til enten start af linjen (^) eller ( | ) hvor der optræder en tegn, der betyder orddeling. Hvis [\p{L}] er defineret som tegn, der kan udgøre bogstaver i et ord, så kan vi bruge negationen som udtryk for tegn, der repræsenterer det modsatte – altså tegn, der adskiller ord. For at gøre tingene enkle i RegEx er notationen for en negation sjovt nok også en ^ – klaphat. Det giver således (?<=^|[^\p{L})
- Kig fremad til enten afslutningen af strengen ( $ ) eller ( | ) til et tegn, der adskiller ord ([^\wÀ-ÿ]). I alt bliver det (?=$|[^\p{L}])
- Match ord på to bogstaver [\p{L}]{2}
let test = "Åh nej, en ny sætning med en eller to prøvelser, Øv!"; console.log(test3.match(/(?<=^|[\P{L}])[\p{L}]{2}(?=$|[\P{L}])/gui)); // ["Åh", "en", "ny", "en", "to", "Øv"]
Eller muligvis lidt mere overskueligt:
let nonWordchars = "[^a-zA-Z_0-9\\u00C0-\\u00FF]"; let wordChars = "[a-zA-Z_0-9\\u00C0-\\u00FF]"; let boundaryStart = "(?<=^|" + nonWordchars + ")"; let boundaryEnd = "(?=$|" + nonWordchars + ")"; let pattern = new RegExp(boundaryStart + wordChars + "{2}" + boundaryEnd, "gi"); console.log(test.match(pattern));// ["Åh", "en", "ny", "en", "to", "Øv"]