Ruby on Rails

【Rails】日付を扱うクラスのベストプラクティス|2021年版

おはようございます。今日はRubyやRailsでの日付の扱いについて書いていきたいと思います。この記事を読むと日時データを扱う際のベストプラクティスが分かります。
筆者は駆け出しなので間違っていることも多々あるかとは思いますが、分かり次第随時更新していきたいと思います。

タイムゾーンについて

まずは、日付を扱う話をするのに必須なタイムゾーンについてです。
コンピュータの世界では標準をUTC (協定世界時:Coordinated Universal Time)としています。日本時間はJST (日本標準時:Japan Standard Time) で表され、UTCより9時間進んでいます。

素のRubyの場合

素のRubyでは、タイムゾーンは以下によって決まります。環境変数が設定されている場合、そちらが優先されます。

  • OSのタイムゾーン
  • 環境変数ENV[‘TZ’]の値

環境変数が設定されているかどうかはprintenvコマンドで確認ができます。

printenv TZ

Railsの場合

Railsでは、上記とは別にapplication.rbでデフォルトのタイムゾーンを設定できます。

config.time_zone = 'Tokyo'

それでは、次は日付を扱うクラスについて見ていきます。

クラスの種類

純粋なRubyには3種類のクラスが存在します。

  • Timeクラス
  • Dateクラス
  • DateTimeクラス

Railsでは、上記クラスが拡張されていて様々なメソッドが追加されます。また、上記3つとは別にActive::Support::TimeWithZoneクラスが使えるようになります。

Timeクラスについて

Timeクラスで使えるメソッドを見ていきます。

純粋なRubyのTimeクラス

Time.now
=> 2021-03-22 07:51:16 +0900

Time.new(2021, 3, 22, 7, 53, 0)
=> 2021-03-22 07:53:00 +0900

Time.new(2021,1,1)
=> 2021-01-01 00:00:00 +090

Time.now.zone
=> "JST"

time = Time.now
time.year
=> 2021

time.month
=> 3

time.day
=> 22

time.min # minuteじゃなくminでした
=> 34

time.sec
=> 59

time.monday?
=> true

time.strftime("%Y/%m/%d")
=> "2021/03/22"

Railsによって拡張されたTimeクラスのメソッド

time = Time.now
time + 10
=> 2021-03-22 09:33:59 +0900

time + 10.second
=> 2021-03-22 09:33:59 +0900

time + 10.seconds
=> 2021-03-22 09:33:59 +0900

time + 10.minute
=> 2021-03-22 09:43:49 +0900

time + 10.hours
=> 2021-03-22 19:33:49 +0900

time + 10.days
=> 2021-04-01 09:33:49 +0900

time + 1.month
=> 2021-04-22 09:33:49 +0900

time + 3.years
=> 2024-03-22 09:33:49 +0900

time.yesterday
=> 2021-03-21 08:54:35 +0900

time.tomorrow
=> 2021-03-23 09:32:11 +0900

time.next_month
=> 2021-04-22 09:33:49 +0900

time.prev_month
=> 2021-02-22 09:33:49 +0900

time.next_year
=> 2022-03-22 09:33:49 +0900

time.prev_year
=> 2020-03-22 09:33:49 +0900

time.since(2.months)
=> 2021-05-22 09:33:49 +0900

time.ago(2.months)
=> 2021-01-22 09:33:49 +0900

time.since(2.years)
=> 2023-03-22 09:33:49 +0900

time.ago(2.years)
=> 2019-03-22 09:33:49 +0900

time.beginning_of_month
=> 2021-03-01 00:00:00 +0900

time.end_of_month
=> 2021-03-31 23:59:59 +0900

time.beginning_of_year
=> 2021-01-01 00:00:00 +0900

time.end_of_year
=> 2021-12-31 23:59:59 +0900

time.next_occurring(:wednesday)
=> 2021-03-24 09:33:49 +0900

time.next_occurring(:sunday)
=> 2021-03-28 09:33:49 +0900

time.prev_occurring(:sunday)
=> 2021-03-21 09:33:49 +0900

time.next_week(:wednesday)
=> 2021-03-31 00:00:00 +0900

time.prev_week(:wednesday)
=> 2021-03-17 00:00:00 +0900

time.beginning_of_day
=> 2023-02-26 00:00:00 +0000
# https://github.com/rails/rails/blob/HEAD/activesupport/lib/active_support/core_ext/time/calculations.rb#L241

time.end_of_day
=> 2023-02-26 23:59:59.999999999 +0000
# https://github.com/rails/rails/blob/HEAD/activesupport/lib/active_support/core_ext/time/calculations.rb#L259

