Help us understand the problem. What is going on with this article?

RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い

More than 1 year has passed since last update.

はじめに:日時を扱うクラスはTime?それともDateTime?

先日、社内で「RubyやRailsで日時を扱うときってTimeを使うのがいいのか、それともDateTimeがいいのか、どっち?」という議論が起きました。

僕自身はなんとなく、「日時 => 日 + 時 => Date + Time => DateTime!」という短絡的な発想でDateTimeをよく使っていましたが、そもそもTimeとDateTimeの違いを今まで深く考えたことはありませんでした。

そこで、この記事ではTimeとDateTimeをはじめとした日時関連クラスの違いやその使い分け、そしてタイムゾーンの扱いについてまとめてみたいと思います。

対象となる読者

  • TimeクラスとDateTimeクラスの違いがよくわかっていない人
  • Railsで Time.nowTime.current の違いがよくわかっていない人
  • その他、RubyやRailsで理解が曖昧なままに日付や日時を操作しているなあ、と思っている人

対象バージョン

  • Ruby 2.1.5
  • Rails 4.1.8

おことわり

ここに書いた情報は僕がつい最近知った仕様をまとめています。
もしかすると、間違って理解している箇所がところどころにあったりするかもしれません。

また、タイムゾーンの扱い等についても、僕個人はユーザーが世界中にまたがるようなアプリケーションを開発した経験がほとんどないので、経験豊富な方が見ると「それは違うよ」と思うところがあるかもしれません。

内容に不備があればコメントまたは編集リクエストで優しく指摘してやってください m(_ _)m

最初にざっくり結論:「特に理由がなければ Time(RailsならTimeWithZone)を使う」

日時やタイムゾーン周りの話はかなりややこしく、このあとの説明もかなり長くなります。
ボトムアップで説明すると途中で脱落する人が出てきそうなので、最初に結論を簡単に(乱暴に?)まとめておきます。

  • 素のRubyならTimeクラスを使う。
  • RailsならTimeWithZoneクラスを使う。

僕のように「えっ!?今までTimeよりDateTimeの方をよく使ってたんですけど!」という人はこのあとの説明をじっくり読んでみてください。

Part 1. 素のRubyの場合

まずはRailsとは無関係な素のRubyに限定した説明をします。

タイムゾーンの設定

素のRubyの場合、デフォルトのタイムゾーンは以下の2つによって決まります。

  • システムのタイムゾーン
  • 環境変数 ENV['TZ'] の値

環境変数が設定されていればそちらが優先されます。
環境変数が設定されていなければシステムのタイムゾーンが使われます。

有効なタイムゾーンの一覧

ENV['TZ'] で使えるタイムゾーンの一覧は以下のページで確認できます。

http://en.wikipedia.org/wiki/List_of_tz_database_time_zones

ちなみにTZの列が有効なタイムゾーンの名前です。

  • Asia/Tokyo
  • US/Central

なお、 無効なタイムゾーン('Tokyo'や'Hoge'など)が設定されている場合は特にエラーにならず、世界標準時(UTC)がデフォルトになる ので注意してください。(システムのタイムゾーン設定にもなりません)

続いて、日時に関係するRuby標準のクラスを説明していきます。

Timeクラス

http://www.ruby-doc.org/core-2.1.5/Time.html
http://www.ruby-doc.org/stdlib-2.1.5/libdoc/time/rdoc/Time.html

  • 日時を扱える。
  • Time.now で現在日時が取得できる。このとき、環境変数またはシステムに設定されているタイムゾーンで日時が取得される。
  • zoneメソッドでそのインスタンスのタイムゾーンが確認できる。(JSTCSTのような文字列が返ってくる)
  • parseのような一部のメソッドはrequire 'time'が必要。
  • requireが必要なメソッドの一覧はこちら

Dateクラス

