Javaエンジニア、React+Firebaseでアプリを作る

趣味で作ったものいろいろ

Pythonでひらがなの国語辞典の見出し語順ソート

Pythonなどのプログラミング言語では文字列のソートは 辞書式順序 - Wikipedia でされます。 これは、1文字目をみて、違う値であれば、その順序を、同じ値であれば2文字目を比較して…ということを繰り返す方法です。

例えば、

'はっか', 'はっぱ', 'はつか', 'はつが', 'はつばい', 'ばつ'

pythonで並び替えを行うとこの順に並びます。

ひらがなは文字コード上では、以下の順番になっています。

  1. 清音(濁点・半濁点がついていない小文字でない)はあいうえお順に並んでいる
  2. 拗音(ぁぃぅぇぉゃゅょ)と促音()は対応する清音の直前
  3. 濁音・半濁音は対応する清音の直後(ただし、'ゔ'のみ'う'と離れている)
0 1 2 3 4 5 6 7 8 9 A B C D E F
E3 81 80
E3 81 90
E3 81 A0
E3 81 B0
E3 82 80
E3 82 90

UTF8 3byte (e3) より引用

上記の例並べ替えの例では、まず1文字目は'は'と'ば'の並び順が決まり、1文字目が'は'の言葉の2文字目は'っ'、'つ'なのでこの順に並び、 3文字目の'か'と'ぱ'はこの順、'か'と'が'はこの順に並びます。

しかし国語辞典の見出し語の並びはこれとは少し違います。大辞泉の見出しの配列のページから、ひらがなに関係ある部分をを引用します。

  1. 見出しは、五十音順に配列した。一字目が同じものは二字目のかなの五十音順とし、以下も同様に扱った。
  2. 同じかなのときは、以下の基準を設けて配列した。
    1. 清音・濁音・半濁音の順。 ハート→ハード→バード→パート
    2. 拗音・促音は、直音の前。 ひょう【表】→ひ‐よう【費用】

goo国語辞書 凡例

国語辞典では、例えば

'ばつ', 'はっか', 'はつか', 'はつが', 'はっぱ', 'はつばい'

はこの順番にならびます。

1.見出しは、五十音順に配列した。

の部分ですが、実際の辞書の並び順と照らし合わせてみると、 濁音半濁音はそれぞれ濁点をとったもの、拗音と促音は普通の大きさの文字にしたもの(直音にしたもの)順で並び替えを行うという意味のようです。

これらのルールのもとにひらがなを国語辞典の見出し語順に並び替えてみましょう。

アルゴリズム

ひらがなを国語辞典順に並び替えるプログラムを作成しましょう。おおまかなアルゴリズムは以下です。

  1. 濁音と半濁音を清音に、拗音と促音を直音に変換してソートする
  2. 1の並べ替えで順序が同じ場合は、濁音と半濁音はもとの文字のまま、拗音と促音は直音に変換してソートする
  3. 2の並び替えでも順序が同じ場合は、もとのひらがなの文字コード順でソートする

なお、小さい'ゎ' 'ゕ' 'ゖ'は今回は考慮の対象外としています。

コード

上記アルゴリズムを実装したpythonのコードです。

DAKU_TO_SEION = {
    'ゔ': 'う',
    'が': 'が', 'ぎ': 'き', 'ぐ': 'く', 'げ': 'け', 'ご': 'こ',
    'ざ': 'さ', 'じ': 'し', 'ず': 'す', 'ぜ': 'せ', 'ぞ': 'そ',
    'だ': 'た', 'ぢ': 'ち', 'づ': 'つ', 'で': 'て', 'ど': 'と',
    'ば': 'は', 'び': 'ひ', 'ぶ': 'ふ', 'べ': 'へ', 'ぼ': 'ほ',
    'ぱ': 'は', 'ぴ': 'ひ', 'ぷ': 'ふ', 'ぺ': 'へ', 'ぽ': 'ほ',
}

SMALL_TO_NORMAL = {
    'ぁ': 'あ', 'ぃ': 'い', 'ぅ': 'う', 'ぇ': 'え', 'ぉ': 'お',
    'っ': 'つ',
    'ゃ': 'や', 'ゅ': 'ゆ', 'ょ': 'よ',
}

TO_NORMAL_SEION_TABLE = str.maketrans({**DAKU_TO_SEION, **SMALL_TO_NORMAL})

TO_NORMAL_TABLE = str.maketrans(SMALL_TO_NORMAL)


def sort_as_ja_dict(kana_list):
    return sorted(kana_list,
                  key=lambda kana: (kana.translate(TO_NORMAL_SEION_TABLE),
                                    kana.translate(TO_NORMAL_TABLE), kana))


# 使い方
kana_item = [
    "じょうたい",
    "はつか",
    "しゃちょう",
    "はつばい",
    "しよう",
    "しょうじょう",
    "はっぱ",
    "ばつ",
    "じょう",
    "じゅうたい",
    "しょうたい",
    "はつが",
    "しょう",
    "はっか"
]

sort_as_ja_dict(kana_item)
# => ['しゃちょう', 'じゅうたい', 'しょう', 'しよう', 'じょう', 'しょうじょう', 'しょうたい', 'じょうたい', 'ばつ', 'はっか', 'はつか', 'はつが', 'はっぱ', 'はつばい']

コードの解説

