RailsではTime.parseではなくTime.zone.parseを使おう
結論
Time.parse
は、実行環境に依存するタイムゾーンでの日時が生成される。
Time.zone.parse
は、実行環境によらずRailsの config.time_zone
に設定されたタイムゾーンでの日時が生成される。
→ 特別な理由がなければ Time.zone.parse
を利用した方が安全
発生した問題
とあるRailsアプリケーションで、 config/application.rb
には config.time_zone = 'Asia/Tokyo'
と指定されています。
あるロジックで、日時文字列から時間オブジェクトを生成する際に、以下のように Time.parse
が使われていました。
Time.parse('2022-05-15 00:00:00')
# => 2022-05-15 00:00:00 +0000
上記コードは日本時間で '2022-05-15 00:00:00'
の日時を生成することを意図していますが、実際にはUTCタイムゾーンの日時が生成されていて、日本時間で '2022-05-15 09:00:00'
になってしまっていました。
問題の原因
Time.parse
は、Ruby標準のTimeクラスのメソッドで、システムのタイムゾーン、または環境変数の TZ
に指定されたタイムゾーンで日時を生成します。
上記例のプロジェクトではDockerを利用していて、 TZ
が指定されておらず、デフォルトのUTCが使われていたことが分かりました。
問題の対策
対策1. Time.zone.parseを使う
Time.zone.parse
は ActiveSupport::TimeWithZone
クラスのメソッドで、 application.rb
等の config.time_zone
に設定されたタイムゾーンで日時が生成されます。
# config.time_zone = 'Asia/Tokyo' と指定している
Time.zone.name # => 'Asia/Tokyo'
# 日本時間での日付が生成される
Time.zone.parse('2022-05-15 00:00:00')
# => 2022-05-15 00:00:00 +0900
基本的にはこのように Time.zone.parse
を使うように統一するだけで、対策としては十分だと思います。
対策2. 日付文字列にタイムゾーンを含める
日時に変換したい文字列を、タイムゾーンまで含めた形式で渡すと、 Time.parse
でも Time.zone.parse
でも必ず指定されたタイムゾーンでの日時を生成してくれるようです。
Time.parse('2022-05-15 00:00:00 +09:00')
# => 2022-05-15 00:00:00 +0900
Time.zone.parse('2022-05-15 00:00:00 +09:00')
# => 2022-05-15 00:00:00 +0900
対策3. 環境変数TZを指定する
Docker等の環境変数に、TZ=Asia/Tokyo
のように config.time_zone
と同じ値を指定しておけば、 Time.parse
でも Time.zone.parse
でも同じ日時が生成されるようになります。
終わりに
RubyとRailsの日時まわりの扱いについては、https://qiita.com/jnchito/items/cae89ee43c30f5d6fa2c の記事が大変参考になります。
記事中の、 RailsならTimeWithZoneクラスを使う。
ということをこれまでなんとなく意識してきましたが、その中でも日時のparse処理に関しては、特に注意が必要だということが今回分かりました。
というのも、 Time.now
と Time.zone.now
であれば、たとえタイムゾーンが異なったとしても、イギリスにおける現在時刻と、日本における現在時刻は、DBに入れれば結局同じ時刻になります。
しかし、Time.parse
と Time.zone.parse
の場合、イギリスにおける2022年1月1日0時と日本における2022年1月1日0時では、DBに入ると9時間の差がある時刻となるからです。
参考
Discussion