Lowent 매뉴얼←↑→

unicode — 유니코드 속성 표

소스
lib/unicode.low
층
L0 — 순수 계산
권한
없음

“이 글자가 문자인가, 숫자인가, 공백인가” 를 묻는 곳이다. ASCII 만 다룰 때는 ge c 97 이고 le c 122 면 소문자라고 손으로 답할 수 있었다. '한' 이나 'あ' 나 'Ω' 가 들어오는 순간 그 방식은 무너진다. 유니코드에서 “문자” 인 코드포인트는 658 개의 흩어진 구간에 걸쳐 있어 손으로 적지 못한다. 이 모듈은 그 구간표를 Unicode 15.1.0 원본에서 기계적으로 뽑아 싣고 이진 탐색으로 답한다.

use unicode .

proc ident_start input cp u64 . output bool . effects none . do
  if unicode.is_letter cp . do return true . end
  return eq cp 95 .
end

is_letter 54620('한')은 true, is_letter 128512(😀)는 false 다 — 이모지는 기호이지 문자가 아니다. 입력은 코드포인트(u64)이지 바이트가 아니다. UTF-8 바이트열에서 코드포인트를 꺼내는 일은 utf8 이 하고, 둘을 이어 쓴다.

표는 데이터다 — 코드로 쓰면 거짓말이 섞인다. 이 모듈이 없으면 사람마다 자기 프로그램에 글자 판정을 손으로 적고, 그 판정은 거의 반드시 틀린다. 문제는 틀리는 방식이다. 빠뜨린 구간은 오류가 아니라 false — 조용히 틀린 답으로 나온다. 파서라면 그 글자에서 토큰이 끊기고, 검색이라면 그 낱말이 안 잡히며, 어디에서도 경고가 뜨지 않는다. 표시 폭(term 의 cp_width)은 틀려도 1 이라는 쓸 만한 기본값이 있었지만, 여기에는 그런 기본값이 없다.

설계와 경계#

표는 fn 이 돌려주는 slice u8(문자열 리터럴 — 복사도 할당도 없다)이고, 구조체도 상태도 없다. 표의 불변식은 넷이다 — 구간은 소문자 16 진 12 바이트, 시작 오름차순 정렬, 겹침 없음(인접 구간은 병합), 끝 포함(start ≤ cp ≤ end). 표를 직접 만들어 has_cp 에 넘긴다면 정렬과 겹침 없음을 지켜야 이진 탐색이 선다.

op 한눈에#

op하는 일
tab_letter · tab_number문자(L) 표 658 구간 · 숫자(N) 표 137 구간
tab_punct · tab_space · tab_mark문장부호(P) · 공백(Z, 탭과 개행은 들지 않는다) · 결합 표시(M) 표
tab_zerowidth폭 0(Mn·Me·Cf) 표 354 구간
tab_upper · tab_lower · tab_digit대문자(Lu) 646 · 소문자(Ll) 658 · 십진 숫자(Nd) 64 구간
tab_title · tab_modifier · tab_other_letterLt 10 · Lm 71 · Lo 509 구간
tab_numletter · tab_numotherNl 12 · No 72 구간
tab_toupper · tab_tolower대소문자 짝 표 1423 · 1432 짝
tab_casespecial1 대 1 로 바꾸지 못하는 코드포인트 103 개
range_count표의 구간 개수(len tab / 12)
has_cp표에 이 코드포인트가 드는가 — 이진 탐색
is_letter · is_number · is_punct · is_space · is_mark각 표의 술어
is_alnum문자 또는 숫자
is_upper · is_lower · is_digitLu · Ll · 십진 숫자(Nd)인가
is_zerowidth폭 0(결합 문자·서식 문자)인가
is_title · is_modifier · is_other_letter · is_numletter · is_numotherLt · Lm · Lo · Nl · No 인가
to_upper · to_lower대문자로 · 소문자로(바뀌지 않으면 그대로)
is_special_case1 대 1 로 바꾸지 못하는 코드포인트(ß 따위)인가

표 50.1 — unicode 의 op

op 상세#

쓰는 법#

UTF-8 문자열에서 낱말(문자·숫자가 이어진 덩어리)을 센다.

module wordcount .

use unicode .
use utf8 .

export proc count_words input s slice u8 . output option u64 . do
  var off u64 be 0 .
  var words u64 be 0 .
  var inword bool be false .
  while lt off (len s) . do
    let cp option u64 . be utf8.decode s off .
    guard is_some cp . else return none .
    let n u64 be utf8.seq_len (index s off) .
    guard gt n 0 . else return none .
    if unicode.is_alnum (some_value cp) . do
      if eq inword false . do set words (add words 1) . end
      set inword true .
    end
    if eq (unicode.is_alnum (some_value cp)) false . do
      set inword false .
    end
    set off (add off n) .
  end
  return some words .
end

"한글 word 123" 에서 3 이 나온다 — ASCII 만 아는 판정으로는 '한글' 을 세지 못한다. 정규식의 \p{L}·\P{L} 이 바로 이 표를 쓴다 (regex). 정규식으로 충분하면 정규식을 쓰고, 코드포인트 하나만 물으면 되는 자리에서는 이 모듈을 직접 부르는 편이 훨씬 싸다.

반례#

반례. 바이트를 그대로 넘긴다

rem ✘ index 는 바이트를 준다. '한' 의 첫 바이트는 0xED 다
if unicode.is_letter (widen u64 (index s 0)) . do … end

0xED(237)는 코드포인트 U+00ED(í)다 — 우연히 문자로 판정되지만 묻고 있던 글자가 아니다. UTF-8 을 다루면 반드시 utf8.decode 를 거친다.

반례. 폭을 세면서 is_mark 를 쓴다

Mc(자기 칸을 차지하는 결합 표시)까지 0 으로 세게 된다. 폭에는 is_zerowidth 다 — 애초에 폭은 term 의 cp_width 가 다 처리한다.

반례. to_upper 가 늘 바꿔 준다고 여긴다

ß 는 그대로 온다. 정확해야 하면 먼저 is_special_case 로 묻고, 두 글자 이상으로 펼치는 일은 호출자가 정한다.

반례. is_number 로 자릿수를 계산한다

rem ✘ Ⅶ(U+2166)도 통과하고 cp − 48 은 뜻 없는 수다
if unicode.is_number cp . do set v (add (mul v 10) (sub cp 48)) . end

십진 숫자만 받으려면 is_digit 이다.

반례. is_upper 가 false 라서 소문자로 여긴다

한글·한자·아랍 문자는 둘 다 false 다. 그 판정은 세상 대부분의 글자를 소문자로 분류한다. 소문자인지는 is_lower 로 직접 묻는다.

주의#