RailsでTimeとDateを比較すると9時間ずれる

先日、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時間進んでしまうのだった。

不用意に違う型のオブジェクト同士を比較するなという事ですな。