LoginSignup
6
3

More than 5 years have passed since last update.

TimeをDateにしたら日付がかわった

Last updated at Posted at 2015-07-14

はじめに

[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を設定している。

date_core.c
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のままインスタンス作ってくれればいいのに。

参考

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3