📅

閏年の2月29日の1年前 Ruby(on Rails) & Go

2024/03/23に公開

仕事でRailsとGoの両方を扱っていて、最近その両方で日付の計算を扱うことがあって、たまたま今年が閏年で、2月29日の一年前の振る舞いが果たしてどうなるかということが気になって調べてみます。

Goの場合(v1.22.0)

package main

import (
	"fmt"
	"time"
)

var jst = time.FixedZone("Asia/Tokyo", 9*60*60)

func main() {
	t1 := time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC)
	t2 := time.Date(2024, 2, 29, 0, 0, 0, 0, jst)

	fmt.Println(t1.AddDate(-1, 0, 0))
	fmt.Println(t2.AddDate(-1, 0, 0))
}

結果

❯ go run main.go
2023-03-01 00:00:00 +0000 UTC
2023-03-01 00:00:00 +0900 Asia/Tokyo

Goの標準のtime.Timeを利用している場合に、1年前の計算をAddDateを利用することになり、上記のように、タイムゾーンに関係なく、3月1日になります。

理由は実装を追うと、わりと簡単にわかります。
AddDate関数は内部ではDate関数を利用しており、

func (t Time) AddDate(years int, months int, days int) Time {
	year, month, day := t.Date()
	hour, min, sec := t.Clock()
	return Date(year+years, month+Month(months), day+days, hour, min, sec, int(t.nsec()), t.Location())
}

そしてDate関数の説明では以下のような記述が見られます。

// The month, day, hour, min, sec, and nsec values may be outside
// their usual ranges and will be normalized during the conversion.
// For example, October 32 converts to November 1.

ということで、2023-2-30というありえない日付を試すと、しっかりと、2023-3-2が出てくることが確認できます。

fmt.Println(time.Date(2023, 2, 30, 0, 0, 0, 0, jst))

Ruby (v3.2.2)

いつもRailsのTimeWithZoneのほうを使っており、改めてRubyの日時について調べてみましたが、RubyにはTime, Date, DateTimeの3つ似た感じのクラスがあります。

公式では

DateTime は deprecated とされているため、 Timeを使うことを推奨します

という記述があるので、DateTimeは触れないでおきます。

まず、Time.newとGoのtime.Date()の振る舞いは同じです。

irb(main):010> Time.new(2023, 2, 29)
=> 2023-03-01 00:00:00 +0900
irb(main):013> Time.new(2023, 2, 30)
=> 2023-03-02 00:00:00 +0900

Dateのほうというと、存在しない日時を渡すと、例外が発生します。

irb(main):003> Date.new(2023, 2, 29)
(irb):3:in `initialize': invalid date (Date::Error)

Date.new(2023, 2, 29)
         ^^^^^^^^^^^

Timeオブジェクトには1年を進ませたり、戻したりするというようなメソッドがありません。一応+で秒数を指定することが可能ですが、そうなると、使う側が1年の秒数を考えることになりますので、曖昧性を検証する意味がなくなってしまいます。

Dateオブジェクトはというと、うってつけのメソッド.prev_yearが存在します。

irb(main):005> Date.new(2024, 2, 29).prev_year
=> Tue, 28 Feb 2023

結果、こちらは2月28日が返ってきました。Goの振る舞いとは異なりますね。
ちなみに、Dateにはオフセットやタイムゾーンといった概念はありません。

Railsの場合(v7.1.3.2)

まず、不正な日付については、Timeと同様に、正規化の働きがあります。

irb(main):002> Time.zone.local(2023, 2, 29)
=> Wed, 01 Mar 2023 00:00:00.000000000 UTC +00:00
irb(main):003> Time.zone.local(2023, 2, 30)
=> Thu, 02 Mar 2023 00:00:00.000000000 UTC +00:00

日付を1年戻す操作を行うと、

irb(main):011> Time.zone.local(2024, 2, 29).advance(years: -1)
=> Tue, 28 Feb 2023 00:00:00.000000000 UTC +00:00
irb(main):012> Time.zone.local(2024, 2, 29) - 1.year
=> Tue, 28 Feb 2023 00:00:00.000000000 UTC +00:00
irb(main):013>

Dateの時と同じように2月28日が返ります。タイムゾーンによる影響はありません。

irb(main):013> Time.zone.local(2024, 2, 29).change(offset:"+0900").advance(years: -1)
=> Tue, 28 Feb 2023 00:00:00.000000000 JST +09:00
irb(main):014> Time.zone.local(2024, 2, 29).change(offset:"+0900") - 1.year
=> Tue, 28 Feb 2023 00:00:00.000000000 JST +09:00

今回調べて、はじめて、TimeWithZoneには.advanceというメソッドがあることに気づきました。しかもちゃんとプラス・マイナスの書き方と振る舞いが一貫してますね。

Discussion