Jest taki upał, iż choćby kot mi się zepsuł… Nie wierzycie? No to patrzcie:
Czemu 🐈 się psuje?
Rzućmy okiem do MDN-u:
The length data property of a String value contains the length of the string in UTF-16 code units.
[Własność length cią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, iż 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, iż 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:
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:
Niekoniecznie jest to pożądany rezultat. Na całe szczęście jest Intl.Segmenter!
Najpierw tworzymy instancję segmentera (1). Informujemy go, iż dzielony tekst będzie po polsku oraz iż 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!



