합동회사 코무라소프트
5장

UTF-16 과 서로게이트 페어 — 문자 수의 함정

UTF-16 은 16 비트 단위의 인코딩. BMP 밖의 문자는 서로게이트 페어로 2 단위가 되고, C# 이나 JavaScript 의 length 는 눈에 보이는 문자 수와 일치하지 않게 된다.

「3 문자의 이름이 들어가지 않는다」— 문자 수 세는 법의 사건

「성명은 4 문자까지」일 터인 폼에 일본어 「𠮷野家」(요시노야)의 3 문자가 들어가지 않는다. 이모지 2 개의 게시물이 「4 문자」로 카운트된다. C# 에서 Length 를 보고 잘라냈더니 문자의 잔해가 섞였다. — 이 장은 「바이트 수」에 이은 또 하나의 수의 어긋남, 「문자 수」를 세는 방법의 이야기입니다.

주역은 UTF-16. Windows 의 내부, Java·C#·JavaScript 의 문자열 타입이 채용하고 있는 인코딩으로, 16 비트(2 바이트)의 코드 단위를 기본으로 합니다. 일본어 문자 「あ」 U+3042 라면, 그대로 16 비트에 들어가 1 단위 — 여기까지는 순조롭습니다.

그러나 제 2 장에서 본 대로, Unicode 의 번호는 U+10FFFF 까지 있습니다. 16 비트로 나타낼 수 있는 U+FFFF 까지의 범위를 BMP(기본 다국어 평면)라고 부르는데, 이모지(U+1F600 대)나 「𠮷」(U+20BB7) 같은 문자는 BMP 의 바깥쪽. 16 비트 1 개에는 들어가지 않습니다.

서로게이트 페어 — 2 단위로 1 문자를 나타낸다

UTF-16 의 답은, 2 개의 코드 단위를 페어로 만들어 1 문자를 나타내는 것이었습니다. 절차는 UTF-8 의 템플릿과 많이 닮았습니다.

Step 1 — 0x10000 을 뺀다
U+20BB7 − 0x10000 = 0x10BB7. BMP 밖의 번호는, 이렇게 하면 반드시 20 비트에 들어간다
Step 2 — 20 비트를 10 + 10 으로 나눈다
상위 10 비트를 0xD800 에 더해 고위 서로게이트 D842, 하위 10 비트를 0xDC00 에 더해 저위 서로게이트 DFB7
Step 3 — 2 단위를 나란히 놓는다
「𠮷」 = D842 DFB7. D800〜DFFF 는 단독으로는 문자가 되지 않는 예약 영역이므로, 읽는 쪽은 페어임을 즉시 알 수 있다

이 2 단위 1 조를 서로게이트 페어라고 부릅니다. 이모지도 같은 구조로, 「😀」 U+1F600 은 D83D DE00 의 2 단위입니다.

일본어 「あ」는 UTF-16 에서 1 코드 단위, 「𠮷」 U+20BB7 은 서로게이트 페어 D842 DFB7 의 2 코드 단위가 됨을 보여 주는 그림

BMP 내의 「あ」는 1 단위, BMP 밖의 「𠮷」는 2 단위. length 가 세고 있는 것은 이 「상자의 수」이지, 눈에 보이는 문자 수가 아닙니다.

소문제 5-1 — UTF-16 과 서로게이트 페어

16 비트 단위의 세계에서, 큰 번호의 문자가 어떻게 표현되는지를 확인합니다.

Q1. UTF-16 의 설명으로 올바른 것은 어느 것입니까.

Q2. 일본어 한자 「𠮷」(쓰치요시)의 코드 포인트는 U+20BB7 로, U+FFFF 를 넘습니다. UTF-16 에서는 어떻게 표현됩니까.

Q3. C# 의 string.Length 나 JavaScript 의 .length 는 UTF-16 의 코드 단위 수를 돌려줍니다. 일본어 「𠮷野家」(𠮷 = U+20BB7, 野·家는 BMP 내)의 length 는 얼마입니까.

length 가 돌려주는 것은 「상자의 수」

