RubyのDateTimeとTimeはどちらを使うべきか

TimeはもともとPOSIXのtime_t型のラッパーで、内部的には1970年1月1日を起点とする秒数を保持している。

WikipediaによるUnix timeの記述を見ると、time_tは、なんと初期のUnixでは32ビット整数であったものの、60Hzのクロックに同期してカウントアップしていたそうだ。1秒間に60カウント進む。これでは2年半しか表現できない。のちに1Hz、つまり1秒1カウントで進むように変更され、これで前後130年の時刻を表現できるようになったそうな。

よく知られる2038年問題は32ビットの符号付き整数で表現できる1970年1月1日を起点とした場合の上限が2038年1月19日の午前3時14分7秒であるという話。

RubyのTimeクラスも、この制限を受けていた。

「受けていた」というのは、Ruby 1.9.2や、1.9系の新機能を1.8系へバックポートした1.8.7以降では、この制限はなくなりBIGNUMで扱える数字を扱うようになっているからだ。例えば、time.cの時刻の足し算を見ると、

static VALUE
add(VALUE x, VALUE y)
{
    if (FIXNUM_P(x) && FIXNUM_P(y)) {
        long l = FIX2LONG(x) + FIX2LONG(y);
        if (FIXABLE(l)) return LONG2FIX(l);
        return LONG2NUM(l);
    }
    if (RB_TYPE_P(x, T_BIGNUM)) return rb_big_plus(x, y);
    return rb_funcall(x, '+', 1, y);
}

と、Fixnumに収まらない場合は何ごともなかったかのようにBignumで計算するようになっている。西暦1万年でも西暦100万年でも扱える。いや重要なのは、2038年を超えても大丈夫とか、1901年より前の時刻も指し示せるということなんだろうか(しかし、数百年単位で時間を扱うのは現実的に可能なんだろうか。物理的な経過時間と歴史上の時刻なんて対応させるのは極めて困難だろうし、暦だって非連続なところがいっぱいあるだろうし、現実的な意味としては「指し示せる」というより「表現できる」ということか)。

Rubyの標準クラスにはTimeに似た時間を扱うクラスとして、DateクラスとDateTimeクラスというのもがある。requre 'date' とすることで利用できる。これらは文字通り、日付、もしくは時刻も含む日付を扱う。DateTimeはDateクラスのサブクラス。

DateTimeクラス最強。なんでDateやTimeがあるんだ? というか、この混乱はなんだ?

まず1つ目の理由として、もともとTimeがtime_tの制限を受けていたことがある。Timeで表現できる日付には制限があったのだ。だからDateTimeが必要だった。でも、これは今では当てはまらない。

もう1つの理由として、処理速度が大きく違ったというのがある。Ruby1.8.6がリリースされた頃、2007年ごろのRubyのソースを見ると、lib/date.rbにあるDateTimeクラスはRubyによる実装だったことが分かる。だから、Timeクラスのほうが断然処理が速かった。ベンチを取ると以下の通り。

require 'benchmark'
require 'date'

Benchmark.bm do |bm|
  bm.report('DateTime:') do
    n1 = DateTime.now
    n2 = DateTime.now
    1_000_000.times{ n1 < n2 }
  end
  bm.report('Time:    ') do
    n1 = Time.now
    n2 = Time.now
    1_000_000.times{ n1 < n2 }
  end
end
$ ruby -v timebench.rb
ruby 1.8.6 (2010-09-02 patchlevel 420) [i686-darwin11.4.2]
      user     system      total        real
DateTime:  4.710000   0.000000   4.710000 (  4.809847)
Time:      0.280000   0.000000   0.280000 (  0.295962)
$ rvm use 1.9.3
Using /Users/ken/.rvm/gems/ruby-1.9.3-p194
$ ruby -v ~/Desktop/timebench.rb
ruby 1.9.3p194 (2012-04-20 revision 35410) [x86_64-darwin11.3.0]
       user     system      total        real
DateTime:  0.240000   0.000000   0.240000 (  0.252748)
Time:      0.250000   0.000000   0.250000 (  0.255503)
$

1.8.6や1.8.7のDateTimeはRuby実装のため、Timeよりも遅い。でも、Ruby 1.9.3ではDateTimeもC実装になっていて、これもDateTimeクラスを使わない理由にならない。

Timeクラスと、Date/DateTimeの最大の違いは、秒を扱うか、日を扱うかという点だろうか。Timeのインスタンスに1を足すと1秒後になり、Date/DateTimeに1を足すと1日後になる。データ表現としてTimeは整数、Date/DateTimeではRationalという違いもある。

$ irb -rdate
[1] pry(main)> t = Time.now
=> 2012-10-01 06:12:50 +0900
[2] pry(main)> t + 1
=> 2012-10-01 06:12:51 +0900
[3] pry(main)> d = DateTime.now
=> #<DateTime: 2012-10-01T06:13:04+09:00 ((2456201j,76384s,174250000n),+32400s,2299161j)>
[4] pry(main)> d + 1
=> #<DateTime: 2012-10-02T06:13:04+09:00 ((2456202j,76384s,174250000n),+32400s,2299161j)>
[5] pry(main)> d + Rational(1, 3)
=> #<DateTime: 2012-10-01T14:13:04+09:00 ((2456202j,18784s,174250000n),+32400s,2299161j)>
[6] pry(main)> d + Rational(1, 24)
=> #<DateTime: 2012-10-01T07:13:04+09:00 ((2456201j,79984s,174250000n),+32400s,2299161j)>
[7] pry(main)> 

DateTimeのオブジェクトにRational(1, 3)、つまり3分の1日を加えると8時間後の時刻が得られる。日付より小さい単位を計算したいときには、Timeのほうが便利かもしれない。のだけど、この辺はActiveSupportによるRailsの拡張を勉強しないと、何をどう使うべきなのか、まだよく分からない。

このほかのTimeとDateTimeの違いは、

  • require 'date' しないとDate/DateTimeは使えない
  • require 'time' としてTimeクラスの拡張メソッドをロードしておかないと時刻文字列のパースは苦しそう
  • 文字列に変換すると、TimeはC言語のctime相当、DateTimeはISO 8601
[7] pry(main)> t.to_s
=> "2012-10-01 06:12:50 +0900"
[8] pry(main)> d.to_s
=> "2012-10-01T06:13:04+09:00"
[9] pry(main)>

というのもある。

結局、TimeかDateTimeのどちらを使うべきかというのはよくわからない。Timeの欠点(130年分しか扱えない)もDateTimeの欠点(遅い)も解消されている今、ますます違いが良くわからない。これは、Javaのような委員会できっちり決めて実装が進む言語と異なり、Rubyという言語では「Timeじゃ足りねぇ、DateTimeを作るべ」、「Time拡張して32ビット制限はずしたよ」、「DateTime遅いからCにしたよ」と同時多発的にビミョウに目的の違ったライブラリの進化が起こったから、という理解でいいんだろうか。

参考: