閏年の2月29日の1年前 Ruby(on Rails) & Go
仕事で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