Przewiń do treści 

🐈 mi się zepsuł

Opublikowany:
Autor:
Comandeer
Kategorie:
JavaScript

Jest taki upał, że nawet kot mi się zepsuł… Nie wierzycie? No to patrzcie:

JavaScript
'🐈'.length // 2

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, ż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:

JavaScript
[ ...'🐈' ].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:

JavaScript
[ ...'👨‍👦' ].length; // 3

Niekoniecznie jest to pożądany rezultat. Na całe szczęście jest Intl.Segmenter!

JavaScript
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.