先日、Railsアプリで、ある日付の午前0時から有効になるという処理を書いたが、0時に有効にならず、9時まで有効にならないというバグに遭遇した。
原因は以下のコードのように、モデルの enabled_from に対応するDBのカラムはDATE型であるのに、Date型ではなくTime型と比較していた事だった。
now = Time.now if now < model.enabled_from # まだ有効でないという表示をする render :not_enabled_yet return end
しかし、Timeとの比較が何故9時間ずれる原因となるのか良く分からなかったので調べてみた。
まず、Ruby標準ではTime型とDate型の比較はできずエラーとなる。RailsではActiveSupportでTimeが拡張されているのでDateと比較できる。ActiveSupportのコードを見ると…
# RUBY_HOME/gems/1.8/gems/activesupport-2.3.8/lib/active_support/core_ext/time/calculations.rb def compare_with_coercion(other) # if other is an ActiveSupport::TimeWithZone, coerce a Time instance from it so we can do <=> comparison other = other.comparable_time if other.respond_to?(:comparable_time) if other.acts_like?(:date) # other is a Date/DateTime, so coerce self #to_datetime and hand off to DateTime#<=> to_datetime.compare_without_coercion(other) else compare_without_coercion(other) end end
# RUBY_HOME/gems/1.8/gems/activesupport-2.3.8/lib/active_support/core_ext/time/conversions.rb def to_datetime ::DateTime.civil(year, month, day, hour, min, sec, Rational(utc_offset, 86400)) end
to_datetime.compare_without_coercion(other) というコードから解かるように、TimeはDateTimeに変換されてからDateと比較されている。DateTimeとDateは標準でも比較可能だ。今度はRubyに添付しているDateTimeの<=>メソッドのソースを見ると
# RUBY_HOME/1.8/data.rb # Comparison is by Astronomical Julian Day Number, including # fractional days. This means that both the time and the # timezone offset are taken into account when comparing # two DateTime instances. When comparing a DateTime instance # with a Date instance, the time of the latter will be # considered as falling on midnight UTC. def <=> (other) case other when Numeric; return @ajd <=> other when Date; return @ajd <=> other.ajd end nil end
コメントにDateをUTCの深夜0時として扱うと書かれている。DateをJSTの午前0時のつもりで扱うのがよろしくないようだ。
2010-08-31 00:00:00 JSTという値のTime型オブジェクトは 2010-08-30 15:00:00 UTCとして計算されるが、TimeZoneを持たないDateでは、2010-08-31 というオブジェクトは 2010/08/31 00:00:00 UTCとして計算される。よって計算するときにDateの方が9時間進んでしまうのだった。
不用意に違う型のオブジェクト同士を比較するなという事ですな。