濁音と半濁音を清音に、拗音と促音を直音に変換するには、str.translate(table)( https://docs.python.org/ja/3.7/library/stdtypes.html#str.translate )とstr.maketrans(x[, y[, z]])( https://docs.python.org/ja/3.7/library/stdtypes.html#str.maketrans )を合わせて使用します。 maketransの引数に{(変換前文字): (変換後文字), }の組み合わせの辞書を渡します。 こうすることで、translateで使用する変換テーブルが作成されます。 translateではこのテーブルに基づいて変換がされます。

濁音と半濁音を清音に、拗音と促音を直音へと対応させる辞書は以下の部分です。

DAKU_TO_SEION = {
    'ゔ': 'う',
    'が': 'が', 'ぎ': 'き', 'ぐ': 'く', 'げ': 'け', 'ご': 'こ',
    'ざ': 'さ', 'じ': 'し', 'ず': 'す', 'ぜ': 'せ', 'ぞ': 'そ',
    'だ': 'た', 'ぢ': 'ち', 'づ': 'つ', 'で': 'て', 'ど': 'と',
    'ば': 'は', 'び': 'ひ', 'ぶ': 'ふ', 'べ': 'へ', 'ぼ': 'ほ',
    'ぱ': 'は', 'ぴ': 'ひ', 'ぷ': 'ふ', 'ぺ': 'へ', 'ぽ': 'ほ',
}

SMALL_TO_NORMAL = {
    'ぁ': 'あ', 'ぃ': 'い', 'ぅ': 'う', 'ぇ': 'え', 'ぉ': 'お',
    'っ': 'つ',
    'ゃ': 'や', 'ゅ': 'ゆ', 'ょ': 'よ',
}

最初の並び替えでは両方の辞書、2回目の並び替えでは2つめの辞書だけを使います。

それぞれ変換テーブルをを作成します。

TO_NORMAL_SEION_TABLE = str.maketrans({**DAKU_TO_SEION, **SMALL_TO_NORMAL})

TO_NORMAL_TABLE = str.maketrans(SMALL_TO_NORMAL)

pythonで並び替えのキーを複数取りたい場合、すなわち、あるキーで並び替えた後、同じ値であれば第2のキーで並び変えたい場合、 ソートのkeyに渡す関数の戻り値をタプルにします。

sort(list, key=lambda value: (最優先されるキー, 2番目に優先されるキー, 3番目に優先されるキー))

今回は最優先される値は、濁音と半濁音を清音に、拗音と促音を直音に変換した文字列、2番目に優先されるキーは拗音と促音は直音に変換した文字列、 3番目に優先されるのは、もとの文字列です。順番にタプルに記載します。

def sort_as_ja_dict(kana_list):
    return sorted(kana_list,
                  key=lambda kana: (kana.translate(TO_NORMAL_SEION_TABLE),
                                    kana.translate(TO_NORMAL_TABLE), kana))

動作を確認してみましょう。 キーを指定して並び替えたもの、1つめのキーだけで並び替えたもの、2つのキーで並び替えたもの、3つのキーで並び替えたものです。 だんだんと並び替えられていく様子がわかります。

kana_item = [
    'じょうたい',
    'はつか',
    'しゃちょう',
    'はつばい',
    'しよう',
    'しょうじょう',
    'はっぱ',
    'ばつ',
    'じょう',
    'じゅうたい',
    'しょうたい',
    'はつが',
    'しょう',
    'はっか'
]

sorted(kana_item)
# => ['しゃちょう', 'しょう', 'しょうじょう', 'しょうたい', 'しよう', 'じゅうたい', 'じょう', 'じょうたい', 'はっか', 'はっぱ', 'はつか', 'はつが', 'はつばい', 'ばつ']

sorted(kana_item, key=lambda kana: kana.translate(TO_NORMAL_SEION_TABLE))
# => ['しゃちょう', 'じゅうたい', 'しよう', 'じょう', 'しょう', 'しょうじょう', 'じょうたい', 'しょうたい', 'ばつ', 'はつか', 'はっか', 'はつが', 'はっぱ', 'はつばい']

sorted(kana_item,
       key=lambda kana: (kana.translate(TO_NORMAL_SEION_TABLE),
                         kana.translate(TO_NORMAL_TABLE)))
# => ['しゃちょう', 'じゅうたい', 'しよう', 'しょう', 'じょう', 'しょうじょう', 'しょうたい', 'じょうたい', 'ばつ', 'はつか', 'はっか', 'はつが', 'はっぱ', 'はつばい']

sorted(kana_item,
       key=lambda kana: (kana.translate(TO_NORMAL_SEION_TABLE),
                         kana.translate(TO_NORMAL_TABLE), kana))
# => ['しゃちょう', 'じゅうたい', 'しょう', 'しよう', 'じょう', 'しょうじょう', 'しょうたい', 'じょうたい', 'ばつ', 'はっか', 'はつか', 'はつが', 'はっぱ', 'はつばい']

以上でひらがなを国語辞典の見出し語順で並び替えることができました。

環境

あとがき

以前に電話帳作成問題を解いていて、濁点の並び順が期待した動作ではなかったので今回pythonで実装してみました。

yucatio.hatenablog.com

文字の変換は文字コードを加減算するスマートな方法も目にしますが、今回の場合は愚直にすべてのケースを辞書に登録しました。このほうがバグが入り込みにくいと思います。