TL; DR
- ActiveModel の attribute API で datetime 型を指定して String を渡した場合、 UTCかシステムタイムゾーンで返却される
はじめに
この記事の情報は、最近ソースリーディングを行って知った挙動をまとめています。内容に不備があればコメントまたは編集リクエストにてご指摘をお願いします。
確認バージョン:
ruby (2.5.1)
activemodel (5.2.2)
activesupport (5.2.2)
時刻をStringで渡したときの挙動を見る
# SystemTimezone:JST
# Time.zone = nil; Time.zone_default = nil
require 'active_support'
require 'active_model'
class Foo
include ActiveModel::Model
include ActiveModel::Attributes
attribute :created_at, :datetime
end
def print_date
foo = Foo.new(created_at: Time.current.to_s)
puts Time.current.to_s
puts foo.created_at
end
print_date
上のスクリプトを実行すると、下記の通り出力されます。
$ bundle exec ruby print_date.rb
2018-12-17 05:40:28 +0900
2018-12-16 20:40:28 UTC
Time.current
は Time.zone
が設定されているときは Time.zone.now
を出力し、設定されていないときは Time.now
つまり JST(システムタイムゾーン)で出力します。
今回はTime.zone
を設定していないのでシステムタイムゾーン(JST)で出力されています。
一方、JSTのはずの Time.current.to_s
を渡した foo.created_at
は UTC で出力されています。
これは、 ActiveModel が文字列をキャストする際の処理に起因しています。
ActiveModelのソースを読んでみる
# ActiveModel::Type::Helpers::TimeValue#new_time:
def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
# Treat 0000-00-00 00:00:00 as nil.
return if year.nil? || (year == 0 && mon == 0 && mday == 0)
if offset
time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
return unless time
time -= offset
is_utc? ? time : time.getlocal
else
::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
end
end
このメソッドでは、Time.zone_default
が nil のとき、Time.utc
で作成されたTimeオブジェクトからオフセット値を差し引いた値を返却します。
上述のスクリプトの場合、Time.zone_default
が nil だったため、 UTC の時刻オブジェクトが返却されていました。
new_time
内の is_utc?
で、Time.zone_default
の設定を見ています。
def is_utc?
::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
end
Time.zone_default
が設定されている場合はtime.getlocal
つまりシステム時刻が適用された値が返却されます。
is_utc?
では nil か UTC がセットされているかを見ているだけなので、システムタイムゾーンを単純に適用したいときは Time.zone_default
を設定すればOKです。
Time.zone = "Tokyo"
Time.zone_default = Time.zone
注意すること
Time.zone_default
に nil 以外の何を設定しても、返却されるのは time.getlocal
(システムタイムゾーン)です。
つまり、Time.zone_default
にシステムタイムゾーンと異なるタイムゾーンを設定していても、それが適用されるわけではありません。
まとめ
attribute API を利用する場合、 datetime 型を指定して String を渡した場合、 UTC あるいはシステムタイムゾーンのどちらかで返却される挙動になっています。
オブジェクトのタイムゾーンを保持したまま処理したい場合、String ではなくTimeオブジェクトで渡しましょう。