電柱日報

日々の由無し事

時刻と時間

※:この記事はCrystal Advent Calendar 2018 の2日目です。

英語の "time" は 時刻(時の流れのある1点)と時間(2つの時刻の差)の両方の意味を含んでいます。

多くの言語にTimeという名前の型が存在しますが,多くの場合それらが表現しているのは時刻であり,時間は「時刻の差」として表現される場合が多いようです。(少なくとも,私の狭い観測範囲の中においては)

例えば,RubyTimeクラスは時刻を表す型で,独立した時間を表す型は標準では用意されていません。2つのTimeオブジェクトの差をとると,その間の秒数(時間)がFloat型で返されます。

start = Time.now
sleep(5)
finish = Time.now
finish - start
#=> 5.004982

また,Timeオブジェクトに数値(秒数)を加減算することで,ある時刻を基準とした別の時刻を取得することができます。

now = Time.now
#=> 2018-11-30 11:56:54 +0900
now + 3600
#=> 2018-11-30 12:56:54 +0900

本来数値には単位はないため,現在時刻に1を加えた際,それが1秒なのか1時間なのか,はたまた1年なのかは明確ではないのですが,ここでは暗黙的に「時刻に加減算される数値は秒数である」という前提が置かれています。

一方,Crystal では,時刻を表すTime型とは別に,時間を表すTime::Span型が標準で用意されており,コンストラクタを使用する場合は以下のようにして生成することができます。

# 日,時間,分,秒,ナノ秒(省略可)を指定する方式
Time::Span.new(1, 2, 3, 4, 567)
# 1日と2時間3分4秒と567ナノ秒

# 時間,分,秒を指定する方式
Time::Span.new(10, 11, 12)
# 10時間11分12秒

# 名前付き引数で秒,ナノ秒で指定する方式
Time::Span.new(seconds: 128, nanoseconds: 256)
# 128秒と256ナノ秒

# 名前付き引数でナノ秒だけを指定する方式
Time::Span.new(nanoseconds: 4000)
# 4マイクロ秒

Timeオブジェクト同士の差を取ると,Time::Spanオブジェクトが返されますし,

start = Time.now
sleep(5)
finish = Time.now
span = finish - start
#=> 00:00:05.004945000
typeof(span)
#=> Time::Span

Timeオブジェクトに直接数値を加減算することはできず,Time::Spanオブジェクト(もしくは後述するTime::MonthSpanオブジェクト)を使用する必要があります。

p now = Time.now
#=> 2018-11-30 15:10:04.188704000 +09:00 Local

p now + Time::Span.new(1,0,0)
#=> 2018-11-30 16:10:04.188704000 +09:00 Local

p now + 3600
#=> Error: no overload matches 'Time#+' with type Int32

ただ,時間計算の度にいちいちTime::Span.newTime::Spanオブジェクトを生成するのは非常に煩雑ですので,標準の整数型にはTime::Spanオブジェクトを生成するためのインスタンスメソッドが定義されています。

  • 1ナノ秒1.nanosecond
  • 1マイクロ秒:1.microsecond
  • 1ミリ秒:1.millisecond
  • 1秒:1.second
  • 1分:1.minute
  • 1時間:1.hour
  • 1日:1.day
  • 1週間(7日間):1.week

Note: これらのメソッドには複数形(weeksdaysなど)も併せて用意されています。

これらを使用すると,時間操作の可読性を大きく向上させることができます。

# rubyで現在時刻から2週間後の時刻を取得する
Time.now + 3600 * 24 * 14

# crystalで現在時刻から2週間後の時刻を取得する
Time.now + 2.weeks

また,Time::Spanには,自身を各単位(ナノ秒,マイクロ秒,ミリ秒,秒,分,時,日)に換算した数値(Float64型)を返すインスタンスメソッド(total_******に複数形の単位名)が用意されています。ですので,「1週間って何秒だっけ?」という時(そんな状況が実際にあるかどうかは別として)にも以下のようにして求めることができます。

p 1.week.total_seconds
#=> 604800.0

月単位の時間操作

1秒は109ナノ秒で1時間は3600秒,1日は24時間で1週間は7日と,週までの時間単位は(閏秒などの例外をのぞいて)いつを起点としても同じ量になります。しかし,月や年といったそれ以上の時間単位になると,そうもいきません。

「ある時刻の1ヶ月後」を取得したい場合には,その月が何日あるのかを考える必要がでてきます。日付の月要素を取り出して1を加える,という大雑把な方法もありますが,年末を挟むと年も修正する必要があったり,8月31日だと翌月(9月)には31日が存在しなかったりと,こちらも細々とした調整が必要になります。

真面目にやろうとすると意外と煩雑な月単位の時間操作のために,Crystal の標準ライブラリにはTime::Span型とは別に月間を表すTime::MonthSpan型が用意されています。

Time::Span型と同様,Time::MonthSpan型にも整数型に生成用のインスタンスメソッドが用意されていますので,自然な形で月や年単位の時間操作を記述することができます。

  • 1ヶ月間:1.month
  • 1年間:1.year
p Time.new(2018,12,1) + 1.month
#=> 2019-01-01 00:00:00.0 +09:00 Local

この時,例えば「1月30日の1ヶ月後」のように,計算後の月には存在しない日を基準として計算した場合,計算後の日付はその月の末日になります。

p Time.new(2018,10,31) + 1.month
#=> 2018-11-30 00:00:00.0 +09:00 Local

ちゃんと閏年も考慮されており,2019年1月29日の1ヶ月後は2019年2月28日が返されますが,2020年(閏年)の1月29日の1ヶ月後は2020年2月29日が返ってきます。

p Time.new(2019,1,29) + 1.month
#=> 2019-02-28 00:00:00.0 +09:00 Local

p Time.new(2020,1,29) + 1.month
#=> 2020-02-29 00:00:00.0 +09:00 Local