TimeZone を推測する

Rails アプリケーションを作ったら、初手で config/application.rb

config.time_zone = "Asia/Tokyo"

って書き込むと思う。

これを入れることで

となって非常に扱いやすくなる。

参考: 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