LoginSignup
3

More than 3 years have passed since last update.

posted at

updated at

ActiveModelのattribute API(datetime)に文字列で時刻を渡したとき、タイムゾーンは保持されない

TL; DR

  • ActiveModel の attribute API で datetime 型を指定して String を渡した場合、 UTCかシステムタイムゾーンで返却される

はじめに

この記事の情報は、最近ソースリーディングを行って知った挙動をまとめています。内容に不備があればコメントまたは編集リクエストにてご指摘をお願いします。
確認バージョン:
ruby (2.5.1)
activemodel (5.2.2)
activesupport (5.2.2)

時刻をStringで渡したときの挙動を見る

print_date.rb
# 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.currentTime.zoneが設定されているときは Time.zone.nowを出力し、設定されていないときは Time.nowつまり JST(システムタイムゾーン)で出力します。
今回はTime.zoneを設定していないのでシステムタイムゾーン(JST)で出力されています。

一方、JSTのはずの Time.current.to_s を渡した foo.created_at は UTC で出力されています。
これは、 ActiveModel が文字列をキャストする際の処理に起因しています。

ActiveModelのソースを読んでみる

rails/activemodel/lib/active_model/type/helpers/time_value.rb
# 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 の設定を見ています。

rails/activemodel/lib/active_model/type/helpers/time_value.rb
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オブジェクトで渡しましょう。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
3