8 janvier 2021

Ensembles et intervalles [...]

Plusieurs caractères ou classes de caractères, entourés de crochets […] signifient “chercher un caractère parmi ceux-là”.

Ensembles

Par exemple, [eao] signifie un caractère qui est soit 'a', 'e', ou 'o'.

On appelle cela un ensemble. Les ensembles peuvent être combinés avec d’autres caractères dans une même expression régulière :

// trouve [t ou m], puis "op"
alert( "Mop top".match(/[tm]op/gi) ); // "Mop", "top"

Bien qu’il y ait plusieurs caractères dans un ensemble, vous remarquez que l’on ne cherche la correspondance que d’un seul de ces caractères.

L’exemple suivant ne donne donc aucun résultat :

// trouve "V", puis [o ou i], puis "la"
alert( "Voila".match(/V[oi]la/) ); // null, pas de correspondance

L’expression régulière recherche :

  • V,
  • puis une des lettres [oi],
  • enfin la.

Ce qui correspondrait à Vola ou Vila.

Intervalles

Les crochets peuvent aussi contenir des intervalles de caractères.

Par exemple, [a-z] est un caractère pouvant aller de a à z, et [0-5] est un chiffre allant de 0 à 5.

Dans l’exemple ci-dessous nous recherchons un "x" suivi par deux chiffres ou lettres de A à F:

alert( "Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g) ); // xAF

Ici [0-9A-F] comporte deux intervalles : il recherche un caractère qui est soit chiffre entre 0 et 9 compris ou bien une lettre entre A et F comprise.

Si nous voulons y inclure les lettres minuscules, nous pouvons ajouter l’intervalle a-f: [0-9A-Fa-f]. Ou bien ajouter le marqueur i.

Nous pouvons aussi utiliser les classes de caractères entre […].

Par exemple, si nous voulons chercher un caractère alphanumérique, un trait de soulignement \w ou un tiret -, alors l’ensemble s’écrit [\w-].

Il est aussi possible de combiner plusieurs classes, p. ex. [\s\d] signifie “un caractère d’espacement ou un chiffre”.

Les classes de caractères sont en fait des racourcis pour des intervalles de caractères particuliers

Par exemple:

  • \d – équivaut à [0-9],
  • \w – équivaut à [a-zA-Z0-9_],
  • \s – équivaut à [\t\n\v\f\r ], plus quelques autres rares caractères unicodes d’espacement.

Exemple : \w multi-langue

Comme la classe de caractères \w est un raccourci pour [a-zA-Z0-9_], il ne peut pas trouver les idéogrammes chinois, ni les lettres cyrilliques, etc.

Nous pouvons écrire un motif plus universel, pour rechercher le caractère d’un mot quelle que soit la langue. Grâce aux propriétés Unicode, on obtient facilement : [\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}].

Déchiffrons cela. Tout comme \w, nous construisons notre propre ensemble qui contient les caractères qui portent les propriétés Unicode :

  • Alphabetic (Alpha) – pour les lettres,
  • Mark (M) – pour les accents,
  • Decimal_Number (Nd) – pour les nombres,
  • Connector_Punctuation (Pc) – pour le trait de soulignement '_' et autres caractères similaires,
  • Join_Control (Join_C) – deux codes spéciaux 200c et 200d, utilisés comme liaisons, p. ex. en arabe.

Exemple d’usage :

let regexp = /[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/gu;

let str = `Hi 你好 12`;

// trouve toutes les lettres et chiffres:
alert( str.match(regexp) ); // H,i,你,好,1,2

Cet ensemble est bien sûr encore modifiable : on peut y ajouter ou retirer des propriétés Unicode. Plus de détail sur ces propriétés Unicode dans l’article Unicode: indicateur "u" et classe \p{...}.

Les propriétés Unicode ne sont pas supportées par IE

Les propriétés Unicode p{…} ne sont pas implémentées dans IE. Si nous en avons vraiment besoin, nous pouvons utiliser la librairie XRegExp.

Ou simplement utiliser des intervalles de caractères dans la langue qui nous intéresse, p. ex. [а-я] pour les lettres cyrilliques.

Intervalles d’exclusion

En plus des intervalles classiques, il existe des intervalles d’exclusion de la forme [^…].

Ils se distinguent par un premier accent circonflexe ^ et correspond à n’importe quel caractère à l’exception de ceux contenus dans ces crochets.

Par exemple :

  • [^aeyo] – n’importe quel caractère sauf 'a', 'e', 'y' ou 'o'.
  • [^0-9] – n’importe quel caractère à l’exception des chiffres, équivalent à \D.
  • [^\s] – tout caractère qui n’est pas un espacement, équivalent à \S.

L’exemple ci-dessous cherche n’importe quel caractère n’étant pas une lettre, un chiffre ou un espace :

alert( "alice15@gmail.com".match(/[^\d\sA-Z]/gi) ); // @ et .

L’échappement entre […]

Habituellement, lorsque nous cherchons précisément un caractère spécial, nous devons l’échapper \.. Et si nous cherchons un backslash, nous utilisons \\, etc.

À l’intérieur de crochets nous pouvons utiliser une grande majorité des caractères spéciaux sans échappement :

  • Les symbols . + ( ) ne sont jamais échappés.
  • Un tiret - n’est pas échappé en début ou fin d’ensemble (là où il ne peut pas définir d’intervalle).
  • Un accent circonflexe ^ est échappé uniquement s’il débute l’ensemble (sinon il signifie l’exclusion).
  • Le crochet fermant ] est toujours échappé (si nous le cherchons précisément).

