Ruby + ActiveSupport で、ある期間と別の期間との関係を調べるメソッドを書いてたところ、期間に無限大を含む場合に失敗した。
# ruby 1.8.7
require "date"
require "rubygems"
require "active_support"
(Date.today..(1.0 / 0)).include?(Date.today..Date.tomorrow) # true これは問題ないが、逆にすると...
(Date.today..Date.tomorrow).include?(Date.today..(1.0 / 0)) # NoMethodError: undefined method `<=' for nil:NilClass
ActiveSupport のコードを追ってみると、原因は Float <=> Date で nil が返ってしまう点にあるらしい。
Date.today <=> 1.0 # 1
1.0 <=> Date.today # nil
Date が演算子の左側にある場合、Date#<=> が呼ばれる。実装は下記。
# File lib/date.rb, line 1267def <=> (other)
case other
when Numeric
return @ajd <=> other
when Date
return @ajd <=> other.ajd
end
nil
end
演算子の右側が Numeric なら、@ajd と比較している。@ajd は、紀元前4713年1月1日からの日数を示す数値。数値なので Float と比較できる。
いっぽう、Float が演算子の左側にある場合は、Float#<=> が呼ばれるため、Date をどう扱っていいかわからず、比較できない。
そこで Date#coerce を実装した。
require "date"
class Date
def coerce(other)
return [other, @ajd] if other.is_a? Numeric
nil
end
end
Numeric は、比較対象と型が異なる場合、比較対象の coerce メソッドを (あれば) 呼び出して、型を合わせてから比較する。これにより、@ajd を使って比較できるようにする。これで演算子の左側が Float でも比較できるようになり、include? や overlaps? も動く。
1.0 <=> Date.today # -1
(Date.today..Date.tomorrow).include?(Date.today..(1.0 / 0)) # false
また、始点に Float を、終点に Date を持つ Range も作成できる。
(-(1.0 / 0)..Date.today) # 無限遠の過去から今日までの Range
include? や overlaps? 含め、おおむね期待どおりに動くが、この場合、Date の範囲でなく Float の範囲になってしまうので、Enumerable にならないなどの制限がある。
しかし coerce なんて、ごく最近、プログラミング言語 Ruby 読むまで知らなかった。記述細かいなあと思ってたけど、役に立つもんですね。