⏱️

RailsではTime.parseではなくTime.zone.parseを使おう

2022/05/15に公開

結論

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.parseActiveSupport::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.nowTime.zone.now であれば、たとえタイムゾーンが異なったとしても、イギリスにおける現在時刻と、日本における現在時刻は、DBに入れれば結局同じ時刻になります。

しかし、Time.parseTime.zone.parse の場合、イギリスにおける2022年1月1日0時と日本における2022年1月1日0時では、DBに入ると9時間の差がある時刻となるからです。

参考

https://qiita.com/jnchito/items/cae89ee43c30f5d6fa2c

Discussion