自分で調べてみてこんなにもメソッドがたくさんあるのかと思いました。Timeクラスを使えばほとんどのことは表現できそうですね。

もぐくん
もぐくん
覚えるの大変だ・・・
さかい
さかい
使うときに調べればいいからいろんなことができると知っておけばOKだよ

Dateクラス

時間情報を持ちません。また、require 'date'としないと使えないので注意です。

require 'date'
Date.today
=> #<Date: 2021-03-22 ((2459296j,0s,0n),+0s,2299161j)>
Date.new(2021,3,22)
=> #<Date: 2021-03-22 ((2459296j,0s,0n),+0s,2299161j)>

Timeクラスで日付を扱えるので使う機会はそこまで多くないでしょう。

DateTimeクラス

Dateクラスのサブクラスで時間情報を扱えるようにしたものです。公式のリファレンスで下のように非推奨とされているので、Timeクラスを使ったほうがよいでしょう。

DateTime は deprecated とされているため、 Timeを使うことを推奨します。

https://docs.ruby-lang.org/ja/latest/class/DateTime.html
require 'date'
DateTime.now
=> #<DateTime: 2021-03-22T07:57:10+09:00 ((2459295j,82630s,374058000n),+32400s,2299161j)>

Rubyで日付や時間情報を扱う場合はTimeクラスを使う

Active::Support::TimeWithZoneクラス

Railsで使える日付を扱うクラスです。これは次のような特徴を持っています。

  • Rails独自のクラスで、Timeクラスと完全な互換性がある。なので、Timeクラスで使用できるメソッドはすべて使える
  • 実装はTimeクラスを継承しているのではなく親クラスはObject
  • タイムゾーンはapplication.rbの設定が適用される

RailsでTimeWithZoneクラスを使ったほうがいい理由

国をまたいだ開発をする際、Aさんの環境変数TZはUTC、Bさんの環境変数TZはJSTとなっているとします。そうした場合に、AさんとBさんで表示される日時が異なってきます。TimeWithZoneクラスを使えばapplication.rbの設定が適用されるので、環境による差はなくなります。なので、Railsを使って日付を扱う場合は、TimeWithZoneクラスを使うのがよいでしょう。

TimeWithZoneの使い方

currentメソッドやzone.nowメソッドを使うとTimeクラスをTimeWithZoneクラスに変換できます。これにより、Timeクラスを使っていると思っていても実はTimeWithZoneクラスを使っていることも多いです。また、おなじみのcreated_atの中身もTimeWithZoneクラスです。

もぐくん
もぐくん
ややこしい・・
さかい
さかい
currentメソッドやzone.nowメソッドを使うことを意識しよう

Time.now
=> 2021-03-22 08:47:56 +0900

Time.now.class
=> Time

Time.current
=> Mon, 22 Mar 2021 08:46:06 JST +09:00 # 曜日やタイムゾーンに関する情報まで出力されている

Time.current.class
=> ActiveSupport::TimeWithZone

Time.zone.now
=> Mon, 22 Mar 2021 08:49:45 JST +09:00 # Time.zone.nowとTime.currentは同等

User.first.created_at.class # created_atの中身もActive::Support::TimeWithZone
=> ActiveSupport::TimeWithZone

Railsで使える便利メソッド

最後にRailsで使える便利なメソッドを紹介しておこうと思います。

  • to_timeメソッド
    Timeクラスのオブジェクトに変換します。
  • in_time_zoneメソッド
    TimeWithZoneクラスのオブジェクトに変換します。
"2020-03-02".to_time
=> 2020-03-02 00:00:00 +0900

"2020-03-02".in_time_zone
=> Mon, 02 Mar 2020 00:00:00 JST +09:00

"2020".in_time_zone
ArgumentError: argument out of range # 一定の規則を満たさないとエラーになります。

time = Time.now
=> 2021-03-22 10:55:07 +0900
time.in_time_zone
=> Mon, 22 Mar 2021 10:55:07 JST +09:00 # もちろん、Timeクラスのオブジェクトでも変換できます。

これらはTimeクラス → TimeWithZoneクラスだけじゃなく、Stringクラス → TimeWithZoneクラスへの変換もできるので、開発で使う機会も多いのではないかと思います。

  • RailsではTimeWithZoneクラスを使う
  • どの地域のタイムゾーンを使うかはapplication.rbで設定ができる

まとめ

2021年3月時点でのRubyとRailsで日付を扱うベストプラクティスはRubyがTimeクラスRailsがTimeWithZoneクラスです。これらを意識して使うといいかと思います。
ここまで読んでくださりありがとうございました。

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!