豪鬼メモ

一瞬千撃

日本語文字列照合順番によるソート

日本語の辞書の順番で文字列のリストをソートするための手順をメモ。Pythonで実装した。それに基づいて、和英辞書の見出し語の順序も標準的な日本語文字列照合順番に合わせるように変更した。


和英辞書を構築するにあたって、漢字を平仮名に変換する実装は既にあり、平仮名の辞書順で見出し語を並べて収録していた。しかし、多くのプログラミング言語において、文字列のリストをソートした場合、各文字の文字コードの大小を基準にして比較が行われる。近頃はUnicodeを使うことがほとんどだろうが、それを基準にソートすると、平仮名だけであっても、日本語の辞書の順序(いわゆる五十音順)とは異なるものになる。Unicodeの辞書順と日本語辞書の辞書順は違うのだ。例えば、五十音順では、「ばーばー」は「はあはあ」の位置に来るべきだが、普通にソートすると「ば」のコードは「は」より大きいので、全ての「は」始まりの語の後になってしまう。詳しくはWikipediaにある日本語文字列照合順番という記事を参照されたい。

今回は手を抜かずに日本語文字列照合順番を実装する。基本戦略としては、入力された文字列リストの個々の要素を正規化してから、通常のソートによってUnicodeの辞書順でソートする。ただし、個々の文字を正規化する際に優先度を示す数値を末尾に置いて微調整を行う。変換の例を以下に示す。

ぽーてーじ ほおてえし73536
ごーるどまんさっくす こおるとまんさつくす6356555455
ぶれいでぃー ふれいていい655643
そーゔぃにょん そおういによん5364545

正規化の対象にならない普通の文字は、優先度5をつける。5は標準の優先度を示す。長音(伸ばし棒)は前の文字の母音に正規化されるが、元から「あいうえお」である文字よりも前に来るべきなので、優先度3だ。濁音は濁音記号を除去した文字に正規化されるが、元からその文字であるものよりも後に来るべきなので、優先度6だ。半濁音はそれより後になるので、正規化した上で優先度7をつける。拗音や促音の小さな仮名は対応する大きい仮名に正規化されるが、元から大きい仮名であるものよりも前に来て、かつ長音より後に来るべきなので、優先度4をつける。これらをPythonコードに落とし込むと、以下のようになる。

KANA_CONVERSION_MAP = {
  "が": ("か", 6), "ぎ": ("き", 6), "ぐ": ("く", 6), "げ": ("け", 6), "ご": ("こ", 6),
  "ざ": ("さ", 6), "じ": ("し", 6), "ず": ("す", 6), "ぜ": ("せ", 6), "ぞ": ("そ", 6),
  "だ": ("た", 6), "ぢ": ("ち", 6), "づ": ("つ", 6), "で": ("て", 6), "ど": ("と", 6),
  "ば": ("は", 6), "び": ("ひ", 6), "ぶ": ("ふ", 6), "べ": ("へ", 6), "ぼ": ("ほ", 6),
  "ぱ": ("は", 7), "ぴ": ("ひ", 7), "ぷ": ("ふ", 7), "ぺ": ("へ", 7), "ぽ": ("ほ", 7),
  "っ": ("つ", 4),
  "ぁ": ("あ", 4), "ぃ": ("い", 4), "ぅ": ("う", 4), "ぇ": ("え", 4), "ぉ": ("お", 4),
  "ゃ": ("や", 4), "ゅ": ("ゆ", 4), "ょ": ("よ", 4), "ゕ": ("か", 4), "ゖ": ("け", 4),
  "ゔ": ("う", 6),
}

def MakeYomiKey(yomi):
  norm_chars = []
  priorities = []
  last_norm_char = ""
  for char in yomi:
    priority = 5
    if char == "ー":
      if last_norm_char in "あかさたなはまやらわ":
        norm_char, priority = ("あ", 3)
      elif last_norm_char in "いきしちにひみりゐ":
        norm_char, priority = ("い", 3)
      elif last_norm_char in "うくすつぬふむゆる":
        norm_char, priority = ("う", 3)
      elif last_norm_char in "えけせてねへめれゑ":
        norm_char, priority = ("え", 3)
      elif last_norm_char in "おこそとのほもよろを":
        norm_char, priority = ("お", 3)
      elif last_norm_char in "ん":
        norm_char, priority = ("ん", 3)
      else:
        norm_char, priority = (char, 3)
    else:
      norm_char, priority = KANA_CONVERSION_MAP.get(char) or (char, 5)
    norm_chars.append(norm_char)
    priorities.append(str(priority))
    last_norm_char = norm_char[0]
  return "".join(norm_chars) + "\0" + "".join(priorities)

あとは、元来の文字列のリストをソートする代わりに、ソート用のキーと元来の文字列のペアのリストをソートしてから、元来の文字列だけを取り出せばよい。実際は、漢字入りの元の文字列とその読み仮名のペアのリストをソートすることが多いだろう。その場合、ソート用変換文字列、読み仮名文字列、元の漢字入り文字列のトリオのリストをソートする。そうすると、同じ読みの場合には漢字の文字コード順で決着がつけられる。わかりやすくコードに落とすと、以下のようになる。

yomi_kanji_list = [
  ("たこやき", "蛸焼き"),
  ("たこやき", "タコ焼き"),
  ("ばーばー", "バーバー"),
  ("ぱあぱあ", "パーパー"),
  ("はあはあ", "ハアハア"),
  ("はんどう", "反動"),
  ("はんく", "半句"),
  ("はんくあーろん", "ハンクアーロン"),
  ("ばんくおぶあめりか", "バンクオブアメリカ"),
  ("ばんく", "バンク"),
  ("はんく", "パンク"),
]

def SortYomiList(yomi_kanji_list):
  trio_list = []
  for yomi, kanji in yomi_kanji_list:
    trio_list.append((MakeYomiKey(yomi), yomi, kanji))
  sorted_trio_list = sorted(trio_list)
  return [x[1:] for x in sorted_trio_list]
  
for yomi, kanji in SortYomiList(yomi_kanji_list):
  print(yomi, "|", kanji)

複雑なロジックを実装した比較関数を実装するより、正規化と優先順位付けが同時にできるソートキーを書き出した方がわかりやすいし、動作も高速だ。Python以外の言語でも同じ方法でやれば楽に実装できると思う。