0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

gemを読む[1]: ActiveSupportが拡張するDateクラス

Posted at

gemを読むとは

Rubyの実装力を上げる手法のひとつとして、質の高いソースコードを理解することが挙げられます。
広く使われているgemは質の高い製品そのものであるため、gemを読んでRubyレベルを上げようという魂胆です。

ActiveSupport

今回はActiveSupportの挙動を理解するために、Rails 7.2のソースコードを読みたいと思います。

READMEを翻訳すると、下記のように記載されています。

Active Supportは、Railsフレームワークにとって有用であると見なされたユーティリティクラスや標準ライブラリの拡張機能のコレクションです。これらの追加機能は、このパッケージ内に収められているため、Rails以外のRubyプロジェクトでも必要に応じてロードすることができます。

つまり、ActiveSupportはRailsをRailsたらしめるライブラリであると言えそうです。

Dateクラス

ActiveSupportは下記にてRubyの標準クラスであるDateにメソッドを追加・拡張しています。

まずはこのクラスに対して実装されているメソッドを見てみます。

blank?, present? メソッド

Railsではほとんどのクラスでblank?present?メソッドを使用することが可能です。
Dateクラスもその一つであり、これらのメソッドを使用することができます。

[1] pry(main)> Date.today.blank?
=> false
[2] pry(main)> Date.today.present?
=> true

これらは、irb(生のruby)ではNoMethodErrorになってしまいます。

