ひょんな拍子に、JavaScriptで「ガ」のような濁点付きの仮名を「カ」に直さないといけなくなったのですが、テーブルなしで対応できることが判明しました。
NFCとNFD
「ガ
」と「ガ
」、肉眼で見ればほぼ違いがないですが、データとしては全く別物になっています。
-
ガ
…文字コードは\u30ac
、濁点まで含めて1つの文字 -
ガ
…文字コードは\u30ab\u3099
、「カ
+結合文字の濁点1」
このように、Unicodeでは、「複数のコードポイントを結合した文字」と「結合した状態で1つのコードポイントに登録された文字」の両方が存在していることがあります。視覚的にも意味的にも全く同じものが別なコードで表されていると(正準等価)、検索の際に厄介です。
そのようなときに役立つルールとして、Unicodeは正規化の手順も定義しています。これは4つありますが、まずは2つを取り上げます。
- NFD…正準等価な文字については、結合文字に分解した形で正規化する
- NFC…正準等価な文字については、NFDで正規化したものを再度合成した形に正規化する
これを使うことで、1コードポイントで書かれた「ガ」から「カ」を抽出することが可能となります。
JavaScriptから使ってみる
ES6でJavaScriptにもString.prototype.normalize()
というメソッド(MDN)が用意され、Unicode正規化を施した文字列を得られるようになっています。引数には、NFC
、NFD
、NFKC
、NFKD
(あと2つは後述)の4つがあります。
そして、濁音・半濁音入りの文字列をNFDで正規化すれば、1文字目は清音になるので、これを取り出せば濁点を外せます。逆に、結合文字の濁点を追加してからNFC正規化をかければ、1コードポイントになった濁音入りの仮名が得られます2。
console.log('ガ'.normalize('NFD')[0]) // => 'カ'
NFKCとNFKDと半角カナ
Unicode正規化には、NFC・NFD以外に、「意味が同じになる(互換等価)」を基準にしたNFKC・NFKDというものがあります。これは、正準等価の正規化に加えて、
- 上付き・下付き文字→通常の文字
- 装飾付き文字の装飾を外す(
①
→1
) - 組文字を解体する(
㈱
→(㈱)
) - 半角・全角形を使わない形にする(
Aア
→Aア
)
のような正規化を行います(NFKCとNFKDの違いはNFCとNFDの違いと同じで、NFKCは結合済み文字を、NFKDはばらした文字を優先します)。ということで、この変換を使えば、全角英数→半角英数、半角カナ→全角カナの変換が可能です(性質上、逆変換はできません)。
注意点
-
String.prototype.normalize()
はES6で加わったメソッドですので、IE11には存在しません。そして、Polyfillをやろうにも、Unicodeのテーブルが必要なので、相応の容量を食うことが予想されます。 - 4つの正規化全ては、もとに戻せません。特に、「CJK互換漢字」という一部の漢字は、見た目が違うものにNFC・NFD正規化でも変わってしまうことがあります(
靖
→靖
など)。これもあって、macOSでは「CJK互換漢字などを除外してNFD正規化を行う」という我流の方法でファイル名を格納しています。