Extraire efficacement une sous-chaîne de texte
Par Christophe Porteneuve • Publié le 5 mai 2020 • 4 min

This page is also available in English.

Voici le deuxième article de notre série quotidienne « 19 pépites de JS pur ». Cette fois-ci, nous allons parler d’extraire une portion de chaîne de caractères, et voir que pas moins de 3 méthodes se tirent la bourre… mais qu’une seule mérite votre attention 😉

Dans la série…

Extrait de la liste des articles :

  1. Dédoublonner efficacement un tableau
  2. Extraire efficacement une sous-chaîne de texte (cet article)
  3. Formater proprement un nombre
  4. Array#splice, le couteau suisse
  5. …au-delà, c’est la surprise ! (mais les 19 sont déjà calés)…

Le truand : substr(…)

Le savais-tu ? Les chaînes ont une méthode substr. Non, tu le savais pas ? Eh bah tant mieux ! On ne peut pas lui faire confiance, et en prime elle n’est pratique.

  • Elle n’est pas vraiment officielle. Elle figure à l’annexe B de la spec, qui est certes «normative» depuis ES2015 mais n’était qu’«informative» auparavant, et concerne les parties du langage et de sa bibliothèque qui n’ont jamais été très propres et sont activement découragées, parfois depuis bien longtemps (pour substr, ça remonte à ES3, en 1999, les gens).
  • Elle a une signature inhabituelle : substr(index, longueur). Oui, longueur. Pas deux index, mais un index et une longueur.
  • Elle a des implémentations incompatibles. En particulier, alors qu’elle autorise explicitement les index négatifs pour partir de la fin (ça c’est bien !), cet aspect ne marche pas sur JScript, le moteur JS d’Internet Explorer avant la 9.0.
'MAMIE c’est la lozere !'.substr(3, 16).replace('z', 's')
// => … tu sais que tu vas vouloir tester ce code ;-)
// -> https://bit.ly/mamie-lozere

En plus, elle a un nom bien pourri, bien tronqué n’importe comment, qui n‘est pas sans rappeller les jours sombres des premières versions de PHP (nl2br, oui, je pense à toi, et à bien d’autres).

Bref, jette-moi ça aux oubliettes.

La brute : substring(…)

Beaucoup de monde utilise substring. Beaucoup de monde. Beaucoup trop de monde. C’est un peu comme cette !@# de parseInt : tout le monde se dit que non, en fait, ça va, je gère. Et juste quand tu mets ton truc le plus critique de ta vie en prod’, paf le chien. Le bug caché. Le piège sournois.

Bon, au moins, le nom est clair, rien à dire. Et les arguments sont des index, c’est cool.

MAIS— !

  • Les index ne peuvent pas être négatifs (pas de confort d’indexation de fin de chaîne)
  • Y’a une Grosse Blague™ si le deuxième est inférieur au premier.
'je le laisse tranquille'.substring(10, 0).replace('la', 'ha')
// => Mais 😤💩 quoi !

Tu as deviné ? Et oui, si le deuxième index est inférieur au premier, ça les inverse ! What could go wrong?! Évidemment, c’est carrément ce que tu veux, comme quand new Date(2020, 0, -6) te donne Noël 2019, c’est logique, quoi !

Allez, recalé !

Le bon : slice(…)

Le voilà, notre copain ! Tu connais sûrement slice sur les tableaux, eh bien il existe aussi sur les chaînes, avec exactement la même API, ce qui est pratique : on a bien assez d’API distinctes à mémoriser, alors quand on peut réutiliser… Pas mal de points positifs, donc :

  • 100% API-compatible avec le slice de Array
  • Donc deux index, autorisant le négatif tous les deux (et comme d’hab le deuxième est exclusif)
  • Pas d’inversion à la noix si le second est inférieur au premier

Bref, que du bonheur ! 😍

Il y a aussi deux aspects sympa, mais qu’il partage avec les deux précédents, ce ne sont donc pas à proprement parler des avantages sur eux, mais je les liste quand même :

  • Si on omet le deuxième index, on va jusqu’au bout de la chaîne
  • Si on omet même le premier argument, on démarre au début de la chaîne
'<love>'.slice(1, -1)
// => 'love
'Living on the Edge'.slice(-4)
// => 'Edge'

Enfin !

« Oui mais c’est pas kawaï ! »

Tu l’as compris, slice est mon ami. Et pourtant, comme toute l’API traditionnelle de String, il trébuche souvent sur Unicode. On y reviendra bientôt (Spoiler Alert), mais les chaînes JS sont, comme en Java (argh !), encodées en UCS-2 / UTF-16LE, et ce que l’API appelle à tort des caractères (charAt, charCodeAt, etc.) sont en fait des code units de 16 bits (2 octets). Ça suffit amplement pour les caractères latins, les chiffres, la ponctuation occidentale classique, mais dès qu’on franchit un certain niveau de codepoints Unicode, par exemple avec certains idéogrammes chinois, kanjis japonais ou carrément Emoji, ça ne tient plus, et il nous faut une surrogate pair :

'😍'.length // => 2 🤔
'😍 👨‍👩‍👦‍👦'.length // => 14 😱

Et oui, '😍' comprend en fait deux code units. Normalisée en source ASCII, elle s’écrirait '\ud83d\ude0d'. Super, hein ? Un Emoji, mais une chaîne de « longueur » 2. Un codepoint, deux code units qui forment une surrogate pair. Du coup :

'En vrai 😍'.slice(8, 9) // => caractère invalide

Alors comment faire pour extraire un segment en « parlant de codepoints » ? Si tu en as vraiment besoin, tu peux profiter du fait que depuis ES0215 les String sont itérables par codepoints, et non par code units. Fais-en un tableau, slice le tableau, et recompose la chaîne :

Array.from('En vrai 😍').slice(8, 9).join('') // => '😍'

Ouf ! Bon après quand tu as des combinaisons de codepoints à coup de ZWJ (Zero-Width Joiners), ça ne suffira pas toujours…

Array.from('😍 👨‍👩‍👦‍👦').slice(2, 3).join('')
// => '👨' -- Monsieur se retrouve tout seul tout à coup

…mais quand même, avec un peu de chance on peut réunir toute la famille :

Array.from('😍 👨‍👩‍👦‍👦').slice(2).join('') // => '👨‍👩‍👦‍👦'

Tout est bien qui finit bien !