give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

漢数字が数字順にソートされない理由を調べてみた

はじめに:「なぜ漢数字は数字順に並ばない!?」

先日、こんなツイートをしたところ、結構たくさんの人にリツイートされました。(執筆時点で50件以上)


「なぜ漢数字は数字順に並ばないのか」という問いに対して、表面的な回答をするなら「数字順に並ばないのは、数字の大きさではなく文字コード順でソートされているから」ということになります。


いや、もちろんそれはわかってるんです。
問題は「そもそもなんで数字順に文字コードを振らなかったの!?」ということです。


感覚的には「一郎、二郎、三郎」って並んでほしいじゃないですか。でも、プログラム上でソートすると「一郎、三郎、二郎」って並んじゃうんです。
これはちょっと気持ち悪い。


というわけで、どういうルールでこの文字コードが振られているのかを調べてみました。

一から九までをソートしてみる

まず、漢数字の一から九までを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にそれらしき情報が載っていました。

配列

原則として登録される毎に部首画数順で配列されている。但し一部に乱れが存在している上追加が相次いだために検索が困難になってきており、Unihanデータベースでは割り当てられたUnicode値と部首番号、部首別画数から導出される値をソートキーとして規格化している。
 

CJK統合漢字 - Wikipedia


おお、なるほど、「部首画数順」なのか!!
というわけで、文字コードは部首画数順で割り振られているという仮説を元に、各漢字の部首と画数を調べてみました。

各漢字の部首と画数を検証する

部首と画数の検索と確認は次のサイトを利用させてもらいました。

 

「一」「七」「三」の部首は一画の「一(いち)」

以下の通り、「一」「七」「三」の部首は「一(いち)」で、画数の順番も「一」「七」「三」になってます!

部首が一「いち」の漢字一覧
f:id:JunichiIto:20141203181428p:plain
 

「九」の部首は一画の「乙(おつ)」

「九」の部首は「乙(おつ)」でした。(知らなかった・・・)

部首が乙「おつ」の漢字一覧
f:id:JunichiIto:20141203181653p:plain
 

「二」「五」の部首は二画の「二(に)」

「二」「五」の部首は「二」です。おお、これも文字コードの順番通り。

部首が二「に」の漢字一覧
f:id:JunichiIto:20141203182018p:plain
 

「八」「六」の部首は二画の「八(はち)」

「八」「六」の部首は「八」でした。残すはあと一つ!

部首が八「は・はち・はちがしら」の漢字一覧
f:id:JunichiIto:20141203182321p:plain
 

「四」の部首は三画の「口(くにがまえ)」

最後に残った「四」は三画の「くにがまえ」が部首になっていました!

部首が「くにがまえ」の漢字一覧
f:id:JunichiIto:20141203182604p:plain
 

おさらい:やっぱり部首画数順だった!

というわけで上で見た部首と画数をここでまとめてみましょう。

  • 「一」「七」「三」の部首は一画の「一(いち)」
  • 「九」の部首は一画の「乙(おつ)」
  • 「二」「五」の部首は二画の「二(に)」
  • 「八」「六」の部首は二画の「八(はち)」
  • 「四」の部首は三画の「口(くにがまえ)」

そして、一から九までをソートした結果はこれでした。

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の文字コード表をじっと見ていると予想がつきます。

Shift_JIS 文字コード表

f:id:JunichiIto:20141203183939p:plain
f:id:JunichiIto:20141203183945p:plain
f:id:JunichiIto:20141203183954p:plain

はい、周りの漢字を見ていると「いちいちいち・・・」「にににに・・・」「さんさんさん・・・」。
これはどうやら「音読み」の順番に並んでいるようです。


というわけで、「音読みの順に並んでいる」という仮説を立てて検証してみましょう。

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の場合、漢数字は音読みの順に並ぶ


いずれも数字順には並ばない


・・・というのが今日のまとめです。
みなさんも漢数字をソートするときは注意してくださいね!!

文字コードをもっと詳しく知るための技術書

「文字コード超研究」は僕も読みました。
「プログラマのための文字コード技術入門」もいい本みたいですよ。

文字コード「超」研究 改訂第2版

文字コード「超」研究 改訂第2版

プログラマのための文字コード技術入門 (WEB+DB PRESS plus) (WEB+DB PRESS plusシリーズ)

プログラマのための文字コード技術入門 (WEB+DB PRESS plus) (WEB+DB PRESS plusシリーズ)

 

2014.12.04 5:00am 追記:執筆裏話

本文ではRubyを使ってソートしていますが、この問題に気付いたのはSQLでテスト用のユーザーを名前順にソートしたときです。


「SELECT * FROM users ORDER BY last_name」みたいなSQLを実行したときに、テストユーザーが「一郎」「三郎」「二郎」の順で並んだときに「あれ?」と気付きました。
(正確にはお客さんに指摘されたんですが)


なまじ「一郎」が最初に出てきちゃうから、「じゃあそのまま二郎、三郎、って続けてよ!」と思ってしまうんですよねえ。