はじめに
今回は準標準の golang.org/x/text 配下のパッケージを紹介します。
ここではは主に多言語、多locale対応の際の文字列操作をサポートするパッケージが管理されています。
アジア圏などの文字管理に考慮事項が多いlocaleに関してはそれ向けに特化したパッケージなども提供されています。
今日はその中からいくつかのパッケージを紹介したいと思います。
これは Go6 Advent Calendar 2019 の投稿です(ちょっと遅刻しました)
golang.org/x/text/number
doc: https://golang.org/x/text/number
このパッケージは、言語による数値表現の差分を吸収してくれます。
出力の際は同じく準標準のパッケージである https://golang.org/x/text/message をつかって出力できます。
例えば英語圏では数字3桁ごとにカンマを入れますが、そのようなケースでこのパッケージが利用できます。
以下はドキュメントで紹介されている例です
p := message.NewPrinter(language.English)
p.Printf("%v bottles of beer on the wall.", number.Decimal(1234))
// Prints: 1,234 bottles of beer on the wall.
p.Printf("%v of gophers lose too much fur", number.Percent(0.12))
// Prints: 12% of gophers lose too much fur.
p = message.NewPrinter(language.Dutch)
p.Printf("There are %v bikes per household.", number.Decimal(1.2))
// Prints: Er zijn 1,2 fietsen per huishouden.
実行するとこうなります。 playground: https://play.golang.org/p/f-1feZPHV8t
英語圏だけでなく、他の言語での数値表現に対しても同様に利用できます。
出力フォーマットは浮動小数やパーセンテージ、学術的な数値表現など、様々なフォーマットに対応しています。
いろいろ出力した例。playground: https://play.golang.org/p/9rnYs5P7Og1
また、出力の際の数値の有効桁数の指定や、増減時の数値の刻み方などもオプションで指定できます。
golang.org/x/text/encoding/japanese
doc: https://golang.org/x/text/encoding/japanese
このパッケージは Shift JIS
, EUC-JP
のエンコーディングに対応してくれるパッケージです。
Goはプログラム自体は UTF-8 で書くので、プログラム内で定義したstringについては基本的に UTF-8 のバイト列です。
ですが、外部ファイルから内容を読み取ったときなど、stringに UTF-8 として解釈できないバイト列が入ってくる場合があります。このようなバイト列をそのまま文字として出力しようとすると、UTF-8 として解釈できず、文字化けなどが発生します(たまたま指し示すバイトが一致すると別の文字が出たりする)
// utf-8 で `あいうえお` となるバイト列
utf8 := []byte{227, 129, 130, 227, 129, 132, 227, 129, 134, 227, 129, 136, 227, 129, 138}
fmt.Println(string(utf8))
// -> あいうえお
// s-jis で `あいうえお` となるバイト列
sjis := []byte{130, 160, 130, 162, 130, 164, 130, 166, 130, 168}
fmt.Println(string(sjis))
// -> ����������
プログラム内で UTF-8 以外の文字列を扱う場合は、一度 UTF-8 に変換しないと
- UTF-8 範囲外の値を指定された場合
- 範囲内でも指し示す文字が異なる場合
などで期待した処理が行えません。
このパッケージは Shift JIS エンコーダー、デコーダーを提供しています。
同じく準標準の https://golang.org/x/text/transform と組み合わせることで、
- UTF-8 to Shift JIS
- Shift JIS to UTF-8
にバイト列を変換できます。試しに UTF-8 にデコードしてみます。
// s-jis で `あいうえお` となるバイト列
sjis := []byte{130, 160, 130, 162, 130, 164, 130, 166, 130, 168}
// Shift JIS to UTF-8
result, _, _ := transform.Bytes(japanese.ShiftJIS.NewDecoder(), sjis)
fmt.Println(string(result))
// -> あいうえお
playground: https://play.golang.org/p/ywDO0ZLrU6A
また、transformパッケージは変換用のReader,Writerを生成する NewReader
, NewWriter
API があるので、
エンコーディングA -> UTF-8 -> エンコーディングB のような変換を io パッケージの恩恵にあやかって書くこともできます
以下は io.Reader, io.Writer を使って変換する例です。
// s-jis で `あいうえお` となるバイト列
sjis := []byte{130, 160, 130, 162, 130, 164, 130, 166, 130, 168}
r := transform.NewReader(bytes.NewBuffer(sjis), japanese.ShiftJIS.NewDecoder())
w := transform.NewWriter(os.Stdout, japanese.EUCJP.NewEncoder())
io.Copy(w, r)
playground: https://play.golang.org/p/z0jdXC7VCaz
Reader, Writer が取れるので、各種IOと連携してストリーム的に処理したり、パイプ的に処理したり、いろいろできます。
golang.org/x/text/width
doc: https://golang.org/x/text/width
このパッケージは文字列の全角、半角が取れるパッケージです。(正確には全角、半角を判断するための文字種別が取れる)
またそれらの文字種別を変換するAPIも提供されています。
こいつの紹介がしたくてこの記事を書いたと言っても過言ではありません。
まず、Unicodeの全角、半角の取り扱いについては Unicode® Standard Annex #11
にて EAST ASIAN WIDTH
という定義がなされています。リンクはこれです https://unicode.org/reports/tr11/
上記の定義では Unicode 文字をいくつかの種類に分類していて、ざっくりいうと
- East Asian Fullwidth (F): Unicode で Fullwidth と定義されているもの。全角英数など
- East Asian Halfwidth (H): Unicode で Halfwidth と定義されているもの。半カナなど
- East Asian Wide (W): F ではない全角文字が入る。漢字とか。
- East Asian Narrow (Na): 対応する全角文字が存在する H ではない半角文字
- East Asian Ambiguous (A): 文脈により半角、全角が違う。判断のためには文字コード以外のメタ情報が必要。
- Neutral (Not East Asian):: 中立。東アジア圏と無関係の文字。キリル文字とかたぶんそう。
の6種類あります。
F,W -> 常に全角
H,Na,N -> 常に半角
A -> 文脈による
ということですね。
これらの判断を Unicode 情報を参考にしつつ仕様に則って実装していたらとても大変です。
ですが安心してください。Goには https://golang.org/x/text/width があります。
このパッケージは、これらの仕様に基づいて、文字列から該当する文字種類をとりだしたり、文字列をそれぞれの種別に変換可能なものだけ相互に変換する機能などを提供しています。
以下はパッケージドキュメントにある Narrow 文字に変換する example コードです。
s := "abヲ₩○¥A"
n := width.Narrow.String(s)
fmt.Printf("%U: %s\n", []rune(s), s)
fmt.Printf("%U: %s\n", []rune(n), n)
// Ambiguous characters with a halfwidth equivalent get mapped as well.
s = "←"
n = width.Narrow.String(s)
fmt.Printf("%U: %s\n", []rune(s), s)
fmt.Printf("%U: %s\n", []rune(n), n)
// output
//[U+0061 U+0062 U+30F2 U+FFE6 U+25CB U+FFE5 U+FF21]: abヲ₩○¥A
//[U+0061 U+0062 U+FF66 U+20A9 U+FFEE U+00A5 U+0041]: abヲ₩○¥A
//[U+2190]: ←
//[U+FFE9]: ←
このように、複雑な仕様をラッピングしつつ、かつ仕様に沿った正確な情報などを参照できるようになっています。
個人的には実装、公開しているAPIの粒度が非常にGoらしく、好きなパッケージの一つです。
最後に
https://golang.org/x/text 配下のパッケージをいくつか見ていきました。
このパッケージ群ではほかにも
- 言語特有の文脈依存の表記ゆれを吸収して文字検索ができる https://golang.org/x/text/search
- 言語タグの仕様を吸収して、言語タグからサービスがサポートしている最も親しい言語を取得できる https://golang.org/x/text/language
- 多通貨対応の https://golang.org/x/text/currency
など、i18n や localization に便利な機能群が提供されています。
これらの機能を言語が準標準としてホストしてくれているのは安心感があり、ありがたいですね。
また、今回紹介した x/text/encoding/japanese や x/text/width などのように、
特定のlocale固有の文字表現の問題などを吸収する機能も提供されています。
なにかと変則的な文字の取り扱いが多いアジア圏に住む我々からすると非常に助かりますね。
実装自体も i18n 対応の手本にできる部分は大いにあるので、別言語で似たような対応が必要なときにも参考になりそうですね!
goの標準、準標準パッケージは色々なユースケースに対して柔軟に対応できるように基本的な機能はあらかたサポートされているのがとてもありがたいですね、いつも助かっています。
標準、準標準パッケージをうまく使い、慣習に沿ったメンテナビリティの高いコードを書いていきたいですね!
参考
https://godoc.org/golang.org/x/text/number
https://godoc.org/golang.org/x/text/language
https://godoc.org/golang.org/x/text/message
https://godoc.org/golang.org/x/text/encoding/japanese
https://godoc.org/golang.org/x/text/transform
https://godoc.org/golang.org/x/text/width
http://www.unicode.org/reports/tr11