"👩💻🎉".length = 7 ??? Comment compter les émojis avec Javascript
cestoliv, il y a 2 ans - lun. 14 nov. 2022
Dans cet article
1. Le problème
Cette semaine, un ami a rencontré un problème en Javascript lorsqu'il voulait vérifier que son utilisateur ne saisissait qu'un seul caractère dans un champ de texte.
En effet, la première solution à laquelle on pense est de regarder la longueur de la chaîne, mais des problèmes surviennent lorsque cette chaîne contient des émojis :
"a".length // => 1
"🛏".length // => 2 ??
En réalité, c'est assez logique, sachant que la fonction .length
en Javascript retourne la longueur de la chaîne en unités de code UTF-16, et non le nombre de caractères visibles.
2. Une première solution avec l'opérateur de spreading
La première solution à laquelle j'ai pensé était de diviser la chaîne sur chaque caractère, puis de compter le nombre d'éléments :
"🛏".split('') // => ["�","�"]
"🛏".split('').length // => 2
Aïe... Malheureusement, .split('')
divise aussi en unités de code UTF-16.
Mais il existe un autre moyen de diviser une chaîne sur chaque caractère en Javascript, en utilisant l'opérateur de propagation :
[..."🛏"] // => ["🛏"]
[..."🛏"].length // => 1, Hourra !!
[..."🛏🎉"] // => ["🛏", "🎉"]
[..."🛏🎉"].length // => 2, Hourra !!
[..."👩💻"] // => ["👩", "\u{200D}", "💻"]
[..."👩💻"].length // => 3, Oups...
Mince... Toujours pas, malheureusement pour nous, certains émojis sont composés de plusieurs émojis, séparés par un "" (U+200D, un Zero Width Joiner) :
[..."👩💻"] // => ["👩", "\u{200D}", "💻"]
[..."👩💻👩❤️💋👩"] // => ["👩", "\u{200D}", "💻", "👩", "\u{200D}", "❤", "\u{fe0f}", "\u{200D}", "💋", "\u{200D}", "👩"]
3. Un deuxième algorithme avec Zero Width Joiner
Comme on peut le voir dans cet exemple, pour compter le nombre de caractères visibles, on peut compter le nombre de fois où deux caractères qui NE SONT PAS des Zero Width Joiner sont côte à côte.
Par exemple :
[..."a👩💻🎉"]
// => ["a", "👩", "\u{200D}", "💻", "🎉"]
// | 1 | 2 | 3 |
Nous pouvons donc en faire une fonction simple :
function visibleLength(str) {
let count = 0;
let arr = [...str];
for (c = 0; c < arr.length; c++) {
if (
arr[c] != '\u{200D}' &&
arr[c + 1] != '\u{200D}' &&
arr[c + 1] != '\u{fe0f}' &&
arr[c + 1] != '\u{20e3}'
) {
count++;
}
}
return count;
}
visibleLength('Hello World'); // => 11
visibleLength('Hello World 👋'); // => 13
visibleLength("I'm going to 🛏 !"); // => 16
visibleLength('👨👩👧👦'); // => 1
visibleLength('👩💻👩❤️💋👩'); // => 2
visibleLength('🇫🇷'); // => 2 AAAAAAAAAAAAAA!!!
Notre fonction fonctionne dans de nombreux cas, mais pas dans le cas des drapeaux, car les drapeaux sont constitués de deux lettres-émojis placées côte à côte, mais elles ne sont pas séparées par un Zero Width Joiner, elles sont simplement transformées en drapeaux par les plateformes qui les prennent en charge.
[..."🇫🇷"] // => ["🇫", "🇷"]
[..."🇺🇸"] // => ["🇺", "🇸"]
4. La meilleure solution (à utiliser en production)
L'une des meilleures solutions dont nous disposons pour gérer tous ces cas est d'utiliser un algorithme de Grapheme capable de séparer les chaînes en phrases, mots ou caractères visibles.
Heureusement pour nous, Javascript intègre cet algorithme nativement : Intl.Segmenter
C'est très simple à utiliser, ET ÇA FONCTIONNE AVEC TOUS LES CARACTÈRES ! :
function visibleLength(str) {
return [...new Intl.Segmenter().segment(str)].length
}
visibleLength("I'm going to 🛏 !") // => 16
visibleLength("👩💻") // => 1
visibleLength("👩💻👩❤️💋👩") // => 2
visibleLength("France 🇫🇷!") // => 9
visibleLength("England 🏴!") // => 10
visibleLength("と日本語の文章") // => 7
Il y a juste un petit problème, Intl.Segmenter()
n'est pas du tout compatible avec Firefox (Desktop et Mobile) et Internet Explorer.
![[100 - Obsidian/Media/e24bb60a4913b5ee6017cd580890b446_MD5.jpeg|Mozilla Developer Network screenshot of Intl Segmenter Browser compatibility]]
4.1 L'utiliser sur Firefox/IE
Pour rendre cette solution compatible avec Firefox, nous devons utiliser ce polyfill : https://github.com/surferseo/intl-segmenter-polyfill
Comme le fichier est très volumineux (1,77 Mo), nous devons nous assurer qu'il n'est chargé que pour les clients qui ne prennent pas encore en charge Intl.Segmenter()
.
Parce que cela sort un peu du cadre de cet article, voici simplement ma solution :
### 4.2. L'utiliser avec TypeScriptComme cette implémentation est relativement récente, si vous souhaitez utiliser Intl.Segmenter()
avec TypeScript, assurez-vous d'avoir au moins ES2022 comme cible :
// Fichier : tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
// [...]
}
// [...]
}
Merci d'avoir lu !