RailsのActiveSupportのin_time_zoneメソッドの処理の中身を覗いてみた

こんにちは!kossyです!




さて、今回はRailsActiveSupportのメソッドであるin_time_zoneメソッドの処理の中身を見てみたので、
ブログに残してみたいと思います。



環境

Ruby 2.6.6
Rails 6.0.3.4
Docker for Mac




メソッドの定義位置を確認

まずはsource_locationでメソッドの定義位置を確認しましょう。

Date.today.method(:in_time_zone).source_location
=> ["/usr/local/bundle/gems/activesupport-6.0.3.4/lib/active_support/core_ext/date_and_time/zones.rb", 20]

zones.rbの20行目に定義されていることがわかりました。

githubRailsソースコードを見に行ってみましょう。

github.com

コメントアウト部分を訳してみます。

ゾーンが指定されている場合、またはTime.zone_defaultが設定されている場合は、同時時間をTime.zoneで返します。 それ以外の場合は、現在の時刻を返します。

Time.zone = 'Hawaii' # => 'Hawaii'

Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00

Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00


このメソッドは、オペレーティングシステムタイムゾーンの代わりにローカルゾーンとしてTime.zoneを使用することを除いて、Time#localtimeに似ています。

TimeZoneを引数として識別するTimeZoneインスタンスまたは文字列を渡すこともできます。変換は、Time.zoneではなくそのゾーンに基づいて行われます。

ソースコード部分はこんな感じ。

module DateAndTime
  module Zones

    def in_time_zone(zone = ::Time.zone)
      time_zone = ::Time.find_zone! zone
      time = acts_like?(:time) ? self : nil

      if time_zone
        time_with_zone(time, time_zone)
      else
        time || to_time
      end
    end

    private
      def time_with_zone(time, zone)
        if time
          ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
        else
          ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
        end
      end
  end
end

見たことないメソッドもあるので、コンソールで実行しながら動作を確認してみます。

Time.zone

$ Time.zone
=> #<ActiveSupport::TimeZone:0x0000556b8a3a7488 @name="Tokyo", @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>, @utc_offset=nil>

$ Time.zone.methods
=> [:<=>, :to_s, :tzinfo, :strptime, :=~, :tomorrow, :yesterday, :name, :period_for_local, :period_for_utc, :periods_for_local, :local_to_utc, :utc_to_local, :rfc3339, :at, :parse, :now, :iso8601, :local, ...]

ActiveSupport::TimeZoneインスタンスを返すメソッドのようです。
application.rbでtimezoneをTokyoにしているので、name属性にTokyoが指定されていました。

in_time_zoneの引数は、zoneが与えられていればそのzoneを使い、与えられてなければTime.zoneの返り値が使われていました。

Time.find_zone!

$ zone = Time.zone

$ Time.find_zone! zone
=> #<ActiveSupport::TimeZone:0x0000556b8a3a7488 @name="Tokyo", @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>, @utc_offset=nil>

github.com

Time.find_zone!は引数で与えられてzone情報を元にしたActiveSupport::TimeZoneクラスのインスタンスを返すメソッドでした。

acts_like?

railsguides.jp

メソッドの中身は以下でした。
レシーバが引数のクラスのように振る舞うかどうかを検証しているようです。

  def acts_like?(duck)
    case duck
    when :time
      respond_to? :acts_like_time?
    when :date
      respond_to? :acts_like_date?
    when :string
      respond_to? :acts_like_string?
    else
      respond_to? :"acts_like_#{duck}?"
    end
  end

in_time_zoneにおいては、timeクラスのような振る舞いをするかどうかを検証し、trueならselfを、falseならnilを返すようです。

time_zoneが存在すればprivateメソッドであるtime_with_zoneを呼んでいますね。

    private
      def time_with_zone(time, zone)
        if time
          ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
        else
          ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
        end
      end

コンソールで試してみます。

$ zone = Time.zone

$ time = Time.now
=> 2021-03-06 11:45:37 +0000

$ ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
=> Sat, 06 Mar 2021 20:45:37 JST +09:00

Time.zone.nowした時と同様の返り値が返りました。

Time.zone.now.class
=> ActiveSupport::TimeWithZone
ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone).class
=> ActiveSupport::TimeWithZone

どちらもActiveSupport::TimeWithZoneクラスのインスタンスが返っていますね。

ActiveSupport::TimeWithZoneクラスの中身は以下に定義してあります。

github.com


ユースケース

to_timeとin_time_zoneがよく比較対象として上がるようです。

to_timeが環境のタイムゾーンを基に値を算出するのに対し、in_time_zoneはRailsタイムゾーンを参照してくれるので、
環境によって際が生まれずにブレが無くなるといったメリットがあります。

shinkufencer.hateblo.jp




勉強になりました。



大いに参考にさせていただいたサイト

この場を借りて御礼を申し上げます。

Active Support コア拡張機能 - Railsガイド
rails/time_with_zone.rb at 914caca2d31bd753f47f9168f2a375921d9e91cc · rails/rails · GitHub