🐈 mi się zepsuł
Jest taki upał, że nawet kot mi się zepsuł… Nie wierzycie? No to patrzcie:
'🐈'.length // 2
Czemu 🐈 się psuje?
Rzućmy okiem do MDN-u:
The
lengthdata property of aStringvalue contains the length of the string in UTF-16 code units.[Własność
lengthciągów tekstowych zawiera długość tego ciągu w jednostkach kodowych UTF-16.]
No dobrze, ale co to są “jednostki kodowe” (code units)? Sięgnijmy raz jeszcze do przepastnej skarbnicy MDN-u:
A character encoding system uses one or more code units to encode a Unicode code point. […] In UTF-16, each code point is encoded using one or two 16-bit code units.
[System kodowania znaków używa jednego lub więcej jednostek kodowych, by zakodować punkt kodowy. […] W UTF-16 każdy punkt kodowy jest zapisywany przy pomocy jednej lub dwóch 16-bitowych jednostek kodowych.]
Punkty kodowe można porównać do współrzędnych znaku w wielkiej tabeli wszystkich znaków standardu Unicode. W praktyce oznacza to, że każdy znak zareprezentować można jako liczbę, np. litera A to U+0041, natomiast 🐈 to U+1F408.
Czemu jednak JS poprawnie rozpoznaje A jako ciąg tekstowy zawierający jeden znak, podczas gdy 🐈 jako dwa? Wynika to z ograniczenia UTF-16, a dokładniej: 16-bitowa jednostka kodowa może zakodować co najwyżej 216 (65536) znaków. Standard Unicode zawiera ich zdecydowanie więcej, więc część znaków jest kodowana przy pomocy dwóch jednostek kodowych. I do tej grupy zaliczają się także emoji. Cały problem polega na tym, że String#length zwraca liczbę jednostek kodowych w ciągu, nie – liczbę znaków. Stąd 🐈 jest zepsuty.
Dygresja
Zasady działania standardu Unicode w JS-ie są mocno zawiłe, niemniej MDN zawiera dokładniejszy opis całego mechanizmu.
Jak naprawić 🐈?
Jeśli chcemy policzyć znaki w ciągu tekstowym, o wiele bezpieczniejszym sposobem jest wykorzystanie wbudowanego w ciągi iteratora:
[ ...'🐈' ].length; // 1
Ale oczywiście nie może być za prosto! Istnieje jeszcze coś takiego jak klastry grafemów (grapheme clusters). W wielkim uproszczeniu: niektóre znaki mogą być tak naprawdę połączeniem wielu różnych znaków, np. 👨👦 to połączenie trzech znaków: 👨, (łącznika o zerowej szerokości) oraz 👦. I iterator ciągu rozbije takie emoji na poszczególne znaki składowe:
[ ...'👨👦' ].length; // 3
Niekoniecznie jest to pożądany rezultat. Na całe szczęście jest Intl.Segmenter!
const segmenter = new Intl.Segmenter( 'pl', { granularity: 'grapheme' } ); // 1
console.log( [ ...segmenter.segment( '👨👦' ) ].length ); // 2
Najpierw tworzymy instancję segmentera (1). Informujemy go, że dzielony tekst będzie po polsku oraz że chcemy dzielić po grafemach. Następnie, przy pomocy metody Intl.Segmenter#segment(), dzielimy ciąg zawierający klaster grafemów (2). Tym razem uzyskujemy wynik 1 – jeden widoczny znak został policzony jako jeden znak.
I tym sposobem 🐈 został naprawiony!
Komentarze
Przejdź do komentarzy bezpośrednio na Githubie.