日本語の全角・半角を気にせずに文字列検索を行いたい場合、扱う対象が Unicode なら Unicode 正規化を行うのが一般的らしいです。正規化にも色々種類があるそうなのですが、インターネットを放浪した限りだと NFKC(Normalization Form Compatibility Composition)がよく使われるみたいです。
OCaml でこの手の処理を行いたい場合、言語機能や標準ライブラリでは不十分なので[1]サードパーティのライブラリを使うことになります[2]。有名なのは Camomile と uunf です。個人的な事情で、以前 uunf の姉妹ライブラリである uutf は使ったことがあったので、逆に今回は Camomile v2.0.0 を使って NFKC による正規化をしてみます。
今回行いたい処理は Camomile.UNF.Make にあります。ただしこの定義だけを見ても使い方はよく分かりません。テストの実装などを見ながら[3]しばらく試行錯誤すると以下のようなコードで UTF-8 文字列の NFKC による正規化の結果を取得できることが分かりました:
let nfkc =
let module NF = Camomile.UNF.Make (Camomile.UTF8) in
NF.nfkc
(* テストは ppx_inline_test の形式
https://github.com/janestreet/ppx_inline_test *)
let%test_module "nfkc" =
(module struct
(* テストケースは以下の例を抜粋
https://note.nkmk.me/python-unicodedata-normalize/ *)
let%test_unit "case1" = assert (nfkc "123abcアイウエオ①㈱㌖" = "123abcアイウエオ1(株)キロメートル")
let%test_unit "case2" = assert (nfkc "123abcアイウエオ①②③¹²³" = "123abcアイウエオ123123")
let%test_unit "case3" = assert (nfkc "がガぱパ" = "がガぱパ")
end)
これを組み込むと、正規化した状態で文字列検索をする処理は以下のように書けます。文字列検索のライブラリとして re2 を使っています。ついでに正規表現もサポートします。let* は Result.bind に束縛しています(参考)。
type t = { raw : Re2.t; normalized : Re2.t }
let create ~regex ~case_sensitive text =
let* raw =
Re2.(create ~options:{ Options.default with literal = not regex; case_sensitive } text)
|> Result.map_error Core.Error.to_string_mach
in
let* normalized =
Re2.(create ~options:{ Options.default with literal = not regex; case_sensitive } (nfkc text))
|> Result.map_error Core.Error.to_string_mach
in
Ok { raw; normalized }
let does_match { raw; normalized } s =
Re2.matches raw s || Re2.matches normalized (nfkc s)
let%test_module "does_match" =
(module struct
let%test_unit "basic literal" =
let m = create ~regex:false ~case_sensitive:false "1" |> Result.error_to_failure in
assert (does_match m "1");
let m = create ~regex:false ~case_sensitive:false "2" |> Result.error_to_failure in
assert (not (does_match m "1"));
()
let%test_unit "zen/han literal" =
let m = create ~regex:false ~case_sensitive:false "1" |> Result.error_to_failure in
assert (does_match m "1");
let m = create ~regex:false ~case_sensitive:false "1" |> Result.error_to_failure in
assert (does_match m "1");
()
end)
注釈
-
OCaml の標準でも一定の Unicode のサポートはあります。uucp のドキュメントに詳しいです。 ↩
-
当然自前で実装してもいいわけですが、それを除くと。 ↩
-
チュートリアルらしいチュートリアルも無いので困ります。一応 Cookbook にページはあるのですが、今回はあまり参考になりませんでした。 ↩