RailsでTimeWithZoneを使う

真面目にプログラムをしはじめるとタイムゾーンは非常に面倒です。まず、rubyのTimeオブジェクトはUNIXのシステムをそのまま使っているようなので時刻として

UTC

または

現在のOSのlocaltime

の2つしかとりえません。なのでユーザ毎に異なるタイムゾーンをもつシステムとかを作るときは非常に面倒です。localtimeは/etc/localtimeや 環境変数TZで決まるので例えば現在台北で今何時よ?というときは

ENV['TZ'] = "Asia/Taipei
Time.now

とかやればいいわけですが、ENVはプロセス全体に影響を及ぼしうるので、あまり推奨されません。こういったことを何とかしてくれるライブラリとしてTZinfoというのがあるのですが、 Railsの4あたりからTimeWithZoneとして標準で入っているようです。 これを使うのが簡単でしょう。

Timeを TimeWithZoneに変換するには、次のようにします。

Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
Time.local(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 06:00:00 AKST -09:00

逆ににアラスカの2000年1月1日の0時を求めたいときは、次のようにします。

Time.zone = 'Alaska'
Time.zone.local(2000,1, 1) #=> Sat, 01 Jan 2000 00:00:00 AKST -09:00

Time.zoneに代入していると、クラス変数に代入しているような気がしますが、これはスレッドセーフに作られているようなのでとりあえず代入してしまって問題ないようです。

Time.zoneを設定すると、ActiveRecord等の時間のフィールドはそのtimezoneになるようなのでControllerのフィルタあたりでユーザ毎にTime.zoneを設定してやれば一発で済みそうです。個人的には内部的に時間を格納するときはすべてUTCにしておいて、表示や検索するときに適宜変換するのが混乱がないと思います。Railsの標準設計もそうなっているでしょう。

現在時刻を持ったTimeWithZoneオブジェクトを作るには

Time.zone.now

とすればOKです。TimeWithZoneオブジェクトはTimeオブジェクトとメソッドで互換があるのでActiveSupportのの便利なメソッドが使えます。

夏時間等あるエリアでは、24時間前などを取得しても1日前になる保証はないので、日付の差分を取得したいときは、yesterdayやbeginning_of_yearなどを活用しましょう。

例えばハワイの今年の正月は以下のようにすればUTCで求まります。

Time.zone.now.beginning_of_year.utc

2015年3月29日の2時の一日前の同時刻との差はどうなるでしょうか?

Time.zone="London"
a=Time.zone.local(2015,3,29,2,0,0) # => Sun, 29 Mar 2015 02:00:00 BST +01:00
b=a.yesterday #=> Sat, 28 Mar 2015 02:00:00 GMT +00:00
(a-b)/3600 => 23.0

ロンドンでは2015年3月29日の2時に1時間時間が進みますので前日の同じ時間との差は23時間ということがわかります。

その他、詳しい使い方は

に例があるのでこちらを参考にしてください。

注記)

ruby-2.2からはlocaltimeでもないUTCでもないオフセットを持った時刻を保持できるようです

Time.parse("2015-02-24 16:32:20 +1200") #=> 2015-02-24 16:32:20 +1200