http://ruby-doc.org/stdlib-2.1.5/libdoc/date/rdoc/Date.html

  • 日付を扱える。
  • Date.today で現在の日にちが取得できる。このとき、環境変数またはシステムに設定されているタイムゾーンで日にちが取得される。
  • 生成されたインスタンス自体はタイムゾーンを考慮しない。(タイムゾーンの情報は保持されない)
  • 使用する場合は require 'date' が必要。

DateTimeクラス

http://ruby-doc.org/stdlib-2.1.5/libdoc/date/rdoc/DateTime.html

  • 日時を扱える。
  • Dateクラスのサブクラスになっている。
  • DateTime.now で現在日時が取得できる。このとき、環境変数またはシステムに設定されているタイムゾーンで日時が取得される。
  • zoneメソッドでそのインスタンスのUTCからのオフセット値(時差)が確認できる。(+09:00-06:00のような文字列が返ってくる。)
  • 使用する場合は require 'date' が必要。

TimeクラスとDateTimeクラスの違い / オススメはどっち?

TimeクラスもDateTimeクラスも、どちらも日時を扱えるクラスになっています。
かつては 以下のような違いがあったらしいです。(その時代は僕はRubyを使っていませんでしたが)

  • Timeは速い。ただし、UNIX時間に依存するため、2038年問題を持っている。
  • DateTimeはTimeに比べると遅い。ただし、UNIX時間には依存しないので、より柔軟に日時を表現できる。

しかし、バージョン1.9.2以降のRubyではTimeクラスの2038年問題は解決されています。
また、Ruby 1.9.3からはDateTimeクラスもCの実装になっていてパフォーマンスは良くなったそうです。
よって、機能的にはほとんど違いがなくなっています。

ただし、Timeクラスの方が若干高機能なところがあります。
具体的には以下のような点です。(普通はどちらも滅多に使わないと思いますが)

  • Timeクラスはサマータイムを扱える。
  • Timeクラスはうるう秒を扱える。

また、Timeクラスはrequireなしで使えますが、DateTimeクラスは require 'date' しないと使えません。

上記のような理由から、 特別な理由がなければTimeクラスを使う と判断して問題ないでしょう。

他には次のような違いもあるので、インスタンスを作成するときは注意してください。(どちらも引数で具体的なタイムゾーンを指定しなかった場合です)

  • DateTime.new(y, m, d, ...) のようにインスタンスを作るとタイムゾーンがUTCになっている。(システムまたは環境変数のタイムゾーンを考慮しない)
  • Time.new(y, m, d, ...) のようにインスタンスを作るとタイムゾーンがシステムまたは環境変数のものになっている。

2016.9.12追記:Ruby 2.4での不具合修正について

Ruby 2.3まではDateTime#to_timeTime#to_timeを呼び出すと、元のオブジェクトのタイムゾーン情報が実行環境のタイムゾーンに置き換わるという不具合がありました。

# 実行環境がJSTだと、to_timeの結果もJSTになる(Ruby 2.3以前)
DateTime.strptime('2015-11-12 CET', '%Y-%m-%d %Z').to_time.to_s
#=> "2015-11-12 08:00:00 +0900"

Time.new(2005, 2, 21, 10, 11, 12, '+01:00').to_time.to_s
#=> "2005-02-21 18:11:12 +0900" 

Ruby 2.4ではこの不具合が修正され、タイムゾーン情報が保持されるようになっています。

# Ruby 2.4ではto_timeを呼んでもタイムゾーン(ここではCET)が保持される
DateTime.strptime('2015-11-12 CET', '%Y-%m-%d %Z').to_time.to_s
#=> "2015-11-12 00:00:00 +0100"

Time.new(2005, 2, 21, 10, 11, 12, '+01:00').to_time.to_s
#=> "2005-02-21 10:11:12 +0100"

参考:サンプルコードでわかる!Ruby 2.4の新機能と変更点 - Qiita

参考:うるう秒を扱うサンプルコード

2012年6月30日はうるう秒が挿入されたため、23時59分60秒が存在します。

