[Ruby入門] 14. 日付と時刻を扱う（全パターン網羅）

&gt;&gt; 連載の目次は [こちら！](http://qiita.com/prgseek/items/237d02be844534063f1d)

今回は、Rubyで日付と時刻を扱う方法を整理してみる。
Timeクラス、Dateクラス、DateTimeクラス、
さらに Active Support による拡張機能について整理する。

全パターン盛り込んだら、かなり大きなトピックになってしまった...


### ■ Time クラス
UNIXタイムで日時を扱うクラス。これだけでもかなり強力

#### ● 現在時刻でTimeオブジェクトを生成
```ruby
now1 = Time.new; p now1  # 2017-04-25 21:01:27 +0900
now2 = Time.now; p now2  # 2017-04-25 21:01:27 +0900
```

#### ● 特定の日時でTimeオブジェクトを生成

※日付文字列からTimeに変換する方法については、後述の [文字列からTimeオブジェクトに変換](#__anchor1) を参照

```ruby
# 方法1
t1 = Time.local(2017, 5, 1, 9, 33, 45, 0); p t1
   # 2017-05-01 09:33:45 +0900
   # 形式は Time.local(year, mon = 1, day = 1, hour = 0, min = 0, sec = 0, usec = 0)
   # year以外は省略可能。usecはマイクロ秒。

# 方法2
t2 = Time.local(12, 44, 23, 22, 5, 2017, 0, 0, false, "JST"); p t2
   # 2017-05-22 23:44:12 +0900
   # 形式は Time.local(sec, min, hour, mday, mon, year, wday, yday, isdst, zone)
   # 引数は省略できない！ しかも wday, yday, zone は無視されるらしい。
   # 誰が使うのこの形式？
```



#### ● いろんな切り口でTimeオブジェクトから値を取り出す
```ruby
t = Time.new
p [
  t.year,  # 2017   年
  t.month, # 4      月。mon でも同じ。
  t.day,   # 25     日。mday でも同じ。
  t.hour,  # 21     時
  t.min,   # 11     分
  t.sec,   # 25     秒
  t.wday,  # 2      曜日番号。0(日曜日) 〜 6(土曜日)
  t.zone   # JST    タイムゾーン
]
```
上記の他に、夏時間があるか？(isdst) とか 1月1日からの通算日(yday) とかあるけど、あんまり使わないだろうな。



#### ● 曜日を確認する
```ruby
t = Time.new;
p [
  t.sunday?,
  t.monday?,
  t.tuesday?,
  t.wednesday?,
  t.thursday?,
  t.friday?,
  t.saturday?
]
```

<a name="__anchor1"></a>
#### ● 文字列からTimeオブジェクトに変換
```ruby
require "time"
t = Time.parse("2017/04/25 19:23:55"); p t  # 2017-04-25 19:23:55 +0900
t = Time.parse("2017/4/25 19"); p t         # 2017-04-25 19:00:00 +0900
t = Time.parse("2017/6/1 7:5"); p t         # 2017-06-01 07:05:00 +0900
t = Time.parse("2017-8-1 1:2:3"); p t       # 2017-08-01 01:02:03 +0900
t = Time.parse("20170907 122456"); p t      # 2017-09-07 12:24:56 +0900
t = Time.parse("20170907123456"); p t       # 2017-09-07 12:34:56 +0900
t = Time.parse("2017-9/12 12:34:56"); p t   # 2017-09-12 12:34:56 +0900
Time.parse("xoxoxo$$") rescue p $!         # #<ArgumentError: no time information in "xoxoxo$$">
```
かなり柔軟にパースしてくれるので便利！


さらに、日本語表記の日付文字列みたいに、変則的な文字列であった場合、strptime を使って、書式を明示してパースすることができます。

@riocampos さんから教えていただきました。ありがとうざいます！ 
参考: [日本語表記の日時をTimeオブジェクトに変換](http://qiita.com/riocampos/items/de59263ac4e991a98f49)

```ruby
require "time"

input_str = "2017年 5月 5日 10:25"
t = Time.strptime(input_str, "%Y年 %m月%d日 %H:%M")
p t  # 2017-05-05 10:25:00 +0900

input_str = "2017年05月05日 10:25"
t = Time.strptime(input_str, "%Y年 %m月%d日 %H:%M")
p t  # 2017-05-05 10:25:00 +0900
```
「_5月_5日」も「05月05日」も、同じ書式文字列でパースできているところがポイントですね。



#### ● Timeオブジェクトから文字列に変換
```ruby
t = Time.new
str = t.strftime("%Y-%m-%d %H:%M:%S")
puts str  # 2017-04-25 21:45:06
```
よく使いそうな書式は以下。

```
%Y	年4桁
%m  月2桁
%d  日2桁
%H  時2桁
%M  分2桁
%S  秒2桁
%X  時:分:秒 (%H:%M:%S と同じ)
%w  曜日番号。0(日曜日) 〜 6(土曜日)
```


#### ● UNIXタイムの経過秒数 ⇔ TIMEオブジェクト  の相互変換
```ruby
# UNIXタイムの経過秒数 → TIMEオブジェクトへ
sec = 1493125226
t = Time.at(sec); p t  # 2017-04-25 22:00:26 +0900

# TIMEオブジェクト → UNIXタイムの経過秒数へ
t = Time.new
time = t.to_i; p time  # 1493125226  tv_secメソッドでも同じ
```


#### ● Timeオブジェクトから配列に変換
```ruby
t = Time.new
arr = t.to_a
p arr  # [11, 54, 21, 25, 4, 2017, 2, 115, false, "JST"]
```
返される配列は、
[秒 分 時 日 月 年 曜日 年内通算日 夏時間? タイムゾーン]



#### ● Timeオブジェクトで加減算
```ruby
t = Time.new; p t              # 2017-04-25 22:24:09 +0900
t2 = t + (60*60*24*3);   p t2  # 2017-04-28 22:24:09 +0900
t3 = t + (60*60*24*365); p t3  # 2018-04-25 22:24:09 +0900
```
秒で加減算される


#### ● Timeオブジェクトの大小比較
```ruby
require "time"

t1 = Time.parse("2017/04/25 19:23:55");
t2 = Time.parse("2017/05/25 19:23:55");

p (t2 > t1)  # true
```


### ■ Date クラス

+ 日付を扱うクラス
+ 「require "date"」する必要がある
+ Active Support を入れなくてもそこそこ便利なメソッドがある


#### ● Dateオブジェクトの生成
```ruby
require "date"

# 本日の日付で生成
d1 = Date.today; p d1
   # #<Date: 2017-04-26 ((2457870j,0s,0n),+0s,2299161j)>

# 指定の日付で生成
d2 = Date.new(2017, 4, 30); p d2
   # #<Date: 2017-04-30 ((2457874j,0s,0n),+0s,2299161j)>
```
+ newの引数は、year以外は省略可
+ todayの第1引数、およびnewの第4引数 というのがあるが、グレゴリオがどーのユリウスがどーのとかファンタジックな引数なので気にしない


#### ● 月末日を取得したい
```ruby
require "date"

day = Date.new(2020, 2 , -1); p day
   # #<Date: 2020-02-29 ((2458909j,0s,0n),+0s,2299161j)>
```
newの第3引数に負の値をわたすと末日から数える。-1が末日になる。


#### ● いろんな切り口でDateオブジェクトから値を取り出す
```ruby
d = Date.today;
p [
  d.year,  # 2017   年
  d.month, # 4      月。mon でも同じ。
  d.day,   # 26     日。mday でも同じ。
  d.wday,  # 3      曜日番号。0(日曜日) 〜 6(土曜日)
]
```

#### ● 曜日を確認する
```ruby
d = Date.today;
p [
  d.sunday?,
  d.monday?,
  d.tuesday?,
  d.wednesday?,
  d.thursday?,
  d.friday?,
  d.saturday?
]
```


#### ● 文字列からDateオブジェクトに変換
```ruby
require "date"

d = Date.parse("2017/04/25"); p d   # #<Date: 2017-04-25 ((2457869j,0s,0n),+0s,2299161j)>
d = Date.parse("2017/5/5"); p d     # #<Date: 2017-05-05 ((2457879j,0s,0n),+0s,2299161j)>
d = Date.parse("2017-8-1"); p d     # #<Date: 2017-08-01 ((2457967j,0s,0n),+0s,2299161j)>
d = Date.parse("20170907"); p d     # #<Date: 2017-09-07 ((2458004j,0s,0n),+0s,2299161j)>
d = Date.parse("2017-9/12"); p d    # #<Date: 2017-09-12 ((2458009j,0s,0n),+0s,2299161j)>
Date.parse("xoxoxo$$") rescue p $! # #<ArgumentError: invalid date>
```

※ strptime による、書式を明示したパース方法に関しては、前述の [文字列からTimeオブジェクトに変換](#__anchor1)  を参照

#### ● Dateオブジェクトから文字列に変換
```ruby
require "date"

day = Date.today
str = day.strftime("%Y-%m-%d %H:%M:%S")
puts str
# 2017-04-26 00:00:00
# Dateは日付情報までしか保持していないので、時刻部分は0で表示される。
```

よく使いそうな書式は以下。Timeといっしょ。

```
%Y	年4桁
%m  月2桁
%d  日2桁
%H  時2桁
%M  分2桁
%S  秒2桁
%X  時:分:秒 (%H:%M:%S と同じ)
%w  曜日番号。0(日曜日) 〜 6(土曜日)
```


#### ● 日付として正しいかチェックする
```ruby
require "date"

p Date.valid_date?(2017, 12, 31)  # true
p Date.valid_date?(2017, 12, 32)  # false

d1 = Date.parse("20171001") rescue false; p d1  # #<Date: 2017-10-01 ((2458028j,0s,0n),+0s,2299161j)>
d2 = Date.parse("xoxoxo$$") rescue false; p d2  # false

```


#### ● n日前・n日後 / n月前・n月後 / n年前・n年後
```ruby
require "date"
day = Date.today; p day  # #<Date: 2017-04-26 ((2457870j,0s,0n),+0s,2299161j)>

# n日前・n日後
p day.prev_day(5)     # #<Date: 2017-04-21 ((2457865j,0s,0n),+0s,2299161j)>
p day.next_day(10)    # #<Date: 2017-05-06 ((2457880j,0s,0n),+0s,2299161j)>

# n月前・n月後
p day.prev_month(5)   # #<Date: 2016-11-26 ((2457719j,0s,0n),+0s,2299161j)>
p day.next_month(10)  # #<Date: 2018-02-26 ((2458176j,0s,0n),+0s,2299161j)>

# n年前・n年後
p day.prev_year(5)    # #<Date: 2012-04-26 ((2456044j,0s,0n),+0s,2299161j)>
p day.next_year(10)   # #<Date: 2027-04-26 ((2461522j,0s,0n),+0s,2299161j)>
```

#### ● Timeオブジェクトに変換する
```ruby
require "date"

day = Date.today; 
t = day.to_time; p t  # 2017-04-26 00:00:00 +0900
```


#### ● Dateオブジェクトの大小比較
```ruby
require "date"

d1 = Date.parse("2017/5/5");
d2 = Date.parse("2017/6/6");

p (d2 > d1)  # true
```



### ■ DateTime クラス

+ 日付と時刻を扱うクラス
+ Dateのサブクラスなので、Dateのメソッドが使える。時刻の情報まで保持しているので、戻り値が詳細になる。
+ 「require "date"」する必要がある
+ Timeクラスがあるから、こちらはあまり使わないかもしれないが、DateとTimeの良いとこ取りな良さもあるかな。


#### ● 現在時刻でDateTimeオブジェクトを生成
```ruby
require "date"
d = DateTime.now; p d
   # #<DateTime: 2017-04-26T22:12:41+09:00 ((2457870j,47561s,244634000n),+32400s,2299161j)>
```

#### ● 特定の日時でDateTimeオブジェクトを生成
```ruby
require "date"
d = DateTime.new(2017, 4, 26, 22, 03, 45, 0.375); p d
   # #<DateTime: 2017-04-26T22:03:45+09:00 ((2457870j,47025s,0n),+32400s,2299161j)>
```
最後の引数は、協定世界時との時差（オフセット）を指定する。
日本の場合は、時差9時間なので、9時間/24時間 = 0.375 を指定する。



#### ● いろんな切り口でDateTimeオブジェクトから値を取り出す
```ruby
require "date"
dt = DateTime.now;

# Dateの属性に加えて、時刻部分を取得できる
p [
  dt.year,  # 2017   年
  dt.month, # 4      月。mon でも同じ。
  dt.day,   # 26     日。mday でも同じ。
  dt.wday,  # 3      曜日番号。0(日曜日) 〜 6(土曜日)
  dt.hour,  # 22     時
  dt.min,   # 18     分
  dt.sec,   # 24     秒
  dt.zone   # +09:00 タイムゾーン ※Timeクラスと戻り値が違うな
]
```

#### ● 曜日を確認する

Dateのサブクラスなので、Dateとまったく同じ。



#### ● 文字列からDateTimeオブジェクトに変換
```ruby
require "date"

dt = DateTime.parse("2017/04/25 19:23:55"); p dt  # #<DateTime: 2017-04-25T19:23:55+00:00 ((2457869j,69835s,0n),+0s,2299161j)>
dt = DateTime.parse("2017/4/25 19"); p dt         # #<DateTime: 2017-04-25T19:00:00+00:00 ((2457869j,68400s,0n),+0s,2299161j)>
dt = DateTime.parse("2017/6/1 7:5"); p dt         # #<DateTime: 2017-06-01T07:05:00+00:00 ((2457906j,25500s,0n),+0s,2299161j)>
dt = DateTime.parse("2017-8-1 1:2:3"); p dt       # #<DateTime: 2017-08-01T01:02:03+00:00 ((2457967j,3723s,0n),+0s,2299161j)>
dt = DateTime.parse("20170907 122456"); p dt      # #<DateTime: 2017-09-07T12:24:56+00:00 ((2458004j,44696s,0n),+0s,2299161j)>
dt = DateTime.parse("20170907123456"); p dt       # #<DateTime: 2017-09-07T12:34:56+00:00 ((2458004j,45296s,0n),+0s,2299161j)>
dt = DateTime.parse("2017-9/12 12:34:56"); p dt   # #<DateTime: 2017-09-12T12:34:56+00:00 ((2458009j,45296s,0n),+0s,2299161j)>
DateTime.parse("xoxoxo$$") rescue p $!            # #<ArgumentError: invalid date>
```

※ strptime による、書式を明示したパース方法に関しては、前述の [文字列からTimeオブジェクトに変換](#__anchor1)  を参照

#### ● DateTimeオブジェクトから文字列に変換

Dateのサブクラスなので、Dateとまったく同じ。
こちらは時刻の情報まで保持しているので、時刻の %H %M %S とかも0ではなくちゃんと表示される。

```ruby
require "date"

dt = DateTime.now
str = dt.strftime("%Y-%m-%d %H:%M:%S")
puts str  # 2017-04-26 22:37:25
```


#### ● Dateオブジェクトの大小比較

Dateのサブクラスなので、Dateとまったく同じ。
















### ■ Active Support で Time, Date, DateTime クラスをパワーアップする

+ 「Active Support」はRubyの組み込みクラスやモジュールなどの機能を拡張してくれるコンポーネント
+ Railsに同梱されているが、単体のGemとしてもインストールできるので試してみる。
+ Time, Date, DateTime に、共通の便利メソッドが追加される。
+ ここでは、例としてTimeクラスで試してみる


#### ● Active Support の導入

連載の第2回のエディタの環境構築のときに、Gem は bundler で入れた。
なので、Active Support の Gem も bundler で入れる

① 確認。

```
$ gem list activesupport

*** LOCAL GEMS ***

$ 
入ってない
```

② Gemfile のあるディレクトリへ移動して、Gemfile を開く

```
cd /Users/miro/
vi Gemfile
```

③ 以下の1行を追記して保存する

```
gem "activesupport"
```

④ んで、インストールを実行

```
$ bundle install
Fetching gem metadata from https://rubygems.org/..............
〜 中略 〜
Bundle complete! 3 Gemfile dependencies, 11 gems now installed.
〜 中略 〜
```

⑤ インストールされたか確認する

```
$ gem list active

*** LOCAL GEMS ***

activesupport (5.0.2)
インストールされた！
```
「gem list gem名」 は、部分一致で検索してくれる


#### ● Active Support 拡張による便利メソッドを整理する
```ruby
require 'active_support/time'

#
# ここでは、例としてTimeクラスで試しているが、
# これらのメソッドは Dateクラス, DateTimeクラス でも使える
#  ・唯一 seconds_since_midnight だけは、Dateクラスでは使えなかった。秒数取得メソッドだし）
#  ・その他、クラスによって保持している情報が違うので、戻り値は微妙に違う。
#

t = Time.current # 現在時刻
# t = Date.today
# t = DateTime.now

puts '----'

# 日単位
p t.yesterday         # 1日前
p t.tomorrow          # 1日後
p t.ago(3.days)       # n日前
p t.days_ago(3)       # n日前
p 3.days.ago          # n日前
p t.since(3.days)     # n日後
p t.days_since(3)     # n日後
p 3.days.since        # n日後
p t.beginning_of_day  # 今日の開始時間。YYYY-MM-DD 00:00:00
p t.end_of_day        # 今日の最終時間。YYYY-MM-DD 23:59:59

puts '----'

# 月単位
p t.prev_month         # 1ヶ月前
p t.next_month         # 1ヶ月後
p t.ago(3.months)      # nヶ月前
p t.months_ago(3)      # nヶ月前
p 3.months.ago         # nヶ月前
p t.since(3.months)    # nヶ月後
p t.months_since(3)    # nヶ月後
p 3.months.since       # nヶ月後
p t.beginning_of_month # 今月の月初日（時間は 00:00:00 になる）
p t.end_of_month       # 今月の月末日（時間は 23:59:59 になる）

puts '----'

# 週単位
p t.beginning_of_week   # 今週の初めの日。月曜が開始。（時間は 00:00:00 になる）
p t.end_of_week         # 今週の最後の日。月曜が開始。（時間は 23:59:59 になる）
p t.prev_week           # 先週の初めの日。月曜が開始。（時間は 00:00:00 になる）
p t.next_week           # 来週の初めの日。月曜が開始。（時間は 00:00:00 になる）
p t.prev_week(:friday)  # 先週のほにゃ曜日（時間は 00:00:00 になる）
p t.next_week(:friday)  # 来週のほにゃ曜日（時間は 00:00:00 になる）
p t.ago(3.weeks)        # n週間前
p t.weeks_ago(3)        # n週間前
p 3.weeks.ago           # n週間前
p t.since(3.weeks)      # n週間後
p t.weeks_since(3)      # n週間後
p 3.weeks.since         # n週間後

puts '----'

# 年単位
p t.prev_year          # 1年前
p t.next_year          # 1年後
p t.beginning_of_year  # 今年の年初日（時間は 00:00:00 になる）
p t.end_of_year        # 今年の年末日（時間は 23:59:59 になる）
p t.ago(3.years)       # n年前
p t.years_ago(3)       # n年前
p 3.years.ago          # n年前
p t.since(3.years)     # n年後
p t.years_since(3)     # n年後
p 3.years.since        # n年後

puts '----'

# 時・分・秒
# ちなみに hours_ago とか seconds_since とかは無いらしい
p t.ago(3600.seconds)    # n秒前
p 3600.seconds.ago       # n秒前
p t.since(3600.seconds)  # n秒後
p 3600.seconds.since     # n秒後
p t.ago(60.minutes)      # n分前
p 60.minutes.ago         # n分前
p t.since(60.minutes)    # n分後
p 60.minutes.since       # n分後
p t.ago(1.hours)         # n時間前
p 1.hours.ago            # n時間前
p t.since(1.hours)       # n時間後
p 1.hours.since          # n時間後

puts '----'

# かっこいいメソッド
p t.seconds_since_midnight  # 今日の深夜0時からの経過秒数を取得

# 四半期！
p t.beginning_of_quarter  # 今四半期の初日  （時間は 00:00:00 になる）
p t.end_of_quarter        # 今四半期の最終日（時間は 23:59:59 になる）

# 差分を指定して取得（ advance ）
p t.advance(months: 6, days: -10)  # ６ヶ月後の10日前。yearsでもhoursでもご随意に。

# 特定の日時を指定して取得（ change ）
p t.change(year:2020, month:12, day:24)  # yearsでもhoursでもご随意に。

puts '----'

# 加減算
p t + 5.days
p t - 1.week
p t - 2.years
```