Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

[Rails5] ActiveSupport::Durationの期間処理メソッド(1)演算、比較など

こんにちは、hachi8833です。

Active Support探訪シリーズ、今回は週刊Railsウォッチに何度か登場したActiveSupport::Durationにお邪魔します。Core Extensionsの外としては初めてですね。何回かに分けてお送りします。

今回のメソッド

以下もご覧ください。

条件

ActiveSupport::Durationのpublicメソッド

active_support/duration.rbにあるActiveSupport::Durationのメソッドです。各コードについては次回のぞいてみます。

ActiveSupport::Durationは日付や時刻による期間(時点と時点の間の大きさ)を表現するライブラリです。

Provides accurate date and time measurements using Date#advance and Time#advance, respectively. It mainly supports the methods on Numeric.

http://devdocs.io/rails~5.0/activesupport/durationで確認できるメソッドは以下です。

演算系

メソッド 説明
#+ Durationに別のDurationまたはNumericを足す
Numericの単位は秒
#- Durationから別のDurationまたはNumericを引く
Numericの単位は秒
#-@ Durationの符号を反転する単項演算子

以下はRailsコンソールでの実行結果です(Rubyのirbやpryで試す場合は明示的に`require 'active_support/all'を実行しておく必要があります)。

# サンプル
1.month + 3.days         #=>  1 month and 3 days
1.month - 1.years        #=> -1 year and 1 month
(1.month + 3.days).class #=> ActiveSupport::Duration

生成されているのはStringではなく、ActiveSupport::Durationクラスのオブジェクトです。

比較系

メソッド 説明
#== 引数(Durationの場合はその値)が同じ値を持つ場合trueを返す
#eql? 引数がDurationクラスかつ値が同じ場合にtrueを返す
# サンプル
dr1 = 1.month             
dr2 = 30 * 24 * 60 * 60   # 1か月を秒で表す

dr1 == dr2                #=> true (値が同じ)
dr1.eql? dr2              #=> false(値は同じだがDurationではない)

dr2 = 10.months - 9.month
dr1.eql? dr2              #=> true (Durationかつ値も同じ)

dr2 = 31.days
dr1 == dr2                #=> false(1か月と31日は等しくない)
dr1.eql? dr2              #=> false(1か月と31日は等しくない)

期間系

メソッド 説明
#ago#until 引数をDurationで表される分さかのぼった新しいTimeかDateを返す
デフォルトは現在時刻
#since#from_now 引数をDurationで表される分未来に進んだ新しいTimeかDateを返す
デフォルトは現在時刻

Duration#untilDuration#agoのエイリアス、Duration#from_nowDuration#sinceのエイリアスです。

なお、これらのメソッドはDurationとDateTimeで実装状況が異なっていますので注意が必要です。

  • DateTime#agoDateTime#sinceは実装されている
  • DateTime#untilDateTime#from_nowは実装されていない(エイリアスがない)
dr = 3.days                    #=> drはActiveSupport::Duration
dt = DateTime.now              #=> dtはDateTimeオブジェクト

dr.method(:ago).owner          #=> ActiveSupport::Duration
dt.method(:ago).owner          #=> DateTime

dr.method(:until).owner        #=> ActiveSupport::Duration
dt.method(:until).owner        # NameError(未実装)

dr.method(:since).owner        #=> ActiveSupport::Duration
dt.method(:since).owner        #=> DateTime

dr.method(:from_now).owner     #=> ActiveSupport::Duration
dt.method(:from_now).owner     # NameError(未実装)

このため、DateTimeオブジェクトをレシーバーにすると#until#from_nowは動きません。

dr.ago dt       #=> Sun, 29 Jan 2017 14:19:40 +0900
dt.ago 3.days   #=> Sun, 29 Jan 2017 14:19:40 +0900

dr.until dt     #=> Sun, 29 Jan 2017 14:19:40 +0900
dt.until 3.days # NoMethodError:

kazzさんが掘り当ててくれたActiveSupport::DateTimeの該当箇所のコードには#until#from_nowといったエイリアスは特に設定されていません。その割には#inというエイリアスはあります。

#calculations.rb
class DateTime

  def ago(seconds)
    since(-seconds)
  end

  def since(seconds)
    self + seconds
  rescue
    to_datetime.since(seconds)
  end
  alias :in :since
...

理由は不明ですが、untilという名前が制御文っぽいから嫌がられたのか、英語っぽくするためのシンタックスシュガーの必要を感じなかったのか、単にこれからエイリアスを設定するつもりなのか、あるいはDurationでは概念上#until#from_nowが必要だったのだろうか、などと考えてしまいました。機会があればもう少し追ってみたいと思います。

ActiveSupport::Durationの残りのメソッドについては次回扱います。ご期待ください。

関連記事

[Rails5] Active Supportの概要をつかむ

Rails: ビューのHTMLエスケープは#link_toなどのヘルパーメソッドで解除されることがある


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。