はじめに
[1] pry(main)> Date.new(1, 1, 1).to_time.to_date
=> Mon, 03 Jan 0001
あ…ありのまま 今 起こった事を話すぜ!
「1月1日だと思ったら 1月3日だった」
な… 何を言っているのか わからねーと思うが
おれも 何をされたのか わからなかった…
頭がどうにかなりそうだった… 催眠術だとか超スピードだとか
そんなチャチなもんじゃあ 断じてねえ
もっと恐ろしいものの片鱗を 味わったぜ…
起こったこと
特定のTime型をDate型に変換したら意図しない日付になった。
[1] pry(main)> Time.new(1, 1, 1).to_date
=> Mon, 03 Jan 0001
[2] pry(main)> Time.new(1582, 10, 14).to_date
=> Thu, 04 Oct 1582
謎すぎる。
深追いしてわかったこと
これは、グレゴリオ暦とユリウス暦の違いが原因で起こっていた。
グレゴリア歴とユリウス暦
グレゴリオ暦とユリウス暦はうるう年の計算方法が違う。昔はユリウス歴の計算方法だったんだけど、最近はより正確なグレゴリオ暦が使われているらしい。
Date型には、ほとんどの場合気にしなくていい.start
というパラメータがあって、これはグレゴリオ暦をつかい始めた日をあらわすユリウス日を指している。デフォルトではイタリアがグレゴリア歴に変更した日(1582/10/15)が入っている。先発グレゴリオ歴を示すDate::GREGORIAN
を指定すると、問答無用でグレゴリオ暦として考える。
[3] pry(main)> Date.today.start
=> 2299161.0
[4] pry(main)> Date::ITALY
=> 2299161
[5] pry(main)> Date::GREGORIAN
=> -Infinity
.start
以前はユリウス暦を使って表現されている。
[6] pry(main)> d = Date.new(1582, 10, 15)
=> Fri, 15 Oct 1582
[7] pry(main)> d.gregorian?
=> true
[8] pry(main)> d - 1 # 一気に日付がかわる
=> Thu, 04 Oct 1582
[9] pry(main)> (d - 1).gregorian?
=> false
どっちの歴を基準にしてるかの違いで、2月29日があったりなかったりする。
[10] pry(main)> Date.new(1100, 2, 29) # イタリア基準で、1582/10/15以前なのでユリウス暦
=> Wed, 29 Feb 1100
[11] pry(main)> Date.new(1100, 2, 29, Date::GREGORIAN) # 先発グレゴリア歴
ArgumentError: invalid date
from (pry):5:in `new'
グレゴリオ歴になった日の直前は、狭間の日になることがある。
狭間の日になる組み合わせの場合エラーになる。
[12] pry(main)> Date.new(1582, 10, 4)
=> Thu, 04 Oct 1582
[13] pry(main)> Date.new(1582, 10, 5)
ArgumentError: invalid date
from (pry):17:in `new'
[14] pry(main)> Date.new(1582, 10, 14)
ArgumentError: invalid date
from (pry):18:in `new'
[15] pry(main)> Date.new(1582, 10, 15)
=> Fri, 15 Oct 1582
Time型の歴
Time型は先発グレゴリオ暦を使用するので、問答無用でグレゴリオ暦になる。
[16] pry(main)> Time.new(100, 2, 29)
=> 0100-03-01 00:00:00 +0900
Time#to_dateの内部実装は、グレゴリオ暦としてのユリウス日を登録したあとに、イタリア基準のstartを設定している。
static VALUE
time_to_date(VALUE self)
{
...
// GREGORIAN(先発グレゴリオ暦)としてy, m, dを登録
ret = d_simple_new_internal(cDate,
nth, 0,
GREGORIAN,
ry, m, d,
HAVE_CIVIL);
{
get_d1(ret);
// DEFAULG_SG(イタリアのグレゴリオ暦開始日)に設定しなおしている。
set_sg(dat, DEFAULT_SG);
}
return ret;
}
Time#to_dateのキャストによって、先発グレゴリオ暦としてのYmdにあたる日をイタリア基準の日で表現してしまっている。なので、イタリアがグレゴリオ暦を使う以前の日付について、(本当はグレゴリオ暦で設定したのに)ユリウス暦を使うのでずれたように見えている。日付情報が失われたわけでも壊れたわけでもない。
けっきょく
.new_start
を使えば基準日を変更した新しいDateインスタンスを取得できる。Date::GREGORIAN
を指定すれば、Time型のときに見えていた値になる。
[17] pry(main)> d = Date.new(1,1,1).to_time.to_date
=> Mon, 03 Jan 0001
[18] pry(main)> d.new_start(Date::GREGORIAN)
=> Mon, 01 Jan 0001
Time#to_date
でset_sg(dat, DEFAULG_SG)なんてしないですなおにGREGORIANのままインスタンス作ってくれればいいのに。
参考