# http://stackoverflow.com/a/21075654/1058763
# Timeはうるう秒が扱える
t = Time.new(2012, 6, 30, 23, 59, 60, 0)
=> 2012-06-30 23:59:60 +0000

# DateTimeはうるう秒を扱えない。(60秒が59秒に変わっている)
dt = t.to_datetime
dt.to_s
=> "2012-06-30T23:59:59+00:00"

うるう秒についてはWikipediaに説明があります。

閏秒 - Wikipedia

参考:サマータイムを扱うサンプルコード

以下はサマータイムを扱うサンプルコードです。

たとえば、中央ヨーロッパ時間(CET)は、3月の最終日曜日午前2時(=夏時間午前3時)から10月の最終日曜日夏時間午前3時(=標準時午前2時)までの間、中央ヨーロッパ夏時間(CEST)になります。

  • CETとUTCの時差は1時間(+01:00)
  • CESTとUTCの時差は2時間(+02:00)
# http://stackoverflow.com/a/21075654/1058763
# 中央ヨーロッパ時間(CET)をデフォルトとする
ENV['TZ'] = 'CET'

# 2012年7月1日のインスタンスを作る
t = Time.local(2012, 7, 1)
=> 2012-07-01 00:00:00 +0200

# タイムゾーンは中央ヨーロッパ夏時間になっている
t.zone
=> "CEST"

# サマータイム(Daylight Saving Time)なのでtrueが返る
t.dst?
=> true

# TimeからDateTimeに変換する
dt = t.to_datetime
dt.to_s
=> "2012-07-01T00:00:00+02:00"

