Edited at

golang の text/unicode/norm パッケージでカタカナ・ひらがなの正規化ができるのか?

PicApp アドベントカレンダー3日目です。前日は こちら

golang でひらがな・カタカナ・全角・半角の正規化をしようとしたときに、 golang.org/x/text/unicode/norm を使う方法がある、と記載したページが見つかりますが、実際、このパッケージは何をやってくれるのでしょうか。具体的には、norm.NFD, norm.NFC, norm.NFKD, norm.NFKC を使った変換は何をするのでしょう。


知りたいこと

golang で「ひらがな、カタカナ、全角、半角」等の文字列を任意の文字種に正規化する目的で golang.org/x/text/unicode/norm は使えるのか?


結論

norm.NF* でできること


  • 「半角カタカナ」を「全角カタカナ」に正規化(逆は不可)

  • 「全角英数字」を「半角英数字」に正規化(逆は不可)

  • 「全角記号」を「半角記号」に正規化(逆は不可)

できないこと


  • 「カタカナ」を「ひらがな」にする

  • 「ひらがな」を「カタカナ」にする

※このパッケージは Unicode で規定されている正準等価、互換等価に基づいて等価な文字列に正規化するので、ひらがな・カタカナのような見た目がまったく異なる文字の正規化はできない


詳細

下記ページの説明が詳しいです。

文字コード地獄秘話 第3話:後戻りの効かないUnicode正規化

http://text.baldanders.info/golang/unicode-normalization/

NFD, NFC 等は Unicode における正準等価(正規等価)、互換等価という2種類の文字の等価性に基づいて、文字列を正規化する方法を表しています。

正準等価は、文字の見た目・機能がまったく一緒だけど、コード的には変わってくるもの、例えば「ペ」は「ヘ」と「゜」に分けることができますが、「ヘ」に「゜」を結合したもの(2文字だが、見た目上1文字になる)も「ペ」も見た目や機能は一緒だから、同じものとして扱うという方法。この等価性に基づき、結合した「ヘ」「゜」は1文字の「ペ」の方に正規化できる。

一方で、互換等価はもう少し広い範囲で等価だとみなすもの。例えば「ガ」と「ガ」が同じものであるとみなすもの。「ガ」は「ガ」に正規化できる。


サンプルコード

実際に日本語で使いそうな各種文字列を使って、正規化を行うサンプルコードです。

package main

import (
"fmt"
"golang.org/x/text/unicode/norm"
"strings"
)

func main(){
strs := "# # 5 5 a A a A ア ガ ア ガ あ が 神 ㍍ ㍻"
for _, s := range strings.Split(strs, " "){
fmt.Printf("%v %#U\n"," ", []rune(s))
nfdb := norm.NFD.String(s)
fmt.Printf("%v %#U\n", " NFD: ",[]rune(nfdb))
nfcb := norm.NFC.String(s)
fmt.Printf("%v %#U\n"," NFC: ",[]rune(nfcb))
nfkdb := norm.NFKD.String(s)
fmt.Printf("%v %#U\n","NFKD: ", []rune(nfkdb))
nfkcb := norm.NFKC.String(s)
fmt.Printf("%v %#U\n","NFKC: ", []rune(nfkcb))
fmt.Println()
}
}

出力は次の通り

       [U+0023 '#']

NFD: [U+0023 '#']
NFC: [U+0023 '#']
NFKD: [U+0023 '#']
NFKC: [U+0023 '#']

[U+FF03 '#']
NFD: [U+FF03 '#']
NFC: [U+FF03 '#']
NFKD: [U+0023 '#']
NFKC: [U+0023 '#']

[U+0035 '5']
NFD: [U+0035 '5']
NFC: [U+0035 '5']
NFKD: [U+0035 '5']
NFKC: [U+0035 '5']

[U+FF15 '5']
NFD: [U+FF15 '5']
NFC: [U+FF15 '5']
NFKD: [U+0035 '5']
NFKC: [U+0035 '5']

[U+0061 'a']
NFD: [U+0061 'a']
NFC: [U+0061 'a']
NFKD: [U+0061 'a']
NFKC: [U+0061 'a']

[U+0041 'A']
NFD: [U+0041 'A']
NFC: [U+0041 'A']
NFKD: [U+0041 'A']
NFKC: [U+0041 'A']

[U+FF41 'a']
NFD: [U+FF41 'a']
NFC: [U+FF41 'a']
NFKD: [U+0061 'a']
NFKC: [U+0061 'a']

[U+FF21 'A']
NFD: [U+FF21 'A']
NFC: [U+FF21 'A']
NFKD: [U+0041 'A']
NFKC: [U+0041 'A']

[U+FF71 'ア']
NFD: [U+FF71 'ア']
NFC: [U+FF71 'ア']
NFKD: [U+30A2 'ア']
NFKC: [U+30A2 'ア']

[U+FF76 'カ' U+FF9E '゙']
NFD: [U+FF76 'カ' U+FF9E '゙']
NFC: [U+FF76 'カ' U+FF9E '゙']
NFKD: [U+30AB 'カ' U+3099 '゙']
NFKC: [U+30AC 'ガ']

[U+30A2 'ア']
NFD: [U+30A2 'ア']
NFC: [U+30A2 'ア']
NFKD: [U+30A2 'ア']
NFKC: [U+30A2 'ア']

[U+30AC 'ガ']
NFD: [U+30AB 'カ' U+3099 '゙']
NFC: [U+30AC 'ガ']
NFKD: [U+30AB 'カ' U+3099 '゙']
NFKC: [U+30AC 'ガ']

[U+3042 'あ']
NFD: [U+3042 'あ']
NFC: [U+3042 'あ']
NFKD: [U+3042 'あ']
NFKC: [U+3042 'あ']

[U+304C 'が']
NFD: [U+304B 'か' U+3099 '゙']
NFC: [U+304C 'が']
NFKD: [U+304B 'か' U+3099 '゙']
NFKC: [U+304C 'が']

[U+FA19 '神']
NFD: [U+795E '神']
NFC: [U+795E '神']
NFKD: [U+795E '神']
NFKC: [U+795E '神']

[U+334D '㍍']
NFD: [U+334D '㍍']
NFC: [U+334D '㍍']
NFKD: [U+30E1 'メ' U+30FC 'ー' U+30C8 'ト' U+30EB 'ル']
NFKC: [U+30E1 'メ' U+30FC 'ー' U+30C8 'ト' U+30EB 'ル']

[U+337B '㍻']
NFD: [U+337B '㍻']
NFC: [U+337B '㍻']
NFKD: [U+5E73 '平' U+6210 '成']
NFKC: [U+5E73 '平' U+6210 '成']

末尾が D の NFD, NFKD は文字を基底文字と結合文字に分解するため、例えば に分解されますが、Chrome では1つの文字として描画されています。この合字の扱いは、描画するアプリケーションによって異なっていて、IntelliJ IDEA では2つの文字として別々に描画されていますが、notepad2 では1つの文字として描画されていてかつ文字の真ん中にカーソルが当てられるようになっています。

合字については下記のページの説明が詳しいです。

第3回 ひらがな・カタカナとUnicode

https://www.taishukan.co.jp/kokugo/webkoku/series003_03.html

一方で、末尾が C の NFC, NFKC は文字を分解したあとに再合成します。ガ の例がわかりやすく、まず に分解されたあとに再合成されて 1文字になっています。

まとめると、norm パッケージは Unicode の正準等価、互換等価という仕様に基づいて文字列を正規化するものであり、任意の日本語文字種間でのマッピングを実現するためのものではありません。


参考