Edited at

文字列の大小比較をもう少し詳しく調べてみる(チェリー本の補足として)

More than 1 year has passed since last update.


はじめに

書籍「プロを目指す人のためのRuby入門」(チェリー本)の内容について、ちょっとわかりにくい箇所があった、というブログ記事がありました。

Kyoto.rb Meetup 20171216 - おうさまのみみはロバのみみ


↓がよくわからない、文字に当てられている番号で比較している?という質問を参加者のかたにしてみた。


'a' < 'b' # true わかる

'a' < 'A' # false わからない
'a' > 'A' # もっとわからない

'abc' < 'def' # true わかる
'abc' < 'ab' # false ?アスキーコードの合計値で比較しているわけでもない?
'abc' < 'abcd' # true ???もしかして文字列の長さか?

'あいうえお' < 'かきくけこ' # true

うーん、そう言われてみると、確かにちょっと説明が雑だったかもしれませんね。。

最終的には勉強会の中で質問して解決されたようですが、本の著者として僕なりの説明を補足しておきます。


文字列の大小比較をもう少し詳しく調べてみる

bytesというメソッドを使うと文字列の各バイトを配列として取得することができます。

'a'.bytes #=> [97]

'b'.bytes #=> [98]
'A'.bytes #=> [65]
'B'.bytes #=> [66]
'abc'.bytes #=> [97, 98, 99]
'ABC'.bytes #=> [65, 66, 67]

半角英数字記号(いわゆるアスキー文字)であれば「1文字=1バイト」ですが、日本語のようなマルチバイト文字は「1文字=2バイト以上」になることもあります。

'あ'.bytes     #=> [227, 129, 130]

'あいう'.bytes #=> [227, 129, 130, 227, 129, 132, 227, 129, 134]

文字列の大小比較は、このバイト配列の大小を比較することと同じになります。

# 'a'.bytes = [97]、'A'.bytes = [65]なので、'a'の方が大きい

'a' > 'A' #=> true

以下は3バイト目までが同じで、4バイト目が異なる(そもそも左辺には4バイト目がない)ケースです。

# 'abc'.bytes = [97, 98, 99]、'abcd'.bytes = [97, 98, 99, 100]で、

# 3バイト目までは同じだが、4バイト目は'abcd'にしかないので'abcd'の方が大きい
'abc' < 'abcd' #=> true

もちろん、単純に文字列の長さで比較しているわけではないので、たとえば1バイト目の時点で大小が明確になっていれば、そこで大小が決まります。

# 'zbc'.bytes = [122, 98, 99]、'abcd'.bytes = [97, 98, 99, 100]で、

# 1バイト目の時点で'zbc'の方が大きいことが決定する
'zbc' > 'abcd' #=> true

日本語文字列の場合も考え方は同じです。

# 'あいう'.bytes = [227, 129, 130, 227, 129, 132, 227, 129, 134]

# 'かきく'.bytes = [227, 129, 139, 227, 129, 141, 227, 129, 143]
# なので、3バイト目で'かきく'の方が大きいことが決定する
'あいう' < 'かきく' #=> true


Q. そもそも文字列の大小比較をする必要ってあるの?

ところで、前述のブログでは次のような感想が述べられています。


とあるのでバイト列での比較になっていた。 でもこんなトリッキーなコード書くことがあるのだろうか?

多分だがこの比較が発生し得るコードがレビューで来たらぼくならリジェクトすると思う。 少なくとも動作の予測が立ちにくいので別の実装を求めるか完全比較以外はリジェクトするかな。 入力される値がある程度決まっているなら配列とかに持たせてチェックさせる…みたいな実装する。


たしかに、文字列の大小関係を比較するコードがあると、ちょっと「うっ」と思いますね。

たとえば、次のようなコードを書いていると、うっかり賞味期限切れのお刺身を食べることになってしまいます💀

# 注: ?で終わるメソッドなので本来は真偽値を返すべきだが、ここではあえて文字列を返す

def eat_sashimi?
# 今日は12月1日で、賞味期限が9月1日
today = '2017/12/1'
expired_on = '2017/9/1'

# 文字列のまま比較するとヤバい
if today <= expired_on
"まだ食べられます!"
else
"賞味期限切れです。"
end
end

eat_sashimi? #=> "まだ食べられます!"

# このあとたぶんお腹を壊す

上のようなコードであれば、文字列のまま比較するのではなく、適切なデータ型に変換してから比較する方が安全です。

require 'date'

def eat_sashimi?
# 文字列ではなく、日付として比較する
today = Date.parse('2017/12/1')
expired_on = Date.parse('2017/9/1')

if today <= expired_on
"まだ食べられます!"
else
"賞味期限切れです。"
end
end

eat_sashimi? #=> "賞味期限切れです。"

# よかったね

ただし、文字列を不等号(<>=など)で直接比較することはなくても、配列の並び替えなどで内部的に文字列の大小関係を比較することはあると思います。

array = ['a', 'b', 'A', 'abc', 'def', 'ab', 'abcd', 'あいうえお', 'かきくけこ']

# 配列をソートする(内部的には大小関係を比較している)
array.sort
#=> ["A", "a", "ab", "abc", "abcd", "b", "def", "あいうえお", "かきくけこ"]

なので、文字列であっても大小関係を比較するユースケースは十分にあり得ます。

なお、大小比較や並び替えでは<=>演算子(通称UFO演算子)が重要な役割を果たすので、こちらもチェックしておきましょう(「プロを目指す人のためのRuby入門」の中でも8.5.4項で説明しています)。

参考: module Comparable (Ruby 2.4.0)


まとめ

というわけで、この記事ではRubyの文字列比較について、少し詳しく調べてみました。

「文字列の大小って何で決まるんだろう?」と疑問に思われている方が他にもいたら、参考にしてみてください!


あわせて読みたい

漢数字の「一二三」は、ソートしても「一二三」の順に並びません。

%w(一 二 三).sort #=> ["一", "三", "二"]

「えー、なんで!?」と思った方はこちらのブログをどうぞ。

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