TimeZone を推測する
Rails アプリケーションを作ったら、初手で config/application.rb
に
config.time_zone = "Asia/Tokyo"
って書き込むと思う。
これを入れることで
- DB には UTC で保存する
- アプリケーション上で DB からレコードを取り出すと JST に勝手に変換される
-
Time.current
も JST になる
となって非常に扱いやすくなる。
参考: Railsと周辺のTimeZone設定を整理する (active_record.default_timezoneの罠) - Qiita
ここで、「 config.time_zone
毎回指定するの面倒じゃない?」って疑問が浮かんでくる。OS のタイムゾーンを勝手に読んで欲しい。
僕は作っていて Asia/Tokyo
以外は基本考えないので指定したいんだけど、public なリポジトリとして開発するんだったら「 Asia/Tokyo
がハードコードされているのおかしくない?」と思ってしまったのです。
OS のタイムゾーンを取得する
基本的には /etc/localtime
がそれで、zoneinfo への symlink になっているはず。
$ readlink /etc/localtime
/var/db/timezone/zoneinfo/Asia/Tokyo
環境変数 TZ
も影響する。
$ date
2020年 1月31日 金曜日 03時17分08秒 JST
$ TZ=America/Los_Angeles date
2020年 1月30日 木曜日 10時17分10秒 PST
というわけで環境変数で指定されていなければ /etc/localtime
がどこの symlink になっているかを取得すれば良い……?
と思うんだけど、このファイルが確実にある自信は無いし、zoneinfo への symlink になっていてファイル名の文字列を信用できる自信も無い。
Time.now から推測する
自分よりも賢い人が作っているであろう仕組みに乗っかろう= Ruby の中になんかあるでしょ。
$ ruby -e "p Time.now"
2020-01-31 03:17:17.345471 +0900
$ TZ=America/Los_Angeles ruby -e "p Time.now"
2020-01-30 10:17:20.330342 -0800
Time.now
がよしなにやってくれていそうなので、この offset を使おう!
と考える先人ももちろん存在していて、Rails に time:zones:local
という rake タスクがある。
$ rails time:zones:local
* UTC +09:00 *
Osaka
Sapporo
Seoul
Tokyo
Yakutsk
https://github.com/rails/rails/blob/v6.0.2.1/railties/lib/rails/tasks/misc.rake#L41-L51
jan_offset = Time.now.beginning_of_year.utc_offset
jul_offset = Time.now.beginning_of_year.change(month: 7).utc_offset
offset = jan_offset < jul_offset ? jan_offset : jul_offset
夏時間もそれっぽく対応していてナルホド感ありますね。(このコード読むまで夏時間= 7 月みたいなイメージでいて、南半球のことすっかり忘れてたよね)
ところで Osaka
, Sapporo
等は ActiveSupport の拡張であって tz database のタイムゾーン名ではないので、 TZInfo
から正しい timezone 名を取り出そうと思うと
jan_offset = Time.now.beginning_of_year.utc_offset
jul_offset = Time.now.beginning_of_year.change(month: 7).utc_offset
offset = jan_offset < jul_offset ? jan_offset : jul_offset
ActiveSupport::TimeZone.all.detect {|zone| zone.utc_offset == offset }.tzinfo.name
となります。
ActiveSupport が使えないところなら TZInfo::DataTimezone.all_country_zones
から引くんだろうな。
というわけで、
今作っている sinatra application ではこうしました。
zone_name = ENV["TZ"] || begin
# https://github.com/rails/rails/blob/v6.0.2.1/railties/lib/rails/tasks/misc.rake#L46-L48
jan_offset = Time.now.beginning_of_year.utc_offset
jul_offset = Time.now.beginning_of_year.change(month: 7).utc_offset
offset = jan_offset < jul_offset ? jan_offset : jul_offset
ActiveSupport::TimeZone.all.detect {|zone| zone.utc_offset == offset }.tzinfo.name
end
Time.zone_default = Time.find_zone!(zone_name)
ActiveRecord::Base.time_zone_aware_attributes = true
ActiveRecord::Base.default_timezone = :utc