blog.anqou.net
rss
author
tags

OCaml で Camomile を使い NFKC で Unicode 正規化

日本語の全角・半角を気にせずに文字列検索を行いたい場合、扱う対象が Unicode なら Unicode 正規化を行うのが一般的らしいです。正規化にも色々種類があるそうなのですが、インターネットを放浪した限りだと NFKC(Normalization Form Compatibility Composition)がよく使われるみたいです。

OCaml でこの手の処理を行いたい場合、言語機能や標準ライブラリでは不十分なので[1]サードパーティのライブラリを使うことになります[2]。有名なのは Camomileuunf です。個人的な事情で、以前 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)

注釈

  1. OCaml の標準でも一定の Unicode のサポートはあります。uucp のドキュメントに詳しいです。

  2. 当然自前で実装してもいいわけですが、それを除くと。

  3. チュートリアルらしいチュートリアルも無いので困ります。一応 Cookbook にページはあるのですが、今回はあまり参考になりませんでした。