rubyで時刻だけのオブジェクトを扱いたい。そんな時は多機能なgem todを使おう。

ちょっとテンションが上がってしまったのでTodについてまとめました。

多機能なTod::TimeOfDay

todをインストールするとTod::TimeOfDayクラスが使えるようになります。

  • 時刻のパース
  • 比較
  • フォーマット出力
  • 演算
  • その他ちょろちょろ
  • Railsのシリアライズもサポートしてるよ!

と、こんな機能を提供してくれます。

インストールも簡単

gem install tod

bundlerを使っているならGemfileにtodを追加してbundle install
--path vendor/bundleは必要なら。

bundle install # --path vebdor/bundle

もちろんいつもの感じでnewできる

tod = Tod::TimeOfDay.new(15, 1, 1) # 15:01:01
# => #<Tod::TimeOfDay:0x007fa6b5c553d8 @hour=15, @minute=1, @second=1, @second_of_day=54061>

思い通りに時刻をパース

詳しくは公式のReadMeを読んでいただければと思いますが、こんな感じで思い通りにパースできます。

Tod::TimeOfDay.parse("8")
# => #<Tod::TimeOfDay:0x007fa6b5d34330 @hour=8, @minute=0, @second=0, @second_of_day=28800>
Tod::TimeOfDay.parse("8am")
# => #<Tod::TimeOfDay:0x007fa6b5d2d008 @hour=8, @minute=0, @second=0, @second_of_day=28800>
Tod::TimeOfDay.parse("8pm")
# => #<Tod::TimeOfDay:0x007fa6b5d1f700 @hour=20, @minute=0, @second=0, @second_of_day=72000>
Tod::TimeOfDay.parse("8p")
# => #<Tod::TimeOfDay:0x007fa6b4cd4c60 @hour=20, @minute=0, @second=0, @second_of_day=72000>
Tod::TimeOfDay.parse("9:30")
# => #<Tod::TimeOfDay:0x007fa6b4cc6390 @hour=9, @minute=30, @second=0, @second_of_day=34200>

to_sすると分かりやすい時刻表示を取得できます。

Tod::TimeOfDay.parse("8").to_s
# => "08:00:00"
Tod::TimeOfDay.parse("8am").to_s
# => "08:00:00"
Tod::TimeOfDay.parse("8pm").to_s
# => "20:00:00"
Tod::TimeOfDay.parse("8p").to_s
# => "20:00:00"
Tod::TimeOfDay.parse("9:30").to_s
# => "09:30:00"

パースできるかわからないオブジェクトに対しても柔軟に対応

パースできればパース、できなければnilを返してくれるtry_parseメソッド

Tod::TimeOfDay.try_parse "3:30pm"
# => #<Tod::TimeOfDay:0x007fb37c438f28 @hour=15, @minute=30, @second=0, @second_of_day=55800>
Tod::TimeOfDay.try_parse "foo"
# => nil

パースできるかチェックして、BOOLで返してくれるparsable?メソッド

Tod::TimeOfDay.parsable? "3:30pm"
=> true
Tod::TimeOfDay.parsable? "foo"
# => false

もちろん時刻の比較や演算もできる

start_at = Tod::TimeOfDay.parse("10:00")
end_at = Tod::TimeOfDay.parse("13:00")
start_at < end_at
# => true
start_at > end_at
# => false
start_at == end_at
# => false

======= 追記 2016/5/15 =======
Timeと同じようにTod::TimeOfDayでも引き算できるようにしてみたらマージされたので追記しました。
足し算はTimeでもできないし、そもそもする場面が想像つかないのでつけませんでした。

end_at - start_at
# => #<Tod::TimeOfDay:0x007fbd49e8a9a0 @hour=3, @minute=0, @second=0, @second_of_day=10800>

いつも通りのフォーマット指定で思い通りに時刻出力

tod = Tod::TimeOfDay.parse("9:30:01")
tod.strftime("%I:%M:%S %p")
=> "09:30:01 AM"