En d’autres termes, tous les caractères spéciaux ne sont pas échappés, sauf s’ils ont un sens particulier pour un ensemble.

Un point . à l’intérieur de crochets signifie juste un point. Le motif [.,] recherche un caractère : soit un point soit une virgule.

Dans l’exemple ci-dessous l’expression régulière [-().^+] cherche un des caractères -().^+:

// Pas besoin d'échapper
let regexp = /[-().^+]/g;

alert( "1 + 2 - 3".match(regexp) ); // trouve +, -

… Si vous décidez de les échapper, “au cas où”, il n’y aura de toute façon aucun d’impact :

// Tout échappé
let regexp = /[\-\(\)\.\^\+]/g;

alert( "1 + 2 - 3".match(regexp) ); // fonctionne aussi: +, -

Intervalles et marqueur “u”

S’il y a une paire de seizets d’indirection(surrogate pair) dans l’ensemble, le marqueur u est requis pour qu’elle soit interprétée correctement.

Par exemple, cherchons [𝒳𝒴] dans la chaîne 𝒳:

alert( '𝒳'.match(/[𝒳𝒴]/) ); // affiche un caractère étrange qui ressemble à [?]
// (la recherche n'a pas fonctionné correctement, seule une moitié du caractère est retournée)

Le résultat n’est pas celui attendu, car par défaut une expression régulière ne reconnait pas une telle paire.

Le moteur d’expression régulière pense que [𝒳𝒴] – ne sont pas deux mais quatre caractères :

  1. la moitié gauche de 𝒳 (1),
  2. la moitié droite de 𝒳 (2),
  3. la moitié gauche de 𝒴 (3),
  4. la moitié droite de 𝒴 (4).

On peut voir le code de ces caractères ainsi :

for(let i=0; i<'𝒳𝒴'.length; i++) {
  alert('𝒳𝒴'.charCodeAt(i)); // 55349, 56499, 55349, 56500
};

Donc, le premier exemple trouve et affiche la première moitié de 𝒳.

Mais si nous ajoutons le marqueur u, on aura alors le comportement attendu :

alert( '𝒳'.match(/[𝒳𝒴]/u) ); // 𝒳

On retrouve un mécanisme similaire dans les intervalles, comme [𝒳-𝒴].

Si nous oublions le marqueur u, il y aura une erreur :

'𝒳'.match(/[𝒳-𝒴]/); // Error: Invalid regular expression

En effet sans le marqueur u une paire de seizets est perçue comme deux caractères distincts, donc [𝒳-𝒴] est interprété en [<55349><56499>-<55349><56500>] (chacune des paires est remplacée par ses codes). Il est maintenant évident que l’intervalle 56499-55349 n’est pas valide : le premier code 56499 est plus grand que le dernier 55349. Ce qui explique l’erreur précédente.

Avec le marqueur u le motif est interprété correctement :

// Cherche un caractère entre 𝒳 et 𝒵 compris
alert( '𝒴'.match(/[𝒳-𝒵]/u) ); // 𝒴

Exercices

Considérons l’expression rationnelle /Java[^script]/.

Trouve-t-elle quelque chose dans la chaîne de caractères Java? Dans la chaîne JavaScript?

Réponses : non, oui.

  • Dans la chaîne de caractères Java, elle ne trouve aucune correspondance, parce que [^script] signifie “n’importe quel caractère sauf ceux cités”. L’expression rationnelle cherche donc "Java" suivi d’un autre symbole, mais arrivant en fin de chaîne, elle n’en trouve aucun.

    alert( "Java".match(/Java[^script]/) ); // null
  • Oui, car la partie [^script] correspond au caractère "S". Qui n’est pas l’un des caractères de script. Comme l’expression rationnelle est sensible à la casse (pas de marqueur i), elle considère bien "S" différemment de "s".

    alert( "JavaScript".match(/Java[^script]/) ); // "JavaS"

L’heure peut être au format hours:minutes ou hours-minutes. Les nombres “hours” et “minutes” sont composées de deux chiffres : 09:00 ou 21-30.

Écrire une expression rationnelle pour trouver l’heure quelle que soit sa forme :

let regexp = /your regexp/g;
alert( "Breakfast at 09:00. Dinner at 21-30".match(regexp) ); // 09:00, 21-30

P.S. Dans cet exercice, on considère n’importe quelle heure comme valide, il n’y a pas besoin d’exclure une heure comme “45:67” par exemple. Nous nous occuperons de cela plus tard.

Réponse : \d\d[-:]\d\d.

let regexp = /\d\d[-:]\d\d/g;
alert( "Breakfast at 09:00. Dinner at 21-30".match(regexp) ); // 09:00, 21-30

A noter que '-' à un sens particulier entre crochet, mais seulement entre deux autres caractères, et pas lorsqu’il débute ou termine l’ensemble, nous n’avons donc pas besoin de l’échapper ici.

Carte du tutoriel

Commentaires

lire ceci avant de commenter…
  • Si vous avez des améliorations à suggérer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de préciser.
  • Pour insérer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen…)