28 septembre 2022

Marqueur collant "y", recherche depuis une position

Le marqueur y permet d’effectuer une recherche à partir d’une position donnée dans la chaîne de caractères source.

Pour appréhender le cas d’usage du marqueur y et mieux comprendre le fonctionnement des regexps, regardons un exemple pratique.

Parmi les usages courants des regexps, l’analyse lexicale : Avec un texte donné, p. ex. dans un langage de programmation, nous avons besoin de trouver ses éléments de structure. Par exemple, l’HTML a des balises et des attributs, le code JavaScript a des fonctions, variables, etc.

L’écriture d’analyseurs lexicaux est un domaine spécifique, avec ses propres outils et algorithmes que nous n’explorerons pas ici, mais il y a une tâche courante : Lire quelque chose depuis une position donnée.

P. ex. prenons la chaîne de caractères let varName = "value", dans laquelle nous devons lire le nom de la variable, qui commence à la position 4.

Nous chercherons un nom de variable en utilisant la regexp \w+. Les noms de variable en JavaScript nécessitent en fait pour un résultat exact, une regexp un peu plus complexe, mais c’est sans importance ici.

  • Un appel à str.match(/\w+/) trouvera seulement le premier mot de la ligne (let). Ça n’est pas ça.
  • Nous pouvons ajouter le marqueur g. Mais alors l’appel à str.match(/\w+/g) cherchera tous les mots du text, alors que nous avons besoin que d’un mot à partir de la position 4. Ça n’est donc pas encore ça.

Alors comment rechercher un motif à partir d’une position donnée ?

Essayons en utilisant la méthode regexp.exec(str).

Pour une regexp sans marqueur g ni y, cette méthode cherche seulement la première occurrence, cela fonctionne exactement comme str.match(regexp).

… Mais s’il y a le marqueur g, il effectue alors une recherche dans str, à partir de la position stockée dans la propriété regexp.lastIndex. Et s’il trouve une correspondance, il fixe regexp.lastIndex à l’index immédiatement après cette correspondance.

En d’autres termes, regexp.lastIndex sert de point de départ pour la recherche, puis chaque appel à regexp.exec(str) la change en une nouvelle valeur (“après la dernière correspondance”). Cela, bien entendu, uniquement avec le marquer g.

Donc chaque appel successif à regexp.exec(str) retourne une correspondance après l’autre.

Voici un exemple de tels appels :

let str = 'let varName'; // Cherchons tous les mots dans cette chaîne de caractères
let regexp = /\w+/g;

alert(regexp.lastIndex); // 0 (initialement lastIndex=0)

let word1 = regexp.exec(str);
alert(word1[0]); // let (1er mot)
alert(regexp.lastIndex); // 3 (position après la première correspondance)

let word2 = regexp.exec(str);
alert(word2[0]); // varName (2e mot)
alert(regexp.lastIndex); // 11 (position après la seconde correspondance)

let word3 = regexp.exec(str);
alert(word3); // null (plus aucune correspondance)
alert(regexp.lastIndex); // 0 (réinitialisé à la fin de la recherche)

Nous pouvons ainsi obtenir toutes les correspondances dans la boucle :

let str = 'let varName';
let regexp = /\w+/g;

let result;

while (result = regexp.exec(str)) {
  alert( `Found ${result[0]} at position ${result.index}` );
  // Found let at position 0, puis
  // Found varName at position 4
}

Une telle utilisation de regexp.exec est une alternative à la méthode str.matchAll, avec un peu plus de contrôle sur le processus.

Retournons à notre objectif.

Nous pouvons assigner à lastIndex la valeur 4, pour commencer la recherche à partir de cette position !

Comme ceci :

let str = 'let varName = "value"';

let regexp = /\w+/g; // sans le marqueur "g", la propriété lastIndex est ignorée

regexp.lastIndex = 4;

let word = regexp.exec(str);
alert(word); // varName

Houra ! Problème résolu !

Nous avons recherché le motif \w+, à partir de la position regexp.lastIndex = 4.

Le résultat est valide.

…Mais attendez, pas si vite.

Vous noterez : l’appel à regexp.exec commence la recherche à la position lastIndex et continue ensuite plus loin. S’il n’y a pas de mot à la position lastIndex, mais qu’il y en a un plus loin, c’est celui-ci qui sera trouvé :

let str = 'let varName = "value"';

let regexp = /\w+/g;

// comme la recherche à la position 3
regexp.lastIndex = 3;

let word = regexp.exec(str);
// trouve la correspondance à la position 4
alert(word[0]); // varName
alert(word.index); // 4

Pour certaines tâches, et pour les analyses lexicales en particulier, c’est complètement faux. Nous avons besoin de trouver la correspondance du motif à la position exacte, et non quelque part plus loin. Et c’est justement ce que fait le marqueur y.

Le marqueur y fait que regexp.exec recherche exactement à la position lastIndex, et non à partir de cette position.

Voici la même recherche avec le marqueur y:

let str = 'let varName = "value"';

let regexp = /\w+/y;

regexp.lastIndex = 3;
alert( regexp.exec(str) ); // null (il n'y a pas de mot en position 3, mais un espace)

regexp.lastIndex = 4;
alert( regexp.exec(str) ); // varName (mot en position 4)

Comme nous pouvons le voir, la regexp /\w+/y ne trouve pas de correspondance en position 3 (contrairement au marqueur g), mais trouve la correspondance en position 4.

En plus d’obtenir ce que nous cherchions, il y a un gain significatif de performance avec le marqueur y.

Imaginez avec un long texte, sans aucune correspondance dedans. Une recherche avec le marqueur g ira alors jusqu’à la fin du texte pour ne rien trouver, et cela prendra bien plus de temps qu’avec le marqueur y, qui vérifie seulement à la position exacte.

Dans des tâches comme en analyse lexicale, il y a habituellement beaucoup de recherches sur des positions exactes, pour vérifier ce qu’il s’y trouve. L’utilisation du marqueur y est la clé pour des bonnes implémentations et de bonnes performances.

Carte du tutoriel