여기서 서두의 사건이 풀립니다. C# 의 string.Length 도 JavaScript 의 .length 도, 돌려주고 있는 것은 UTF-16 의 코드 단위 수 — 즉 서로게이트 페어의 「상자」를 2 로 센 값입니다.

문자열눈에 보이는 문자 수length(코드 단위)
あいう3 문자3모두 BMP 내에서 1 단위씩
𠮷野家3 문자4𠮷 가 2 단위
😀😀😀3 문자6이모지는 각 2 단위

이것은 처리계의 버그가 아니라 사양대로입니다. 위험한 것은, length 를 「문자 수」로 바꿔 읽어 문자 수 제한 체크선두 N 문자 잘라내기에 사용할 때. 잘라내기가 페어의 한가운데에 떨어지면, 고위 서로게이트만 남은 「깨진 문자」가 생겨, 후단의 처리나 데이터베이스에서 오류의 씨앗이 됩니다.

한 걸음 더 — 결합 문자와 「눈에 보이는 1 문자」

실은 「코드 포인트 단위로 세면 문자 수가 된다」도, 아직 정답이 아닙니다. 가족 이모지 「👨‍👩‍👧」는, 「남성」 「여성」 「여아」의 3 개의 이모지를 제로 폭 접합자(ZWJ)라는 보이지 않는 코드 포인트로 이은 것으로, 코드 포인트는 5 개, UTF-16 의 코드 단위로는 8 개. 그런데도 화면에 보이는 것은 1 문자입니다. 일본어 「が」를 「か + 탁점」의 2 코드 포인트로 나타내는 결합 문자도 같은 부류입니다(한글이 자모의 결합으로 표현되는 경우도 마찬가지입니다).

사람의 눈에 비치는 1 문자에 대응하는 단위는 그래핌 클러스터(grapheme cluster)라고 불리며, 세려면 전용 API(JavaScript 의 Intl.Segmenter 등)를 사용합니다. 정리하면, 「문자 수」에는 적어도 4 가지 세는 방법 — 바이트·코드 단위·코드 포인트·그래핌 — 이 있고, 어느 것을 세고 있는지를 말할 수 있는 것이 이 장의 목표입니다. 제 3 장의 시뮬레이터에 「𠮷野家」나 가족 이모지를 넣어, 4 가지 수가 전부 어긋나는 것을 확인해 보세요.

소문제 5-2 — length 의 덫과 결합 문자

「문자 수를 센다」는 것의 의외의 어려움을, 이모지로 마무리합니다.

Q4. 이모지 「😀」는 U+1F600 으로, UTF-16 에서는 2 코드 단위입니다. JavaScript 에서 「😀😀😀」(이모지 3 개)의 .length 는 얼마입니까.

Q5. length(Length)가 「눈에 보이는 문자 수」와 어긋나는 장면에서의 올바른 이해는 어느 것입니까.

Q6. 가족 이모지 「👨‍👩‍👧」는, 복수의 이모지를 제로 폭 접합자(ZWJ)로 이은 것으로, JavaScript 의 .length 는 8 이 됩니다. 「눈에 보이는 1 문자」를 올바르게 세고 싶을 때의 적절한 단위는 어느 것입니까.

이 장에서 가져갈 것

  • UTF-16 은 16 비트의 코드 단위가 기본. BMP(U+FFFF 까지)는 1 단위, BMP 밖은 서로게이트 페어로 2 단위
  • 「𠮷」 U+20BB7 = D842 DFB7, 「😀」 U+1F600 = D83D DE00
  • C#·JavaScript 의 length 는 코드 단위 수. 「𠮷野家」는 4, 「😀😀😀」는 6
  • 눈에 보이는 1 문자 = 그래핌 클러스터. 바이트·코드 단위·코드 포인트·그래핌의 4 가지 세는 방법을 구별한다

인코딩의 구조는 여기까지로 모두 나왔습니다. 다음 장부터는 실무편 — 「선언과 실체의 어긋남」이 낳는 현장의 트러블을 정리해 갑니다.