Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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日とする年です。閏年の規則は以下の通りです。

  1. 西暦年が4で割り切れる年は閏年である。
  2. ただし、100で割り切れる年は閏年ではない。
  3. ただし、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.godate メソッドのコメントも、その機能が年の通算日 (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 メソッドの既存のコメントが更新され、このメソッドが yearday of year (年の通算日) を計算すること、そして full=true の場合にのみ monthday を計算することを明確にしています。これは、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() メソッドが様々な条件下で正確な結果を返すことを保証するために非常に重要です。

関連リンク

参考にした情報源リンク