LoginSignup
38
35

More than 5 years have passed since last update.

Rails, 1.month.ago とは何か

Last updated at Posted at 2014-09-17

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#classActiveSupport::Duration#method_missing を経由して、 ActiveSupport::Duration#value へ proxy される。

結果、 1.month.classFixnum を返す。

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? は、ダックタイプによる DateTime の型判定。

(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

ところで

Repro株式会社 では一緒に切磋琢磨できる仲間を募集しています! ぜひこちらをごらんください!

38
35
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
35