レシピ1.5 日付の演算

問題

2つの日付の時間差を割り出したい。または、日付に数値を足して、過去または未来の日付を割り出したい。

 Timeオブジェクトと数値の加算または減算を行うと、その秒数が足されるか引かれることになる。

require 'date'
y2k = Time.gm(2000, 1, 1)             # => Sat Jan 01 00:00:00 UTC 2000
j2k + 1                                          # => Sat Jan 01 00:00:00 UTC 2000
y2k - 1                                          # => Fri Dec 31 23:59:59 UTC 1999
y2k + (60 * 60 * 24 * 365)              # => Sun Dec 31 00:00:00 UTC 2000

y2k_dt = DateTime.new(2000, 1, 1)             
(y2k_dt + 1).to_s                                         # => "2000-01-02T00:00:00Z"
(y2k_dt - 1).to_s                                          # => "1999-12-31T00:00:00Z"
(y2k_dt + 0.5).to_s                                       # => "2000-01-01T12:00:00Z"
(y2k_dt + 365).to_s                                     # => "2000-12-31T00:00:00Z"

 Timeから別のTimeを引くと、、2つの日付の感覚が秒数で得られる。Dateから別のDateを引くと、感覚が日数で得られる。

day_one = Time.gm(1999, 12, 31)
day_two = Time.gm(2000, 1, 1)
day_two - day_one                                       # 86400.0
day_one - day_two                                       # -86400.0

#現在と10秒後の時間を比較する
before_time = TIme.now
before_datetime = DateTime.now
sleep(10)
Time.now - before_time                                # => 10.003414
DateTime.now - before_datetime                   # => Rational(5001557, 43200000000)

 Ruby on Railsの必須条件であるactivesupport gemには、NumericやTimeで時間を操作するための便利な関数が数多く定義されている。(Facets Moreライブラリについても同じである。)

require 'rubygems'
require 'active_support'

10.days.ago                                       # => Wed Mar 08 19:54:17 EST 2006
1.month.from_now                              # => Mon Apr 17 20:54:17 EDT 2006
2.weeks.since(Time.local(2006, 1, 1))   # => Sun Jan 15 00:00:00 EST 2006
y2k - 1.day                                        # => Fri Dec 31 00:00:00 UTC 1999
y2k + 6.3.years                                  # => Thu Apr 20 01:48:00 UTC 2006
6.3.years.since y2k                             # => Thu Apr 20 01:48:00 UTC 2006

解説
 Ruby日付の演算は、タイムオブジェクトの内部表現が数値であることをうまく利用している。日付への数値の加算や2つの日付の差分は、この内部表現の数値に対する加算や減算として処理される。TImeに1を足すと1秒を足すことになり、DateTimeに1を足すと1日を足すことになるのはこのためである。TImeは0時からの経過病数として格納され、DateまたはDateTimeは(別の)0時からの経過秒数として格納される。
 全ての算術演算が日付に対して意味を持つとは限らない。内部表現である数値を掛け合わせれば「2つの日付の乗算」が可能だが、実際の時間に置き換えても意味をなさないため、このような演算子は定義されていない。数値を実世界に置き換えて考えれば、妥当な操作は自ずと制限される。DateTimeオブジェクトに右シフト演算子や左シフト演算子を使用すると、その日付けに対して特定の月数が足されるか引かれる。

(y2k_dt >> 1).to_s                         # => "2000-02-01T00:00:00Z"
(y2k_dt << 1).to_s                         # => "1999-12-01T00:00:00Z"

 activesupportのNumeric#monthメソッドでお同様の結果が得られるが、このメソッドは「1ヶ月」が30日であると想定しており、特定の月の長さに対応しない。

y2k + 1.month                              # => Mon Jan 31 00:00:00 UTC 2000
y2k - 1.month                               # => Thu Dec 02 00:00:00 UTC 1999

対照的に、月の日数が短い(例えば、31日から開始し、次の月が30日しかない)場合、標準ライブラリは新しい月の最後の日を使用する。

# 9月は30日までしかない
halloween = Date.new(2000, 10, 31)
(halloween << 1).to_s                                  # => "2000-09-30"
(halloween >> 1).to_s                                  # => "2000-11-30"
(halloween >>2).to_s                                   # => "2000-12-31"

leap_year_day = Date.new(1996, 2, 29)
(leap_year_day << 1).to_s                            # => "1996-01-29"
(leap_year_day >> 1).to_s                            # => "1996-03-29"
(leap_year_day >> 12).to_s                          # => "1997-02-28"
(leap_year_day << 12 * 4).to_s                     # => "1992-02-29"

参照
・レシピ1.4、レシピ1.6
・RailのActiveSupport::CoreExtentions::Numeric::TimeのRDoc
(http://api.rubyonrails.com/classes/ActiveSupport/CoreExtensions/Numeric/Time.html)