[インデックス 13671] ファイルの概要
このコミットは、Go言語の標準ライブラリである time
パッケージに time.Time
型の新しいメソッド YearDay()
を追加するものです。このメソッドは、指定された time.Time
オブジェクトがその年の何日目であるか(1月1日を1日目とする)を返します。これにより、日付計算で内部的に利用されていた「年の通算日」の情報を、開発者が直接取得できるようになります。
コミット
commit 7802080962dcbffea09894c9864bb4c30fdd6ce3
Author: Carlos Castillo <cookieo9@gmail.com>
Date: Wed Aug 22 20:49:16 2012 -0700
time: add YearDay method for time.Time structs
YearDay provides the day in the year represented by a given time.Time
object. This value is normally computed as part of other date calculations,
but not exported.
Fixes #3932.
R=golang-dev, r, remyoudompheng
CC=golang-dev, rsc
https://golang.org/cl/6460069
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7802080962dcbffea09894c9864bb4c30fdd6ce3
元コミット内容
time: add YearDay method for time.Time structs
このコミットは、time.Time
オブジェクトが表す年の通算日(YearDay)を提供する YearDay
メソッドを追加します。この値は通常、他の日付計算の一部として内部的に計算されていましたが、外部には公開されていませんでした。
この変更は、Issue #3932 を修正します。
レビュー担当者: golang-dev, r, remyoudompheng CC: golang-dev, rsc 変更リスト: https://golang.org/cl/6460069
変更の背景
この変更の背景には、Go言語の time
パッケージにおいて、日付の「年の通算日」(Year Day)を直接取得する機能が求められていたことがあります。既存の time.Time
型は、年、月、日、時、分、秒などの個別の要素を取得するメソッドを提供していましたが、その年における通算日(例えば、1月1日は1日目、1月2日は2日目、2月1日は32日目など)を直接取得する標準的な方法がありませんでした。
コミットメッセージにある Fixes #3932
は、この機能追加がGoのIssueトラッカーで報告された問題 #3932 を解決することを示しています。Issue #3932 のタイトルは「time: add yearday method?」であり、ユーザーがこの機能の必要性を提起したことが伺えます。
通常、年の通算日は、日付の比較や特定の期間計算など、様々なアプリケーションで必要となることがあります。例えば、ある日付が年の前半か後半かを判断したり、特定のイベントが年の何日目に発生するかを記録したりする場合に便利です。この値は time
パッケージの内部で日付計算のために既に利用されていましたが、外部に公開されていなかったため、開発者はこの情報を得るために独自の計算ロジックを実装する必要がありました。
このコミットは、その内部的な計算結果を YearDay()
メソッドとして公開することで、開発者の利便性を向上させ、コードの重複を避け、time
パッケージの一貫性を高めることを目的としています。
前提知識の解説
Go言語の time
パッケージ
Go言語の time
パッケージは、時間の測定と表示のための機能を提供します。これには、時刻(time.Time
)、期間(time.Duration
)、タイムゾーン(time.Location
)などが含まれます。
time.Time
構造体: 特定の時点を表す型です。内部的には、エポック(1970年1月1日UTC)からの経過時間をナノ秒単位で保持しています。time.Time
の既存メソッド:Year()
,Month()
,Day()
,Hour()
,Minute()
,Second()
,Nanosecond()
など、日付と時刻の各要素を取得するメソッドが提供されています。date
メソッド(内部):time.Time
構造体には、日付の各要素(年、月、日、年の通算日)を計算するための内部的なdate
メソッドが存在します。このメソッドは、YearDay()
メソッドが追加される前から、他の日付関連のメソッド(例:Year()
,Month()
,Day()
)の基盤として利用されていました。
年の通算日 (Year Day)
年の通算日(または「年の日」)とは、ある日付がその年の1月1日から数えて何日目にあたるかを示す数値です。1月1日は1日目、1月2日は2日目となります。閏年(うるうどし)の場合、2月29日が存在するため、年の通算日の最大値は366日になります。平年(閏年ではない年)の場合、最大値は365日です。
閏年 (Leap Year)
閏年は、グレゴリオ暦において、通常365日の年に1日(2月29日)を追加して366日とする年です。閏年の規則は以下の通りです。
- 西暦年が4で割り切れる年は閏年である。
- ただし、100で割り切れる年は閏年ではない。
- ただし、400で割り切れる年は閏年である。
例:
- 2000年: 400で割り切れるため閏年 (366日)
- 2004年: 4で割り切れるため閏年 (366日)
- 1900年: 100で割り切れるが400で割り切れないため平年 (365日)
- 2007年: 4で割り切れないため平年 (365日)
テスト駆動開発 (TDD) の原則
このコミットでは、新しい機能 YearDay()
の追加と同時に、その機能を検証するための広範なテストケースが time_test.go
に追加されています。これは、ソフトウェア開発におけるテスト駆動開発(TDD)の原則、または少なくとも堅牢な単体テストの重要性を示しています。新しい機能が期待通りに動作することを確認し、将来の変更によって既存の機能が壊れないことを保証するために、様々なシナリオ(平年、閏年、世紀末の閏年、紀元前、異なるタイムゾーンなど)をカバーするテストが書かれています。
技術的詳細
このコミットの技術的な核心は、time.Time
構造体に YearDay()
メソッドを追加し、その実装が既存の内部メソッド t.date(false)
を利用している点にあります。
YearDay()
メソッドの追加
src/pkg/time/time.go
に以下のメソッドが追加されました。
// YearDay returns the day of the year specified by t, in the range [1, 365] for non-leap years,
// and [1,366] in leap years.
func (t Time) YearDay() int {
_, _, _, yday := t.date(false)
return yday + 1
}
- メソッドシグネチャ:
func (t Time) YearDay() int
は、Time
型のレシーバt
を持つメソッドであり、整数値を返します。 - 内部呼び出し:
t.date(false)
を呼び出しています。date
メソッドはtime.Time
の内部メソッドで、日付の年、月、日、そして年の通算日を計算するために使用されます。date
メソッドのシグネチャはfunc (t Time) date(full bool) (year int, month Month, day int, yday int)
です。full
引数がfalse
の場合、date
メソッドは年と年の通算日 (yday
) のみを効率的に計算し、月と日は計算しないか、ダミー値を返します。これは、YearDay()
メソッドが月や日の情報を必要としないため、不要な計算を避けるための最適化です。
- 戻り値の調整:
t.date(false)
から返されるyday
は、内部的には0から始まるインデックス(0が1月1日)であると推測されます。しかし、一般的に「年の通算日」は1から始まるため、yday + 1
として返しています。これにより、1月1日は1、12月31日は365または366という直感的な値が提供されます。
date
メソッドのコメント修正
src/pkg/time/time.go
の date
メソッドのコメントも、その機能が年の通算日 (yday
) を計算することを含んでいることを明確にするために修正されました。
変更前:
// date computes the year and, only when full=true,
// the month and day in which t occurs.
変更後:
// date computes the year, day of year, and when full=true,
// the month and day in which t occurs.
この変更は、date
メソッドが yday
を常に計算していることを明示し、YearDay()
メソッドがこの内部計算を利用していることを示唆しています。
テストケースの追加 (src/pkg/time/time_test.go
)
YearDay()
メソッドの正確性を保証するために、time_test.go
に広範なテストケースが追加されました。
YearDayTest
構造体: テストデータを定義するための構造体です。type YearDayTest struct { year, month, day int yday int }
year
,month
,day
は入力となる日付、yday
は期待される年の通算日です。yearDayTests
スライス: 様々なシナリオをカバーするYearDayTest
のスライスです。- 平年: 2007年の日付で、1月1日から12月31日までの通算日を検証。
- 閏年: 2008年の日付で、2月29日を含む通算日を検証。
- 閏年ではない世紀末: 1900年の日付で、100で割り切れるが400で割り切れないため閏年ではないケースを検証。
- 紀元1年: 1年(非閏年)の日付を検証。
- 紀元前1年: -1年(非閏年)の日付を検証。
- 紀元前400年: -400年(閏年)の日付を検証。
- 特殊ケース: グレゴリオ暦への移行期(1582年10月4日から10月15日への日付のスキップ)が
YearDay
の計算に影響しないことを検証。
yearDayLocations
スライス: 異なるタイムゾーンでのYearDay()
の動作を検証するためのtime.Location
のスライスです。YearDay
はタイムゾーンに依存しないはずですが、念のため異なるオフセットのタイムゾーンでテストしています。TestYearDay
関数: 実際のテストロジックです。- 各タイムゾーン (
loc
) と各テストデータ (ydt
) の組み合わせに対してループを実行します。 Date(ydt.year, Month(ydt.month), ydt.day, 0, 0, 0, 0, loc)
を使用してtime.Time
オブジェクトを作成します。dt.YearDay()
を呼び出して実際の年の通算日を取得します。- 取得した値が期待される
ydt.yday
と一致するかをif yday != ydt.yday
で比較します。 - 一致しない場合は
t.Errorf
を使用してエラーを報告します。エラーメッセージには、テスト対象の日付とタイムゾーン、期待値と実際の値が含まれ、デバッグに役立ちます。
- 各タイムゾーン (
この包括的なテストスイートは、YearDay()
メソッドが様々なエッジケースやタイムゾーンの状況下でも正確に機能することを保証します。
コアとなるコードの変更箇所
src/pkg/time/time.go
--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -412,6 +412,13 @@ func (t Time) Nanosecond() int {
return int(t.nsec)
}
+// YearDay returns the day of the year specified by t, in the range [1, 365] for non-leap years,
+// and [1,366] in leap years.
+func (t Time) YearDay() int {
+ _, _, _, yday := t.date(false)
+ return yday + 1
+}
+
// A Duration represents the elapsed time between two instants
// as an int64 nanosecond count. The representation limits the
// largest representable duration to approximately 290 years.
@@ -641,7 +648,7 @@ const (
days1970To2001 = 31*365 + 8
)
-// date computes the year and, only when full=true,
+// date computes the year, day of year, and when full=true,
// the month and day in which t occurs.
func (t Time) date(full bool) (year int, month Month, day int, yday int) {
return absDate(t.abs(), full)
src/pkg/time/time_test.go
--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -611,6 +611,103 @@ func TestISOWeek(t *testing.T) {
}
}
+type YearDayTest struct {
+ year, month, day int
+ yday int
+}
+
+// Test YearDay in several different scenarios
+// and corner cases
+var yearDayTests = []YearDayTest{
+ // Non-leap-year tests
+ {2007, 1, 1, 1},
+ {2007, 1, 15, 15},
+ {2007, 2, 1, 32},
+ {2007, 2, 15, 46},
+ {2007, 3, 1, 60},
+ {2007, 3, 15, 74},
+ {2007, 4, 1, 91},
+ {2007, 12, 31, 365},
+
+ // Leap-year tests
+ {2008, 1, 1, 1},
+ {2008, 1, 15, 15},
+ {2008, 2, 1, 32},
+ {2008, 2, 15, 46},
+ {2008, 3, 1, 61},
+ {2008, 3, 15, 75},
+ {2008, 4, 1, 92},
+ {2008, 12, 31, 366},
+
+ // Looks like leap-year (but isn't) tests
+ {1900, 1, 1, 1},
+ {1900, 1, 15, 15},
+ {1900, 2, 1, 32},
+ {1900, 2, 15, 46},
+ {1900, 3, 1, 60},
+ {1900, 3, 15, 74},
+ {1900, 4, 1, 91},
+ {1900, 12, 31, 365},
+
+ // Year one tests (non-leap)
+ {1, 1, 1, 1},
+ {1, 1, 15, 15},
+ {1, 2, 1, 32},
+ {1, 2, 15, 46},
+ {1, 3, 1, 60},
+ {1, 3, 15, 74},
+ {1, 4, 1, 91},
+ {1, 12, 31, 365},
+
+ // Year minus one tests (non-leap)
+ {-1, 1, 1, 1},
+ {-1, 1, 15, 15},
+ {-1, 2, 1, 32},
+ {-1, 2, 15, 46},
+ {-1, 3, 1, 60},
+ {-1, 3, 15, 74},
+ {-1, 4, 1, 91},
+ {-1, 12, 31, 365},
+
+ // 400 BC tests (leap-year)
+ {-400, 1, 1, 1},
+ {-400, 1, 15, 15},
+ {-400, 2, 1, 32},
+ {-400, 2, 15, 46},
+ {-400, 3, 1, 61},
+ {-400, 3, 15, 75},
+ {-400, 4, 1, 92},
+ {-400, 12, 31, 366},
+
+ // Special Cases
+
+ // Gregorian calendar change (no effect)
+ {1582, 10, 4, 277},
+ {1582, 10, 15, 288},
+}
+
+// Check to see if YearDay is location sensitive
+var yearDayLocations = []*Location{
+ FixedZone("UTC-8", -8*60*60),
+ FixedZone("UTC-4", -4*60*60),
+ UTC,
+ FixedZone("UTC+4", 4*60*60),
+ FixedZone("UTC+8", 8*60*60),
+}
+
+func TestYearDay(t *testing.T) {
+ for _, loc := range yearDayLocations {
+ for _, ydt := range yearDayTests {
+ dt := Date(ydt.year, Month(ydt.month), ydt.day, 0, 0, 0, 0, loc)
+ yday := dt.YearDay()
+ if yday != ydt.yday {
+ t.Errorf("got %d, expected %d for %d-%02d-%02d in %v",
+ yday, ydt.yday, ydt.year, ydt.month, ydt.day, loc)
+ }
+ }
+ }
+}
+
var durationTests = []struct {
str string
d Duration
コアとなるコードの解説
time.go
の変更
YearDay()
メソッドの追加: このメソッドはtime.Time
型に新しい機能を追加します。その目的は、time.Time
オブジェクトが表す日付が、その年の何日目にあたるか(1月1日を1日目として)を整数で返すことです。 実装は非常にシンプルで、t.date(false)
という内部メソッドを呼び出しています。date
メソッドは、time.Time
の内部表現から年、月、日、そして年の通算日を計算する役割を担っています。false
を引数として渡すことで、date
メソッドは月と日の詳細な計算をスキップし、年の通算日 (yday
) のみを効率的に取得します。date
メソッドから返されるyday
は0から始まるインデックス(例えば、1月1日は0)であるため、外部に公開するYearDay()
メソッドではyday + 1
として、1から始まる通算日(1月1日は1)に変換しています。これにより、ユーザーは直感的に理解しやすい値を得ることができます。date
メソッドのコメント修正:date
メソッドの既存のコメントが更新され、このメソッドがyear
とday of year
(年の通算日) を計算すること、そしてfull=true
の場合にのみmonth
とday
を計算することを明確にしています。これは、YearDay()
メソッドがdate
メソッドのyday
戻り値を利用していることを反映した、ドキュメントの正確性を高めるための変更です。
time_test.go
の変更
YearDayTest
構造体: テストケースを構造化するために定義されたヘルパー構造体です。year
,month
,day
でテスト対象の日付を指定し、yday
でその日付に対する期待される年の通算日を定義します。これにより、テストデータの可読性と管理が向上します。yearDayTests
変数:YearDayTest
構造体のスライスとして、様々な日付とそれに対応する期待される年の通算日のペアが定義されています。これには、平年、閏年、閏年ではない世紀末の年(例: 1900年)、紀元前後の年、そしてグレゴリオ暦の変更といった、YearDay()
メソッドが正しく機能するかを検証するための多様なエッジケースが含まれています。これにより、メソッドの堅牢性が保証されます。yearDayLocations
変数: 異なるタイムゾーンをテストするためのtime.Location
のスライスです。YearDay
の計算はタイムゾーンに依存しないはずですが、念のため異なるタイムゾーンでテストを実行することで、予期せぬタイムゾーン関連のバグがないことを確認しています。TestYearDay
関数:YearDay()
メソッドの単体テストを実行するGoのテスト関数です。 この関数は二重ループ構造になっており、外側のループで異なるタイムゾーンを、内側のループでyearDayTests
に定義された各日付を処理します。 各テストケースでは、time.Date()
関数を使用して指定された日付とタイムゾーンでtime.Time
オブジェクトを作成し、そのオブジェクトに対してYearDay()
メソッドを呼び出します。 結果として得られたyday
と、テストデータで定義された期待値ydt.yday
を比較します。もし両者が一致しない場合、t.Errorf()
を使用してエラーメッセージを出力します。このエラーメッセージには、どのテストケースで失敗したか(日付、タイムゾーン)、期待値、実際の値が含まれており、デバッグを容易にします。 このテストの網羅性は、YearDay()
メソッドが様々な条件下で正確な結果を返すことを保証するために非常に重要です。
関連リンク
- Go Issue #3932: https://github.com/golang/go/issues/3932
- Go CL 6460069: https://golang.org/cl/6460069
参考にした情報源リンク
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEdFIyl-Tg2qPp-ARBQarEXDpSIQ-ot7Fzjihw8ubvvxz0Z0NHr4hAUpoxnhuj_3Yz1VWqkw7ittlbxeNwdcJLSU4sZGcYI6aP-iJHPi_6lgLxfmj0E8ViSQORzySVhkMcCZcg= (Go issue 3932に関するWeb検索結果)
- Go言語公式ドキュメント
time
パッケージ: https://pkg.go.dev/time (一般的なGoのtimeパッケージに関する情報) - グレゴリオ暦 - Wikipedia: https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%AC%E3%82%B4%E3%83%AA%E3%82%AA%E6%9A%A6 (閏年に関する情報)