Go
golang
unicode

Goのruneを理解するためのUnicode知識

string? byte? rune?

最近LeetCodeというサイトにあるコーディング問題をGoでちまちま解き進めている。
コーディング問題では普段はしないような処理を書くことがあるが、その中で意外にも詰まってしまったのが「文字列を一文字ずつ読んでいく」というものだった。

具体的にはstringにインデックスでアクセスするとbyteが取得でき、rangeでループするとruneが取得できるという点で混乱してしまった。printlnすると謎の数字が出てくるし一体何なんだっけ?という感じだ。

s := "abcde"

for i := 0; i < len(s); i++ {
    b := s[i]       // byte
    fmt.Println(b)  // 227, 129, 130...
}

for _, r := range s {
                    // rune
    fmt.Println(r)  // 12354, 12356, 12358
}

以下、byteやruneの数字の正体を理解するために必要と思う知識をまとめてみた。

文字コードとcode point

Gopherはかわいい

このような文字列もコンピュータから見れば0と1からなるビット列のデータだ。このデータを人間が読める文字に変換するには、各種文字と01の並びを対応づける必要がある。その仕組みが文字コードであり、その一つのUnicodeは世界中のあらゆる文字を収録するために作られている規格だ。
例えば a という文字はUnicodeでu+0041と表されるが、これは16進数で0041という値と対応づけられていることを意味している。この値はcode point、別名code positionと呼ばれていて、文字コード表の中の位置=pointを指し示す文字のIDと言える。

https://unicode-table.com

符号化方式

「符号化方式」とは41とか1F601とかいったcode pointの値をコンピュータが扱うデータ形式に置き換えるための規則だ。方式によってcode pointを何byte単位で扱うかが異なってくる。
Goでは符号化方式としてUTF-8を使用している。UTF-8では1byteから4byteの可変長データでcode pointを置換する。数字やアルファベットのような広く使われている文字は1byteになるし、Unicodeに後から追加された絵文字なんかは4byteになる。Unicodeの符号化方式には他にもUTF-16やUTF-32があって、それぞれ2byteの固定長、4byteの固定長でcode pointを表す。

各符号化方式でのbyte表現の例を示す。16進数表記なので2桁で1byteとなる。

文字 : code point UTF-8 UTF-16 UTF-32
a : 0061 61 00 61 00 00 00 61
あ : 3042 E3 81 82 30 42 00 00 30 42
😨 : 1F628 F0 9F 98 A8 D8 3D DE 28 00 01 F6 28

stringとbyte

code pointとUTF-8についてわかったところで冒頭のコードに戻る。
stringにインデックスでアクセスしたときに得られるbyte値は文字コードをUTF-8で1byteごとに区切った値だ。例えばという文字にインデックスでアクセスしたときに得られる値はちょうど上表のUTF-8でのbyte表現と一致する。

s := "あ"
for i := 0; i < len(s); i++ {
    fmt.Printf("% x", s[i]) // e3 81 82
}

stringとrune

上記のとおり、ひとつの文字は複数byteで表現される可能性があり、文字を表すbyteをまとめて読まないと正しい文字として認識できなくなってしまう。文字列の処理をUTF-16(つまり2byte単位)で行うJavascriptでは絵文字のような4byte文字にインデックスでアクセスしようとすると正しい文字を得ることができない。

// Javascriptのコード
var s1 = "やばいよ";
console.log(s1[3]);     // よ
console.log(s1.length); // 4
var s2 = "やばい😨";
console.log(s2[3]);     // �
console.log(s2.length); // 5 -> 2byteを1単位としてlengthを表すため

そのため文字を数える単位としてはbyteではなくcode pointのほうが都合がいい。そしてGoではcode pointを単位として文字を扱うための仕組み、つまりruneを用意している。

runeの実体はint32のエイリアスだ。Unicodeの4byte、最大1,114,112文字分のcode pointを表現するためのint32である。runeは文字をシングルクオートで囲って書くので、人間からの見た目上はstringのように見えてしまう。しかし実体としてはint32の値だから計算もできるし、rune同士の演算もできる。

fmt.Println('a')        // 97
fmt.Println('a' * 2)    // 194
fmt.Println('a' - 'b')  // -1

この性質と数字のcode pointが順番に並んでいることを利用すると、0〜9からなる文字列をひとつずつint32型の値に変換したい場合は次のコードでいける。

for _, r := range "9876543210" {
    intVal := int(r - '0')  // 9, 8, 7...
}

またstringは[]runeと[]byteにキャストすることができるが、文字列が対応するcode pointとbyteデータに姿を変えているだけと考えれば何の不思議もない。

s := "あいう"
fmt.Println([]rune(s)) // [12354 12356 12358]
fmt.Println([]byte(s)) // [227 129 130 227 129 132 227 129 134]

runeを意識したstring操作例

以下のコードは👨‍👩‍👧‍👦のような複数の絵文字が組み合わさった文字が含まれる場合までは考慮していないことに注意。

stringの長さ

len(str)で得られる値は1byteを単位とした長さになる。
runeを単位とした長さを取りたい場合はlen([]rune(str))、またはunicode/utf8パッケージのRuneCountInStringメソッドを使用する。

s := "Gopherはかわいい"
fmt.Println(len(s))         // 21
fmt.Println(len([]rune(s)))     // 11
fmt.Println(utf8.RuneCountInString(s))  // 11

部分文字列の切り出し

str[i:j]のようにstringをslicingする方法が直感的だがこれもbyte単位。
rune単位で切り出したい場合は一度[]runeに変換してから切り出してstringに戻すという方法がある。

s := "Gopherはかわいい"
fmt.Println(s[0:7]) // Gopher�
fmt.Println(string([]rune(s)[0:7])) // Gopherは

何文字目か

string中の特定の文字が何文字目に現れるかを得たい場合があったとして。
strings.Index(str, “a”)またはstrings.IndexRune(str, ‘a’)で得られる値はbyte単位。つまり目的の文字が何byte目に現れるかを意味する。
rune単位で何文字目かを得る方法はこれしか思いつかなかった。byte単位のインデックスを取っておいて直前までの文字数に+1する方法だ。

s := "Gopherはかわいい"
byteIdx := strings.IndexRune(s, 'か')      // 9
runeIdx := len([]rune(s[0:byteIdx])) + 1   // 8

参考

The Go Blog Strings, bytes, runes and characters in Go

絵文字がある種のUnicodeバグを世界から一掃しつつある件について

JavaScript における文字コードと「文字数」の数え方

書籍

プログラマのための文字コード技術入門