日付情報が必要になっても簡単に情報付加できる

tod = Tod::TimeOfDay.parse("10:00")
# => #<Tod::TimeOfDay:0x007fb37c3e35c8 @hour=10, @minute=0, @second=0, @second_of_day=36000>
tod.on Date.today
# => Sat, 30 Apr 2016 10:00:00 UTC +00:00

時刻が範囲に含まれているかも簡単に確認出来る

start_at = Tod::TimeOfDay.parse("10:00")
end_at = Tod::TimeOfDay.parse("13:00")
range = Tod::Shift.new(start_at, end_at)
range.include?(Tod::TimeOfDay.parse("11:30"))
# => true
range.include?(Tod::TimeOfDay.parse("13:30"))
# => false

require 'tod/core_extensions'すればTimeDateTimeDateクラスに便利なメソッドを追加してくれる

require 'tod/core_extensions'
# => true
tod = Tod::TimeOfDay.parse("10:00")
# => #<Tod::TimeOfDay:0x007fb37c38bad0 @hour=10, @minute=0, @second=0, @second_of_day=36000>
Date.today.at tod # Dateオブジェクトに時刻情報を追加
# => Sat, 30 Apr 2016 10:00:00 UTC +00:00
Time.now.to_time_of_day # TimeオブジェクトをTimeOfDayオブジェクトに変換
# => #<Tod::TimeOfDay:0x007fb37c37b270 @hour=1, @minute=28, @second=12, @second_of_day=5292>
DateTime.now.to_time_of_day # DateTimeオブジェクトをTimeOfDayオブジェクトに変換
# => #<Tod::TimeOfDay:0x007fb37c373318 @hour=1, @minute=28, @second=12, @second_of_day=5292>

Tod::TimeOfDayにTimeやDate、DateTimeオブジェクトを渡せばTimeOfDayオブジェクトに変換してくれる

time = Time.now
# => 2016-04-30 01:06:00 +0900
Tod::TimeOfDay(time)
# => #<Tod::TimeOfDay:0x007fb37d190c98 @hour=1, @minute=6, @second=0, @second_of_day=3960>
date = Date.today
# => Sat, 30 Apr 2016
Tod::TimeOfDay(date)
# => #<Tod::TimeOfDay:0x007fb37d180988 @hour=0, @minute=0, @second=0, @second_of_day=0>
datetime = DateTime.now
# => Sat, 30 Apr 2016 01:08:02 +0900
Tod::TimeOfDay(datetime)
# => #<Tod::TimeOfDay:0x007fb37d169878 @hour=1, @minute=8, @second=2, @second_of_day=4082>

ActiveRecordのSerializeを使うと便利

Serializeは好きな形式のデータをカラムに保存できるActiveRecordの機能です。
ruby, rails界隈で有名な@jnchitoさんのActiveRecord serialize / store の甘い誘惑を断ち切ろうという投稿で詳しく書かれているので是非読んでいただきたいですが、この投稿では以下の理由でSerializeは良くないと言っています。

  1. シリアライズの形式が突然変更される可能性がある
  2. 検索ができない、ソートもできない、集計もできない、インデックスも貼れない
  3. データ構造やデータの内容が簡単にわからない
  4. 仕様変更に弱い
  5. 遅い

しかし、Todはシリアライズロジックを実装しているので、DBへの登録時には検索、インデックスなどしやすい文字列に変換、DBからの取得時はTod::TimeOfDayオブジェクトに変換してくれるため、yamlで登録されて検索やindexが貼れないといったような問題が発生しません。使っちゃいましょう。(この辺りの話は先ほどの記事のコメントにあります。 @jnchito さんありがとうございました。 )

class Order < ActiveRecord::Base
  serialize :time, Tod::TimeOfDay
end
order = Order.create(time: Tod::TimeOfDay.new(9,30))
order.time                                      # => 09:30:00

便利ですね!