# UTCとの時差は保持しているが、サマータイムかどうかの判別はできない。
dt.zone
=> "+02:00"
dt.dst?
NoMethodError: undefined method `dst?' for #<DateTime:0x007f34ea6c3cb8>

2015.6.8 追記:大昔の日付を扱うときはDateTime?

世界史の教科書に出てくるぐらいの古い日付を扱うときは、国によって暦法が異なる場合があります。
例えば、シェークスピアセルバンテスはどちらも 1616年4月23日 に亡くなったことになっています。
一見同じ日付に思えますが、当時の暦法が異なるために実際はシェークスピアはセルバンテスよりも10日後に亡くなっています。

イギリスのシェークスピアと死亡した日が同じであるとされることが多いが、当時はヨーロッパ大陸とブリテン島とで異なる暦を使用しており、実際には同じ日ではない。これは、1582年にローマ教皇がユリウス暦からグレゴリウス暦へ暦の変更を決定し、大陸のカトリックやプロテスタントの国々が順次変えていったのに対し、当時のイギリスは、カトリック教会の権威が及ばないイギリス国教会が優勢だったために新しいグレゴリウス暦を受け入れることが遅れたからである。

ミゲル・デ・セルバンテス - Wikipedia

こういう場合はDateTimeを使うとこの違いを表現できます。

>> shakespeare = DateTime.iso8601('1616-04-23', Date::ENGLAND)
=> Tue, 23 Apr 1616 00:00:00 +0000
>> cervantes = DateTime.iso8601('1616-04-23', Date::ITALY)
=> Sat, 23 Apr 1616 00:00:00 +0000
>> cervantes == shakespeare
=> false
>> (shakespeare - cervantes).to_i
=> 10

詳しくはこちらの記事を参考にしてみてください。

When should you use DateTime and when should you use Time?

・・・まあ、この違いが重要になるケースは滅多にないでしょうけどね。

Part 2. Railsの場合

続いて、Railsアプリケーションの日時操作やタイムゾーンの説明をしていきます。

タイムゾーンの設定

Railsの場合、システムや環境変数とは別に application.rb にデフォルトのタイムゾーンを設定できます。(確認するときは Time.zone.name

# application.rb
config.time_zone = 'Tokyo'

time_zoneには 'Tokyo' や 'Hawaii' のような都市名でも指定できますし、 'Asia/Tokyo' や 'Pacific/Honolulu' のような正式なタイムゾーン名を指定することもできます。
ただし、'JST' や 'HST' のような短縮形(?)や、'Hoge' のような無効な名前を指定するとエラーが発生してRailsが起動しなくなります。

有効な名前の一覧は以下のページのMAPPING欄を参照してください。(KeyとValueのどちらも使用可能です)

http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html

次に、Railsで登場する日付関連のクラスを説明します。

ActiveSupport::TimeWithZoneクラス

http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html

  • 日時を扱える。
  • Rails独自のクラスで、Timeクラスと完全な互換性がある。ただし、実装としてはTimeクラスを継承して作られているわけではない。(親クラスは Object
  • Timeクラスよりもタイムゾーンを柔軟に扱うことができる。
  • Railsではよくこのクラスが使われている。(created_at1.day.ago の戻り値など)ただし、Timeクラスとほとんど見分けが付かないので、知らないうちに使っていることが多い(はず)。
  • TimeWithZoneクラスのインスタンスはnewではなく、以下のような方法で取得するのが望ましい。
# http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html
Time.zone = 'Eastern Time (US & Canada)'        # => 'Eastern Time (US & Canada)'
Time.zone.local(2007, 2, 10, 15, 30, 45)        # => Sat, 10 Feb 2007 15:30:45 EST -05:00
Time.zone.parse('2007-02-10 15:30:45')          # => Sat, 10 Feb 2007 15:30:45 EST -05:00
Time.zone.at(1170361845)                        # => Sat, 10 Feb 2007 15:30:45 EST -05:00
Time.zone.now                                   # => Sun, 18 May 2008 13:07:55 EDT -04:00
Time.utc(2007, 2, 10, 20, 30, 45).in_time_zone  # => Sat, 10 Feb 2007 15:30:45 EST -05:00

Time, Date, DateTime

http://api.rubyonrails.org/classes/Time.html
http://api.rubyonrails.org/classes/Date.html
http://api.rubyonrails.org/classes/DateTime.html

  • どのクラスもRailsによって大きく拡張されている。
  • 新しいメソッドがたくさん追加されていたり、Ruby標準の挙動をオーバーライドされたりしている。
  • agobeginning_of_day など、クラスをまたがって同じ名前が付けられているメソッドも数多く存在する。(ただし、実装や戻り値の型は別々であることが多い)
  • 上記のAPIドキュメントを読むと、Ruby標準から追加されたりオーバーライドされたりしているメソッドが確認できる。

Railsアプリケーションにおけるタイムゾーンの扱いを整理する

Railsアプリケーションでは「システムまたは環境変数に設定されたタイムゾーン」と「application.rbに設定されたタイムゾーン」の2種類があります。
どちらも同じ設定になっている場合は問題が起きることは少ないですが、設定が異なっていると予期せぬ結果になる場合があります。
ちゃんと設定したつもりでも、環境変数に 'Tokyo' のような無効なタイムゾーンを設定すると黙ってUTC扱いされるので要注意です! (経験者談)

ここでは以下のようなサンプルケースを考えてみます。

  • 環境変数のタイムゾーンは 'Asia/Tokyo'(JST +09:00)
  • application.rb の設定は 'Eastern Time (US & Canada)' (EST -05:00)
  • システム時刻は日本時間の 2015年1月1日 0時00分(ESTでは 2014年12月31日 午前10時00分)

実際はこんな設定にすることはまずないと思いますが、メソッドの挙動を比較しやすいように極端な設定にしてあります。

サンプルコードを実行するための前準備

前準備として、Timecop gemを使ってシステム時刻を偽装します。

# 環境変数上はJST
ENV['TZ'] = 'Asia/Tokyo'
=> Asia/Tokyo

# application.rbはEST
Time.zone.name
=> Eastern Time (US & Canada)

# 日本時間の2015年1月1日 0時00分にシステム日時を固定
time_for_freeze = Time.local(2015, 1, 1)
Timecop.freeze time_for_freeze

また、オブジェクトの内容を確認しやすくするために、独自の l メソッドを用意しました。

def l(object)
  # フォーマット用の関数(ラムダ)
  format = ->(date_or_time) { [I18n.l(date_or_time), date_or_time.try(:zone), date_or_time.class].compact.join(', ') }
  if object.is_a?(Range)
    # Rangeが渡された場合
    str = [object.first, object.last].map{|date_or_time| format.call(date_or_time)}.join('..')
  else
    # Date, Time, DateTimeが渡された場合
    str = format.call(object)
  end
  puts "=> #{str}"
end

TimeまたはTimeWithZoneの場合

それではまず、TimeまたはTimeWithZoneが関連するメソッドの動作を確認していきます。

【ここに注目】
基本的に Time.currentTime.zone.xxxxxx.in_time_zone 等のメソッドを使うと、TimeWithZoneクラスのインスタンスが返却され、application.rbのタイムゾーンが使用されるようです。

Time.nowTime.parse など、Timeクラスから元から存在しているメソッドはRuby標準の挙動と同じく、環境変数のタイムゾーンを使用しています。

# nowは環境変数のタイムゾーンを使う。
l Time.now
=> 2015/01/01 00:00:00, JST, Time

# currentはapplication.rbのタイムゾーンを使う。戻り値もTimeWithZone。
l Time.current
=> 2014/12/31 10:00:00, EST, ActiveSupport::TimeWithZone

# zone.nowはcurrentと同じ。
l Time.zone.now
=> 2014/12/31 10:00:00, EST, ActiveSupport::TimeWithZone

# 数値.day.agoのようなメソッドはapplication.rbのタイムゾーンを使う。
l 0.day.ago
=> 2014/12/31 10:00:00, EST, ActiveSupport::TimeWithZone

# TimeからTimeWithZoneに変換する。タイムゾーンもapplication.rbのタイムゾーンに変わる。
l Time.now.in_time_zone
=> 2014/12/31 10:00:00, EST, ActiveSupport::TimeWithZone

# TimeWithZoneからTimeに変換する。タイムゾーンも環境変数のタイムゾーンに変わる。
l Time.current.to_time
=> 2015/01/01 00:00:00, JST, Time

str = '2015-01-01 00:00:00'

# parseは環境変数のタイムゾーンを使う。
l Time.parse(str)
=> 2015/01/01 00:00:00, JST, Time

# zone.parseはapplication.rbのタイムゾーンを使う。
l Time.zone.parse(str)
=> 2015/01/01 00:00:00, EST, ActiveSupport::TimeWithZone

# 文字列.to_timeは環境変数のタイムゾーンを使う。
l str.to_time
=> 2015/01/01 00:00:00, JST, Time

# 文字列.in_time_zoneはapplication.rbのタイムゾーンを使う。
l str.in_time_zone
=> 2015/01/01 00:00:00, EST, ActiveSupport::TimeWithZone

# UTCからのオフセット(時差)を指定する。
str = '2015-01-01 00:00:00 +00:00'

# parseは環境変数のタイムゾーンに変換する。
l Time.parse(str)
=> 2015/01/01 09:00:00, JST, Time

# zone.parseはapplication.rbのタイムゾーンに変換する。
l Time.zone.parse(str)
=> 2014/12/31 19:00:00, EST, ActiveSupport::TimeWithZone

# 文字列.to_timeはTime.parseと同じ。
l str.to_time
=> 2015/01/01 09:00:00, JST, Time

# 文字列.in_time_zoneはTime.zone.parseと同じ。
l str.in_time_zone
=> 2014/12/31 19:00:00, EST, ActiveSupport::TimeWithZone

# Time.newは環境変数のタイムゾーンを使う。
l Time.new(2015, 1, 1)
=> 2015/01/01 00:00:00, JST, Time

# Time.localはTime.newと同じ。
l Time.local(2015, 1, 1)
=> 2015/01/01 00:00:00, JST, Time

# Time.utcはUTCのタイムゾーンを使う。
l Time.utc(2015, 1, 1)
=> 2015/01/01 00:00:00, UTC, Time

# Time.zone.localはapplication.rbのタイムゾーンを使う。
l Time.zone.local(2015, 1, 1)
=> 2015/01/01 00:00:00, EST, ActiveSupport::TimeWithZone

# Timeのend_of_dayは環境変数のタイムゾーンで23:59:59を返す。
l Time.now.end_of_day
=> 2015/01/01 23:59:59, JST, Time

# TimeWithZoneのend_of_dayはapplication.rbのタイムゾーンで23:59:59を返す。
l Time.current.end_of_day
=> 2014/12/31 23:59:59, EST, ActiveSupport::TimeWithZone

# Timeのsinceは環境変数のタイムゾーンで指定された日時を返す。
l Time.now.since(1.minute)
=> 2015/01/01 00:01:00, JST, Time

# TimeWithZoneのsinceはapplication.rbのタイムゾーンで指定された日時を返す。
l Time.current.since(1.minute)
=> 2014/12/31 10:01:00, EST, ActiveSupport::TimeWithZone

# Timeのall_dayは環境変数のタイムゾーンで00:00:00から23:59:59までのRangeを返す。
l Time.now.all_day
=> 2015/01/01 00:00:00, JST, Time..2015/01/01 23:59:59, JST, Time

# TimeWithZoneのall_dayはapplication.rbのタイムゾーンで00:00:00から23:59:59までのRangeを返す。
l Time.current.all_day
=> 2014/12/31 00:00:00, EST, ActiveSupport::TimeWithZone..2014/12/31 23:59:59, EST, ActiveSupport::TimeWithZone

DateTimeの場合

同様に、DateTimeの場合の挙動を見ていきます。

【ここに注目】
こちらもやはり、DateTime.current を使うとapplication.rbのタイムゾーンが使われます。

parsenew では、環境変数のタイムゾーンでもapplication.rbのタイムゾーンでもなく、UTCのタイムゾーンになる点も注意が必要です。(UTCからのオフセットを指定しない場合)

# nowは環境変数のタイムゾーンを使う
l DateTime.now
=> 2015/01/01 00:00:00, +09:00, DateTime

# currentはapplication.rbのタイムゾーンを使う。
l DateTime.current
=> 2014/12/31 10:00:00, -05:00, DateTime

# TimeからDateTimeに変換する。タイムゾーンは変換前から変わらない。
l Time.now.to_datetime
=> 2015/01/01 00:00:00, +09:00, DateTime

# TimeWithZoneからDateTimeに変換する。タイムゾーンは変換前から変わらない。
l Time.current.to_datetime
=> 2014/12/31 10:00:00, -05:00, DateTime

# DateTimeからTimeに変換する。タイムゾーンは変換前から変わらない。
l DateTime.now.to_time
=> 2015/01/01 00:00:00, JST, Time

# DateTimeからTimeWithZoneに変換する。タイムゾーンはapplication.rbのものに変わる。
l DateTime.now.in_time_zone
=> 2014/12/31 10:00:00, EST, ActiveSupport::TimeWithZone

str = '2015-01-01 00:00:00'

# parseはUTCのタイムゾーンを使う。(環境変数のタイムゾーンを無視する)
l DateTime.parse(str)
=> 2015/01/01 00:00:00, +00:00, DateTime

# 文字列.to_datetimeはparseと同じ。
l str.to_datetime
=> 2015/01/01 00:00:00, +00:00, DateTime

# UTCからのオフセット(時差)を指定する。
str = '2015-01-01 00:00:00 +09:00'

# parseすると文字列中のオフセットが使われる。
l DateTime.parse(str)
=> 2015/01/01 00:00:00, +09:00, DateTime

# 文字列.to_datetimeはparseと同じ。
l str.to_datetime
=> 2015/01/01 00:00:00, +09:00, DateTime

# DateTime.newはUTCのタイムゾーンを使う。(環境変数のタイムゾーンを無視する)
l DateTime.new(2015, 1, 1)
=> 2015/01/01 00:00:00, +00:00, DateTime

# end_of_dayは環境変数のタイムゾーンで23:59:59を返す。戻り値もDateTimeのまま。
l DateTime.now.end_of_day
=> 2015/01/01 23:59:59, +09:00, DateTime

# sinceは環境変数のタイムゾーンで指定された日時を返す。戻り値もDateTimeのまま。
l DateTime.now.since(1.minute)
=> 2015/01/01 00:01:00, +09:00, DateTime

# DateTimeクラスにはall_dayメソッドがない。
# l DateTime.now.all_day

Dateの場合

続いて、Dateの場合を見ていきます。

【ここに注目】
Dateクラスのインスタンスにはタイムゾーン情報が保持されないので、in_time_zoneend_of_day など、日付から日時に変換する場合は思わぬ日付やタイムゾーンに変換される恐れがあります。

# todayは環境変数のタイムゾーンを使う。(ただし、タイムゾーン情報は保持されない)
l Date.today
=> 2015/01/01, Date

# currentはapplication.rbのタイムゾーンを使う。
l Date.current
=> 2014/12/31, Date

# Time.zone.todayはDate.current と同じ
l Time.zone.today
=> 2014/12/31, Date

# tomorrowはapplication.rbのタイムゾーンを使う。
l Date.tomorrow
=> 2015/01/01, Date

# Time.zone.tomorrowはDate.tomorrowと同じ
l Time.zone.tomorrow
=> 2015/01/01, Date

# yesterdayはapplication.rbのタイムゾーンを使う。
l Date.yesterday
=> 2014/12/30, Date

# Time.zone.yesterdayはDate.yesterdayと同じ
l Time.zone.yesterday
=> 2014/12/30, Date

# TimeからDateに変換する。タイムゾーンは変換前と変わらない。
l Time.now.to_date
=> 2015/01/01, Date

# TimeWithZoneからDateに変換する。タイムゾーンは変換前と変わらない。
l Time.current.to_date
=> 2014/12/31, Date

# DateTimeからDateに変換する。タイムゾーンは変換前と変わらない。
l DateTime.now.to_date
=> 2015/01/01, Date

# DateからTimeに変換する。環境変数のタイムゾーンにおける、元の日付 + 00:00:00になる。
l Date.today.to_time
=> 2015/01/01 00:00:00, JST, Time

# DateからTimeWithZoneに変換する。application.rbのタイムゾーンにおける、元の日付 + 00:00:00になる。
l Date.today.in_time_zone
=> 2015/01/01 00:00:00, EST, ActiveSupport::TimeWithZone

# DateからDateTimeに変換する。UTCのタイムゾーンにおける、元の日付 + 00:00:00になる。(環境変数のタイムゾーンを無視する)
l Date.today.to_datetime
=> 2015/01/01 00:00:00, +00:00, DateTime

str = '2015-01-01'

# 文字列をparseしてDateにする。(この場合、タイムゾーン情報は関係しない)
l Date.parse(str)
=> 2015/01/01, Date

# 文字列.to_dateはparseと同じ。
l str.to_date
=> 2015/01/01, Date

# UTCからのオフセット(時差)を指定する。
str = '2014-12-31 10:00:00 -05:00'

# parseすると日付の部分だけが使われる。(環境変数のタイムゾーンに変換したりしない)
l Date.parse(str)
=> 2014/12/31, Date

# 文字列.to_dateはparseと同じ。
l str.to_date
=> 2014/12/31, Date

# Date.newでインスタンスを作成する。(この場合、タイムゾーン情報は関係しない)
l Date.new(2015, 1, 1)
=> 2015/01/01, Date

# today.end_of_dayはapplication.rbのタイムゾーンにおける、元の日付 + 23:59:59を返す。
l Date.today.end_of_day
=> 2015/01/01 23:59:59, EST, ActiveSupport::TimeWithZone

# current.end_of_dayもapplication.rbのタイムゾーンにおける、元の日付 + 23:59:59を返す。
l Date.current.end_of_day
=> 2014/12/31 23:59:59, EST, ActiveSupport::TimeWithZone

# today.sinceはapplication.rbのタイムゾーンにおける、(元の日付 + 00:00:00) に指定された時間を加算して返す。
l Date.today.since(1.minute)
=> 2015/01/01 00:01:00, EST, ActiveSupport::TimeWithZone

# current.sinceもapplication.rbのタイムゾーンにおける、(元の日付 + 00:00:00) に指定された時間を加算して返す。
l Date.current.since(1.minute)
=> 2014/12/31 00:01:00, EST, ActiveSupport::TimeWithZone

# Dateクラスにはall_dayメソッドがない。
# l Date.today.all_day

Modelに格納される日時

最後に、Modelに格納される日時を確認します。

【ここに注目】
created_at はTimeWithZoneになるため、application.rbのタイムゾーンになります。

データベース上ではUTCの日時として保存されますが、これは設定変更(default_timezone)でシステムまたは環境変数のタイムゾーンを使って保存することもできます。
なお、データベースに格納される日時のタイムゾーンについては、@joker1007さんのQiita記事で詳しく説明されています。

Railsと周辺のTimeZone設定を整理する (active_record.default_timezoneの罠)

blog = Blog.create!(title: 'title', content: 'content')

# created_atはapplication.rbのタイムゾーンを使う。
l blog.created_at
=> 2014/12/31 10:00:00, EST, ActiveSupport::TimeWithZone

# DB上のタイムゾーンはUTCになっている。
l blog.created_at_before_type_cast
=> 2014/12/31 15:00:00, UTC, Time

# UTCで保存されるのはRailsの設定がそうなっているため。
puts ActiveRecord::Base.default_timezone
=> utc

Railsで使う日時関連クラスのまとめ / オススメはどのクラス?

ここまで説明してきたように、Railsに登場する日時関連クラスの種類や挙動は非常にややこしいです。

とはいえ、システムまたは環境変数のタイムゾーンとapplication.rbのタイムゾーンが同じ('Asia/Tokyo')で、なおかつ日本人しか使わないRailsアプリケーションであれば、致命的な落とし穴に遭遇する機会は少ないと思います。(日本時間しか登場しないため)

逆に言うと、致命的な落とし穴に遭遇しなかったために、Time(TimeWithZone)やDateTimeの違いに気付く人が少ない、と考えることもできます。(僕のことです、ハイ)

とりあえず、Railsの実装を見ていると日時関連の処理は TimeWithZone を積極的に使おうとしているように思います。
TimeWithZone はタイムゾーンをRuby標準のTimeクラスよりも器用に扱えるので、国際的なwebアプリケーションをターゲットにするのであれば、TimeWithZoneクラスを積極的に使うのは確かに理にかなっています。

なので、我々も 極力 TimeWithZone を使うようにした方が良い 、と考えることができます。

言い換えるなら、Time.nowDateTime.nowよりもTime.current(または Time.zone.now)を使った方が良い、ということです。

もし、開発中のアプリケーションが世界中のユーザーに使われるようになったとしたら、 TimeWithZone を使っていた方が素早く対応できそうです。(経験がないので確信はありませんが)

まとめ

というわけでこの記事ではRubyやRailsで使われる日時関連のクラスやその挙動についてまとめました。

今まではなんとなくDateTimeクラスを使っていましたが、TimeクラスやTimeWithZoneクラスを使った方が良かったんだ、というのは結構衝撃的でした。

僕と同じように「なんとなくDateTimeクラスを使ってた」とか「Time.nowTime.current の違いを意識したことがなかった」という人の参考になれば幸いです。

参考文献

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした