小ネタ : 開発環境でRailsアプリの日付を変更する
はじめに
開発環境で特定の日付に移動したい時ってありませんか?
私は チーム開発1のプラクティスを行っていて、「祝日の場合ダッシュボードの表示を変更したい」というIssueに取り組みました。
実装を行って祝日の表示確認を行う際、アプリ内の日付を祝日に変更する必要に迫られたのです。
以下の方法で開発環境の日付を一時的に変更することが可能ですので、今回ご紹介します。
変更方法
config/environments/development.rb
に以下を追加して、アプリを再起動する。
require "active_support/core_ext/integer/time" # 追加 require "active_support/testing/time_helpers" # 追加 include ActiveSupport::Testing::TimeHelpers # 追加 Rails.application.configure do config.after_initialize do travel_to Time.zone.parse('移動したい日付') # 追加 end end
試しに今日の日付を表示する簡単なアプリを作成して、アプリ内の日付が変更されているか確認します。
祝日であるかどうかの確認のために、holiday-jpというGemを利用しています。
(app/views/home/index.slim)
h2 今日の日付 p | 今日の日付は"#{l(Date.current, format: :long)}"です。 p | #{HolidayJp.holiday?(Date.current) ? '今日は祝日です!🥳' : '今日は祝日ではありません🫠'} h2 現在時刻 p | 現在時刻は"#{l(Time.current)}"です。
普通にアプリを起動した場合(日付は9/18日)。
アプリ内の日付を変更した場合(日付を9/25に変更)。
日付が変わっていることが分かります!
何を行っているの?
日付の変更を行うには、travel_to
メソッドを利用します。
通常travel_to
はテスト内でしか使用することができないため、travel_to
を使う下準備として次のことを行います。
travel_to
が定義してあるファイル(active_support/testing/time_helpers)をrequireする- モジュール(ActiveSupport::Testing::TimeHelpers)をincludeする
そして、config.after_initialize
ブロック内でtravel_to
を実行し、変更したい日付を渡します。
config.after_initialize
ブロック内に記述されたコードは、Railsによってアプリケーションの初期化が完了した後に実行されます。
Rails アプリケーションを設定する - Railsガイド
travel_to
は、Time.now
Date.today
DateTime.now
を引数で与えられた時間または日付を返すようにスタブ化することで、現在の時刻を特定の時刻に変更することができます。
スタブはテスト終了時に削除されます。
ActiveSupport::Testing::TimeHelpers
Time.currentとDate.currentについて
RailsAPIのtravel_to
の頁に、次のような記述がありました。
Dates are taken as their timestamp at the beginning of the day in the application time zone. Time.current returns said timestamp, and Time.now its equivalent in the system time zone. Similarly, Date.current returns a date equal to the argument, and Date.today the date according to Time.now, which may be different. (Note that you rarely want to deal with Time.now, or Date.today, in order to honor the application time zone please always use Time.current and Date.current.)
簡単にまとめると、「Time.current
とDate.current
はアプリケーションのタイムゾーンに応じた時刻と日付を返し、Time.now
とDate,today
はシステムのタイムゾーンに応じた時刻と日付を返すため、アプリ内のタイムゾーンを尊重したい場合はTime.current
とDate.current
を使いましょう」ということが書かれています。
では、このTime.current
とDate.current
という2つのメソッドはどのように現在時刻や日付を返しているのでしょうか?
実装を見てみようと思います。
Time.current
Time.current
は次のようになっています。
def current ::Time.zone ? ::Time.zone.now : ::Time.now end
Time.zone
にはタイムゾーン情報が格納されており、これが存在する場合はTime.zone.now
を、存在しない場合はTime.now
を返しています。
config.time_zone
にタイムゾーンを設定すると、Time.zone
は次のようになります。
irb(main):001:0> Time.zone => #<ActiveSupport::TimeZone:0x000000010eab9618 @name="Tokyo", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>>
では、Time.zone.now
を確認しましょう。
def now time_now.utc.in_time_zone(self) end
最初にtime_now
メソッドが呼ばれています。
このメソッドはただTime.now
を読んでいるだけです。
def time_now Time.now end
次のutc
メソッドですが、おそらくRubyの組み込みライブラリであるTime
クラスのメソッドが呼ばれているのではないかと思います。
https://docs.ruby-lang.org/ja/latest/method/Time/i/gmtime.html
最後にin_time_zone
メソッドが呼ばれています。
タイムゾーンが存在する場合はActiveSupport::TimeWithZone
のインスタンスを作成していることが分かります。
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
コンソール上で確認すると、確かにActiveSupport::TimeWithZone
のインスタンスが返っていますね。
time_zone
属性にタイムゾーンの情報も格納されていることもわかります。
irb(main):004:0> Time.current => Mon, 18 Sep 2023 14:41:00.422901000 JST +09:00 irb(main):005:0> Time.current.class => ActiveSupport::TimeWithZone irb(main):006:0> Time.current.time_zone => #<ActiveSupport::TimeZone:0x000000010e90e908 @name="Tokyo", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>>
Time.current
の実装を簡単ですが追ってみました。
途中でTime.now
メソッドを読んでおり、travel_to
はTime.now
をスタブ化するのでTime.current
の時間も変わるということが分かりましたね。
Date.current
ではDate.current
の方も見てみましょう。
Date.current
はTime.current
と同じく、タイムゾーンが設定されていればTime.zone.today
を、そうでなければDate.today
を返します。
def current ::Time.zone ? ::Time.zone.today : ::Date.today end
では、Time.zone.today
はどうなっているでしょうか。
def today tzinfo.now.to_date end
tzinfo
が呼ばれていますね。
ActiveSupport::TimeZone
が初期化される時に@tzinfo
に値が格納されます。
attr_reader :name attr_reader :tzinfo def initialize(name, utc_offset = nil, tzinfo = nil) @name = name @utc_offset = utc_offset @tzinfo = tzinfo || TimeZone.find_tzinfo(name) end
TimeZone.find_tzinfo
内ではTZInfo::Timezone.get
が呼ばれています。
def find_tzinfo(name) TZInfo::Timezone.get(MAPPING[name] || name) end
TZInfo::Timezone.get
はtzinfoというGemにより提供されるもので、個別のタイムゾーンを表すTZInfo::Timezone
のインスタンスを得ることができます。
A TZInfo::Timezone instance representing an individual time zone can be obtained with TZInfo::Timezone.get
https://github.com/tzinfo/tzinfo#example-usage
Time.zone.today
内で呼ばれるtzinfo
は、ActiveSupport::TimeZone
初期化時に設定されたTZInfo::Timezone
のインスタンスであることが分かりました。
tzinfo.now
は次のようになっています。
Time.now
を渡してto_local
メソッドを呼び出していますね。
def now to_local(Time.now) end def to_local(time) raise ArgumentError, 'time must be specified' unless time Timestamp.for(time) do |ts| TimestampWithOffset.set_timezone_offset(ts, period_for(ts).offset) end end
最後にto_date
メソッドを呼び出し、日付の文字列が返ってくるようになっています。
次のサンプルコードを実行してわかる通り、タイムゾーンに合わせた現在日時を取得していることが分かりますね。
irb(main):001:0> tz = TZInfo::Timezone.get('America/New_York') => #<TZInfo::DataTimezone: America/New_York> irb(main):002:0> tz.now => 2023-09-18 02:25:04.614519 -0400 irb(main):003:0> tz.now.to_date => Mon, 18 Sep 2023 irb(main):004:0> tz_japan = TZInfo::Timezone.get('Asia/Tokyo') => #<TZInfo::DataTimezone: Asia/Tokyo> irb(main):005:0> tz_japan.now => 2023-09-18 15:24:17.173699 +0900 irb(main):006:0> tz_japan.now.to_date => Mon, 18 Sep 2023
Date.current
の実装も簡単ですが追ってみました。
Date.current
も途中でTime.now
を読んでおり、travel_to
の影響を受けることが分かりました。
終わりに
Railsアプリケーションの開発環境で日付を変更する方法と、Time.current
とDate.current
について書きました。
Rails内のコードを読むのは難しく完璧に理解はできませんでしたが、いい勉強になりました!
また気になるコードがあれば、コードリーディングに挑戦してみたいと思います💪
参考文献
RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い - Qiita
- フィヨルドブートキャンプの終盤にあるプラクティスで、フィヨルドブートキャンプアプリ内で振られたIssueの開発を行う、ということを行います。↩