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 メソッド
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
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_zone
はtime_with_zone
を返します。
さらにif文で、time
はnilでfalsyなので、最終的にin_time_zone
はActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
を返すことになります。
これはTimeWithZoneに書かれている通り、任意のタイムゾーンの時間を表現できる時間に似たクラスです。
以上から、beginning_of_day
はレシーバのタイムゾーンを考慮したその日の始まりの時間を返すことができます。
midnight
, at_midnight
, at_beginning_of_day
はbeggining_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_day
やend_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
配下で標準クラスの拡張をしていると考えられます。
参考