irb(main):001> require 'date'
=> true
irb(main):002> Date.today
=> #<Date: 2025-02-21 ((2460728j,0s,0n),+0s,2299161j)>
irb(main):003> Date.today.blank?
(irb):3:in `<main>': undefined method `blank?' for #<Date: 2025-02-21 ((2460728j,0s,0n),+0s,2299161j)> (NoMethodError)
        from /usr/local/rvm/gems/default/gems/irb-1.10.0/exe/irb:9:in `<top (required)>'
        from /usr/local/rvm/gems/default/bin/irb:25:in `load'
        from /usr/local/rvm/gems/default/bin/irb:25:in `<main>'
irb(main):004> Date.today.present?
(irb):4:in `<main>': undefined method `present?' for #<Date: 2025-02-21 ((2460728j,0s,0n),+0s,2299161j)> (NoMethodError)
        from /usr/local/rvm/gems/default/gems/irb-1.10.0/exe/irb:9:in `<top (required)>'
        from /usr/local/rvm/gems/default/bin/irb:25:in `load'
        from /usr/local/rvm/gems/default/bin/irb:25:in `<main>'

これはblank.rbにて実装されています。

それぞれのメソッドが呼ばれたときにtrue, falseを返すように実装されています。

current, tomorrow, yesterday メソッド

実装箇所: https://github.com/rails/rails/blob/7-2-stable/activesupport/lib/active_support/core_ext/date/calculations.rb#L37-L51

Time.zoneやRailsアプリケーション内でconfig.time_zoneが設定されている場合、Date.currentは Time.zone.today を基準に日付を取得します。
もしTime.zoneが設定されていない場合、Date.currentはシステムのローカルタイム(Date.today)を基準に動作します。

ということは、Railsで本日を取得するときはDate.currentを用いるのが安全であると考えることができます。

Date.current.tomorrowは、「Date.currentの翌日」を表します。
ActiveSupportにより、RubyのDateクラスにtomorrowメソッドが追加され、Dateオブジェクトから直接tomorrowを呼び出すことが可能になります。
この時の「Date.current」の挙動は、上で説明したように、タイムゾーン(Time.zone)設定に依存します。

Date.current.yesterdayも同様で、「Date.currentの前日」を表します

beginning_of_day

実装箇所: https://github.com/rails/rails/blob/7-2-stable/activesupport/lib/active_support/core_ext/date/calculations.rb#L67-L72

Dateオブジェクトに対してbeginning_of_dayメソッドを実行すると、その日の始まりの時間(AM 0:00)を取得することができます。

pry(main)> Date.current.beginning_of_day
=> Sun, 23 Feb 2025 00:00:00.000000000 JST +09:00

このメソッドはin_time_zoneを返します。
beginning_of_dayメソッドが実装されているcalculations.rbはactive_support/core_ext/date_and_time/calculationsをrequireしており、このファイル内でin_time_zoneが定義されています。

in_time_zone内ではtime定数をacts_like?(:time)の真偽で判断しています。
acts_like?(duck)active_support/core_ext/object
/acts_like.rb
で定義されており、レシーバのクラスが引数に入れたクラスと同じ振る舞いをするか確認するメソッドです。

pry(main)> Date.current.acts_like?(:date)
=> true
pry(main)> Date.current.acts_like?(:time)
=> false

in_time_zoneの実装箇所ではacts_like?メソッドはレシーバが省略されています。
省略されたレシーバはselfであり、そのメソッドを呼びだしたオブジェクト自身となります。
今回はDateオブジェクトから呼び出されておりTimeクラスではないため、timeにはnilが代入されます。

time_zoneはtrueを返すので、in_time_zonetime_with_zoneを返します。
さらにif文で、timeはnilでfalsyなので、最終的にin_time_zoneActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))を返すことになります。
これはTimeWithZoneに書かれている通り、任意のタイムゾーンの時間を表現できる時間に似たクラスです。

以上から、beginning_of_dayはレシーバのタイムゾーンを考慮したその日の始まりの時間を返すことができます。

midnight, at_midnight, at_beginning_of_daybeggining_of_dayのエイリアスとして定義されているので、これらは同じふるまいをします。

pry(main)> Date.current.midnight == Date.current.beginning_of_day
=> true

middle_of_day, end_of_day メソッド

middle_of_dayは、レシーバの正午を返すメソッドです。

pry(main)> Date.current
=> Tue, 25 Feb 2025
pry(main)> Date.current.middle_of_day
=> Tue, 25 Feb 2025 12:00:00.000000000 JST +09:00

同様にend_of_dayはレシーバの23:59:59.999...を返します。

middle_of_dayend_of_day定義箇所を見ると、in_time_zoneに対してそれぞれ同名のメソッドを呼び出しています。

ここで、in_time_zoneに対して呼び出しているmiddle_of_dayやend_of_dayはどこで定義されているのでしょうか?
まずはin_time_zoneの返り値のクラスを見てみます。

pry(main)> date = Date.current.in_time_zone
=> Sun, 23 Feb 2025 00:00:00.000000000 UTC +00:00
pry(main)> date.class
=> ActiveSupport::TimeWithZone

in_time_zoneの結果はTimeWithZoneクラスですが、、TimeWithZoneクラスにmiddle_of_dayメソッドは定義されていません。

そこで、TimeWithZoneクラスの説明文を見てみると以下のように書かれています。

= Active Support \Time With Zone

任意のタイムゾーンで時刻を表現できる、Timeに似たクラスです。
標準のRubyのTimeインスタンスはUTCとシステムのENV['TZ']で設定されたタイムゾーンに制限されているため、このクラスが必要になります。

通常、+new+を使って直接TimeWithZoneインスタンスを作成する必要はありません。
代わりに、TimeZoneインスタンスのメソッドである+local+、+parse+、+at+、+now+を使うか、
TimeやDateTimeインスタンスの+in_time_zone+を使ってください。

(中略)

TimeWithZoneインスタンスは、RubyのTimeインスタンスと同じAPIを実装しているため、
TimeインスタンスとTimeWithZoneインスタンスは相互に置き換えて利用できます。

よって、middle_in_dayの実装箇所はTimeクラスを見れば分かりそうです。

本当はTimeWithZoneオブジェクトがTimeクラスから移譲されているっぽいのですが、深堀りは別の機会にします。

Time#middle_of_dayは以下のように実装されていました。

# Returns a new Time representing the middle of the day (12:00)
def middle_of_day
  change(hour: 12)
end

def change(options)
  new_year   = options.fetch(:year, year)
  new_month  = options.fetch(:month, month)
  new_day    = options.fetch(:day, day)
  new_hour   = options.fetch(:hour, hour)
  new_min    = options.fetch(:min, options[:hour] ? 0 : min)
  new_sec    = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec)
  new_offset = options.fetch(:offset, nil)
  ...

これによって、レシーバの時間を12:00:00に変更することができます。

同様に、Time#end_of_dayはレシーバの時間を23:59:59.999999999に変更しています。

まとめ

今回はRailsのActiveSupportがRubyの標準クラスであるDateクラスを拡張する様子を深堀りしました。
同様に、ActiveSupportはactivesupport/lib/active_support/core_ext配下で標準クラスの拡張をしていると考えられます。

参考

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?