emahiro/b.log

Drastically Repeat Yourself !!!!

Go で文字数をカウントする

Overview

Go 言語で文字数をカウントする方法について記載します。

そもそも文字数を数えるとは?

LINE Engineering のこのブログが詳しくてわかりやすいです。

engineering.linecorp.com

ある文字列の文字数を計算するときに、コンピューター上で「何文字」として扱われるかは、文字の定義によって異なります。

Go における文字数のカウント方法

utf8.RuneCountInString を使う

マルチバイトの文字(漢字など)を使ったテキストの総文字数を Go でシンプルに計算するときは https://golang.org/pkg/unicode/utf8/#RuneCountInString を使います。

例えば以下のように使用します。

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "こんにちは"
    ss := utf8.RuneCountInString(s)
    fmt.Println(ss)
}

この実装の出力結果は 5 です。

しかしこの文字数の数え方には 絵文字が入った文字列では正確に文字数をカウントできない という漏れがあります。例えば以下のようなケースでは✌️ は Rune Slice の長さ 2 のマルチバイト文字列であることがわかります。

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "✌️"
    ss := utf8.RuneCountInString(s)
    fmt.Println(ss)
}

Grapheme Cluster を使う

ユーザーが認識する文字。文字体系で表現できる文字の最小単位。1 Graphemeは、N code pointで構成されています。

ref: https://engineering.linecorp.com/ja/blog/the-7-ways-of-counting-characters/

Go で Grapheme を使う

OSS として出してくれてるライブラリがあります。

github.com

実装方法は上記の Readme に書いてる通りです。

このライブラリ (というか Grapheme) は Rune Slice が 2 以上あるマルチバイと文字列も「1文字としてカウント」してくれます。

具体的に文字数を数えるときは以下のように実装します。

func main() {
    var sum int
    gr := uniseg.NewGraphemes("✌️!")
    for gr.Next() {
        sum++
    }
    fmt.Printf("%d", sum)
}

この出力は2です。

✌️ + ! の文字列しかないので 2 になります。 utf8. RuneCountInString を使用した場合、上記の文字列は 3 と表示されます。つまりマルチバイトの文字まで数えてしまいます

余談: 同じ絵文字でも文字数が違う場合がある

👉 という絵文字にも以下のような種類が存在します。

f:id:ema_hiro:20210119141227p:plain

この別の肌の色をどう表現してるのかを調べました。

package main

import (
    "fmt"
 
    "github.com/rivo/uniseg"
)

func main() {
    s := "👉"
    gr := uniseg.NewGraphemes(s)
    for gr.Next() {
        fmt.Printf("%x", gr.Runes())
    }

}
// Output: [1f449]

別の肌の色では以下でした。

package main

import (
    "fmt"
 
    "github.com/rivo/uniseg"
)

func main() {
    s := "👉🏻"
    gr := uniseg.NewGraphemes(s)
    for gr.Next() {
        fmt.Printf("%x", gr.Runes())
    }
}
// Output: [1f449 1f3fb]

👉 に色の文字コードが追加された出力されました。なお utf8.RuneCountInString を使用した場合、👉 は1文字ですが、👉🏻 は2文字で計算されます。

同じ絵文字でも文字数が違うことがわかりました。

ちなみに Code Point を調べるとこのことは一発でわかります。

emojipedia.org

👉 U+1F449
🏻 U+1F3FB

と別のデータとして扱われてますね。

まとめ

目で見てる絵文字と、コンピュータ上でどう扱われるかには差があるので「文字数をカウントする」という実装はかなり奥が深い(というかややこしい)ことがわかりました。

Grapheme cluster は覚えておこうと思います。