1.month.ago
Railsで1ヶ月前の日時を使う場合、 1.month.ago
を使う。
1.month.ago #=> Sun, 17 Aug 2014 15:17:04 JST +09:00
では、 1.month
とは一体なんなのか。
1.month #=> 2592000
1.month.class #=> Fixnum
Fixnum
らしい。1.month == 30 * 24 * 60 * 60
で、30日分の秒数だ。
しかし、1ヶ月分の日数は月によって異なるはずだが、以下のコードは日付がずれることはない。
(0..12).each{|m| puts m.months.ago }
2014-09-17 15:20:57 +0900
2014-08-17 15:20:57 +0900
2014-07-17 15:20:57 +0900
2014-06-17 15:20:57 +0900
2014-05-17 15:20:57 +0900
2014-04-17 15:20:57 +0900
2014-03-17 15:20:57 +0900
2014-02-17 15:20:57 +0900
2014-01-17 15:20:57 +0900
2013-12-17 15:20:57 +0900
2013-11-17 15:20:57 +0900
2013-10-17 15:20:57 +0900
2013-09-17 15:20:57 +0900
これはどういうことか。実は、1.month
が返す値は、 Fixnum
ではない。
# File activesupport/lib/active_support/core_ext/integer/time.rb, line 35
def months
ActiveSupport::Duration.new(self * 30.days, [[:months, self]])
end
ActiveSupport::Integer#months
は、 ActiveSupport::Duration
のインスタンスを返すように作られている。
ActiveSupport::Duration
では、 ActiveSupport::Duration
とは何か。
# ActiveSupport::Duration のコードを抜粋
module ActiveSupport
class Duration < ProxyObject
attr_accessor :value, :parts
def initialize(value, parts) #:nodoc:
@value, @parts = value, parts
end
# --- 中略 ---
protected
def method_missing(method, *args, &block) #:nodoc:
value.send(method, *args, &block)
end
end
end
ActiveSupport::ProxyObject
は、 BasicObject
を継承している。
BasicObject
とは、通常の Object
から基本的なメソッドを省いた、より抽象的なオブジェクトクラスだ。
BasicObject
には #class
などのメソッドも定義されていない。このため、ActiveSupport::Duration#class
は ActiveSupport::Duration#method_missing
を経由して、 ActiveSupport::Duration#value
へ proxy される。
結果、 1.month.class
は Fixnum
を返す。
ActiveSupport::Duration#ago
では、 ActiveSupport::Duration#ago
は何をしているのか。
# ActiveSupport::Duration のコードを抜粋
module ActiveSupport
class Duration < ProxyObject
# --- 中略 ---
def ago(time = ::Time.current)
sum(-1, time)
end
# --- 中略 ---
protected
def sum(sign, time = ::Time.current) #:nodoc:
parts.inject(time) do |t,(type,number)|
if t.acts_like?(:time) || t.acts_like?(:date)
if type == :seconds
t.since(sign * number)
else
t.advance(type => sign * number)
end
else
raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
end
end
end
# --- 中略 ---
end
end
parts
の値によって挙動が変わる。 1.month
は [[:months, self]]
を渡しており、この値が parts
にセットされる。 1.day
なども同様。
Object#acts_like?
は、ダックタイプによる Date
と Time
の型判定。
(Date/Time/DateTime)#advance
は、type で指定された単位を考慮して値を変更する。
結果、 1.month.ago
は正確に1ヶ月前の日時を返す。
ActiveSupport::Duration の合成
ActiveSupport::Duration#+
は、parts を合成するので、以下のようなコードが期待通りに動作する。
DateTime.now #=> Wed, 17 Sep 2014 16:00:22 +0900
(2.months + 3.days).ago #=> Mon, 14 Jul 2014 15:59:53 JST +09:00