はじめに:「なぜ漢数字は数字順に並ばない!?」
先日、こんなツイートをしたところ、結構たくさんの人にリツイートされました。(執筆時点で50件以上)
「漢数字はソートしても数字順に並ばない」という事実を生まれて初めて知った。まさかのサプライズ。 pic.twitter.com/Eqx3ltIfHs
— Junichi Ito (伊藤淳一) (@jnchito) 2014年11月27日
「なぜ漢数字は数字順に並ばないのか」という問いに対して、表面的な回答をするなら「数字順に並ばないのは、数字の大きさではなく文字コード順でソートされているから」ということになります。
いや、もちろんそれはわかってるんです。
問題は「そもそもなんで数字順に文字コードを振らなかったの!?」ということです。
感覚的には「一郎、二郎、三郎」って並んでほしいじゃないですか。でも、プログラム上でソートすると「一郎、三郎、二郎」って並んじゃうんです。
これはちょっと気持ち悪い。
というわけで、どういうルールでこの文字コードが振られているのかを調べてみました。
一から九までをソートしてみる
まず、漢数字の一から九までをRubyでソートしてみました。
irb(main):017:0> x = %w(一 二 三 四 五 六 七 八 九) => ["一", "二", "三", "四", "五", "六", "七", "八", "九"] irb(main):018:0> x.sort => ["一", "七", "三", "九", "二", "五", "八", "六", "四"]
おおっ、完全にバラバラじゃないですか・・・。
まったく数字順に並ぶ気配がありません。
各漢字の文字コードを調べてみる
文字コード順に並んでいるはず、ということは明らかですが、念のため文字コードを調べてみましょう。
irb(main):017:0> x = %w(一 二 三 四 五 六 七 八 九) => ["一", "二", "三", "四", "五", "六", "七", "八", "九"] irb(main):018:0> sorted = x.sort => ["一", "七", "三", "九", "二", "五", "八", "六", "四"] irb(main):019:0> codes = sorted.join.unpack('U*').map{|n| n.to_s(16)} => ["4e00", "4e03", "4e09", "4e5d", "4e8c", "4e94", "516b", "516d", "56db"] irb(main):020:0> sorted.zip(codes) => [["一", "4e00"], ["七", "4e03"], ["三", "4e09"], ["九", "4e5d"], ["二", "4e8c"], ["五", "4e94"], ["八", "516b"], ["六", "516d"], ["四", "56db"]]
一番最後の行に漢数字と文字コードの組み合わせを表示しています。
うん、確かに文字コード順に並んでいますね。
文字コードが割り振られているルールを調べる
はい、ここからが一番の難題です。
この文字コードはどういうルールで割り振られているのか?
なぜ数字順に割り振らなかったのか?
その理由を調べてみました。
なかなか「これ!」という答えが見つからなかったのですが、最終的にはWikipediaにそれらしき情報が載っていました。
CJK統合漢字 - Wikipedia配列
原則として登録される毎に部首画数順で配列されている。但し一部に乱れが存在している上追加が相次いだために検索が困難になってきており、Unihanデータベースでは割り当てられたUnicode値と部首番号、部首別画数から導出される値をソートキーとして規格化している。
おお、なるほど、「部首画数順」なのか!!
というわけで、文字コードは部首画数順で割り振られているという仮説を元に、各漢字の部首と画数を調べてみました。
各漢字の部首と画数を検証する
部首と画数の検索と確認は次のサイトを利用させてもらいました。
おさらい:やっぱり部首画数順だった!
というわけで上で見た部首と画数をここでまとめてみましょう。
- 「一」「七」「三」の部首は一画の「一(いち)」
- 「九」の部首は一画の「乙(おつ)」
- 「二」「五」の部首は二画の「二(に)」
- 「八」「六」の部首は二画の「八(はち)」
- 「四」の部首は三画の「口(くにがまえ)」
そして、一から九までをソートした結果はこれでした。
irb(main):017:0> x = %w(一 二 三 四 五 六 七 八 九) => ["一", "二", "三", "四", "五", "六", "七", "八", "九"] irb(main):018:0> x.sort => ["一", "七", "三", "九", "二", "五", "八", "六", "四"]
調べてみる前は「完全にバラバラ」だと思っていましたが、こうやって見ると「そうかー、これは部首画数順に並んでいたのか~」とルールがちゃんと見えてきますね。
あ~、これでスッキリ。
さらにもう少し:エンコーディングが変わるとソート順も変わる!
ところで、ここまで見てきたのはエンコーディングがUTF-8の場合です。
実は、シフトJISやEUCになるとソート順も変わります。
irb(main):004:0> x = %w(一 二 三 四 五 六 七 八 九) => ["一", "二", "三", "四", "五", "六", "七", "八", "九"] irb(main):005:0> x.sort => ["一", "七", "三", "九", "二", "五", "八", "六", "四"] irb(main):006:0> x.map{|s| s.encode("Shift_JIS")}.sort.map{|s| s.encode("UTF-8")} => ["一", "九", "五", "三", "四", "七", "二", "八", "六"]
一番下の行がシフトJISでソートした結果です。
(真ん中はUTF-8でソートした結果)
ちなみにEUCでもシフトJISと同じ順番になりました。
irb(main):008:0> x.map{|s| s.encode("EUC-JP")}.sort.map{|s| s.encode("UTF-8")} => ["一", "九", "五", "三", "四", "七", "二", "八", "六"]
念のために文字コードも調べておきましょう。
以下はシフトJISの場合です。
irb(main):026:0> sorted = x.map{|s| s.encode("Shift_JIS")}.sort.map{|s| s.encode("UTF-8")} => ["一", "九", "五", "三", "四", "七", "二", "八", "六"] irb(main):027:0> codes = x.map{|s| s.encode("Shift_JIS")}.sort => ["\x{88EA}", "\x{8BE3}", "\x{8CDC}", "\x{8E4F}", "\x{8E6C}", "\x{8EB5}", "\x{93F1}", "\x{94AA}", "\x{985A}"] irb(main):028:0> sorted.zip(codes) => [["一", "\x{88EA}"], ["九", "\x{8BE3}"], ["五", "\x{8CDC}"], ["三", "\x{8E4F}"], ["四", "\x{8E6C}"], ["七", "\x{8EB5}"], ["二", "\x{93F1}"], ["八", "\x{94AA}"], ["六", "\x{985A}"]]
こちらはEUCの場合です。
irb(main):029:0> sorted = x.map{|s| s.encode("EUC-JP")}.sort.map{|s| s.encode("UTF-8")} => ["一", "九", "五", "三", "四", "七", "二", "八", "六"] irb(main):030:0> codes = x.map{|s| s.encode("EUC-JP")}.sort => ["\x{B0EC}", "\x{B6E5}", "\x{B8DE}", "\x{BBB0}", "\x{BBCD}", "\x{BCB7}", "\x{C6F3}", "\x{C8AC}", "\x{CFBB}"] irb(main):031:0> sorted.zip(codes) => [["一", "\x{B0EC}"], ["九", "\x{B6E5}"], ["五", "\x{B8DE}"], ["三", "\x{BBB0}"], ["四", "\x{BBCD}"], ["七", "\x{BCB7}"], ["二", "\x{C6F3}"], ["八", "\x{C8AC}"], ["六", "\x{CFBB}"]]
どちらも文字コード順に並んでいます。
さて、これは一体どういうルールになってるんでしょうか・・・??
シフトJISやEUCは音読みの順番で並んでいる
これはシフトJISの文字コード表をじっと見ていると予想がつきます。
はい、周りの漢字を見ていると「いちいちいち・・・」「にににに・・・」「さんさんさん・・・」。
これはどうやら「音読み」の順番に並んでいるようです。
というわけで、「音読みの順に並んでいる」という仮説を立てて検証してみましょう。
irb(main):032:0> kana = %w(いち に さん し ご ろく しち はち く) => ["いち", "に", "さん", "し", "ご", "ろく", "しち", "はち", "く"] irb(main):033:0> sorted_kana = kana.sort => ["いち", "く", "ご", "さん", "し", "しち", "に", "はち", "ろく"] irb(main):034:0> sorted_kanji = x.map{|s| s.encode("Shift_JIS")}.sort.map{|s| s.encode("UTF-8")} => ["一", "九", "五", "三", "四", "七", "二", "八", "六"] irb(main):035:0> sorted_kana.zip(sorted_kanji) => [["いち", "一"], ["く", "九"], ["ご", "五"], ["さん", "三"], ["し", "四"], ["しち", "七"], ["に", "二"], ["はち", "八"], ["ろく", "六"]]
最後の行を見てもらえばわかるように、音読みの順に漢数字が並んでいることを確認できました!
ちなみに余談ですが、ひらがなをソートした場合はちゃんと50音順にソートしてくれるようですね。(例外があるかもしれませんが)
まとめ
というわけで、今回調べた内容のまとめです。
UTF-8の場合、漢数字は部首画数順に並ぶ
シフトJISとEUCの場合、漢数字は音読みの順に並ぶ
いずれも数字順には並ばない
・・・というのが今日のまとめです。
みなさんも漢数字をソートするときは注意してくださいね!!
文字コードをもっと詳しく知るための技術書
「文字コード超研究」は僕も読みました。
「プログラマのための文字コード技術入門」もいい本みたいですよ。
- 作者: 深沢千尋
- 出版社/メーカー: ラトルズ
- 発売日: 2011/07/19
- メディア: 単行本(ソフトカバー)
- 購入: 2人 クリック: 8回
- この商品を含むブログ (4件) を見る
プログラマのための文字コード技術入門 (WEB+DB PRESS plus) (WEB+DB PRESS plusシリーズ)
- 作者: 矢野啓介
- 出版社/メーカー: 技術評論社
- 発売日: 2010/02/18
- メディア: 単行本(ソフトカバー)
- 購入: 34人 クリック: 578回
- この商品を含むブログ (129件) を見る
2014.12.04 5:00am 追記:執筆裏話
本文ではRubyを使ってソートしていますが、この問題に気付いたのはSQLでテスト用のユーザーを名前順にソートしたときです。
「SELECT * FROM users ORDER BY last_name」みたいなSQLを実行したときに、テストユーザーが「一郎」「三郎」「二郎」の順で並んだときに「あれ?」と気付きました。
(正確にはお客さんに指摘されたんですが)
なまじ「一郎」が最初に出てきちゃうから、「じゃあそのまま二郎、三郎、って続けてよ!」と思ってしまうんですよねえ。