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

[インデックス 10809] ファイルの概要

このコミットは、Go言語の標準ライブラリである time パッケージに AddDate メソッドを新しく追加するものです。AddDate メソッドは、指定された年、月、日のオフセットを既存の Time オブジェクトに加算し、新しい Time オブジェクトを返します。これにより、日付の加算・減算が容易になります。

コミット

commit cebf55dc9b2bf6b298f60cf3bffb4ad7a4583f05
Author: Roger Peppe <rogpeppe@gmail.com>
Date:   Thu Dec 15 11:23:01 2011 -0500

    time: new AddDate method
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5465044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/cebf55dc9b2bf6b298f60cf3bffb4ad7a4583f05

元コミット内容

time パッケージに AddDate という新しいメソッドを追加します。このメソッドは、指定された年数、月数、日数を既存の Time オオブジェクトに加算する機能を提供します。

変更の背景

Go言語の time パッケージは、日付と時刻を扱うための基本的な機能を提供しますが、特定の日付オフセット(年、月、日)を加算する直接的なメソッドは存在しませんでした。既存の Add メソッドは Duration 型を受け取るため、秒単位での加算は可能でしたが、月や年といった可変長の時間単位での加算は複雑でした。

例えば、「今日から3ヶ月後」や「5年前の今日」といった計算を行う場合、開発者はうるう年や各月の異なる日数などを考慮して手動で計算する必要がありました。これはエラーの温床となりやすく、コードの可読性も損なう可能性がありました。

AddDate メソッドの導入により、このような日付の加算・減算が直感的かつ正確に行えるようになり、開発者の負担が軽減され、time パッケージの利便性が向上しました。特に、日付の「正規化」を内部で処理することで、例えば10月31日に1ヶ月を加算すると12月1日になる(11月31日という存在しない日付を正規化する)といった、日付計算における一般的な課題を解決しています。

前提知識の解説

Go言語の time パッケージ

Go言語の time パッケージは、日付と時刻を扱うための機能を提供します。主要な型として TimeDuration があります。

  • time.Time 構造体: 特定の時点を表します。内部的には、エポック(1970年1月1日UTC)からの経過秒数とナノ秒数、およびタイムゾーン情報を持っています。
  • time.Duration: 期間を表します。ナノ秒単位で表現され、time.Second, time.Minute, time.Hour などの定数を使って期間を表現できます。
  • Time.Date() メソッド: Time オブジェクトから年、月、日を返します。
  • Time.Clock() メソッド: Time オブジェクトから時、分、秒を返します。
  • time.Date() 関数: 指定された年、月、日、時、分、秒、ナノ秒、ロケーション(タイムゾーン)から新しい Time オブジェクトを作成します。この関数は、日付の「正規化」を行います。例えば、time.Date(2023, time.February, 30, ...) のように存在しない日付を指定した場合、自動的に3月2日などに調整されます。
  • Time.Location() メソッド: Time オブジェクトのタイムゾーン情報を返します。

日付の正規化 (Normalization)

日付の正規化とは、存在しない日付(例: 2月30日、11月31日)や、月の範囲を超える日付(例: 1月32日)が指定された場合に、それを有効な日付に自動的に調整するプロセスを指します。

例えば、time.Date 関数において:

  • time.Date(2023, time.February, 30, ...) は、2023年2月28日の2日後として、2023年3月2日に正規化されます。
  • time.Date(2023, time.October, 31, ...) に1ヶ月を加算する場合、単純に月を11にするだけでは11月31日となり存在しません。この場合、AddDatetime.Date の正規化ロジックを利用して、11月30日の翌日である12月1日に正規化します。

この正規化の挙動は、日付計算の正確性と堅牢性を保証するために非常に重要です。

技術的詳細

AddDate メソッドは Time 型のレシーバメソッドとして定義されています。その実装は非常にシンプルでありながら、Go言語の time パッケージが提供する既存の強力な機能を活用しています。

AddDate の内部ロジックは以下のステップで構成されます。

  1. 現在の年月日と時刻の取得: t.Date() を呼び出して、現在の Time オブジェクト t から年 (year)、月 (month)、日 (day) を取得します。 t.Clock() を呼び出して、現在の Time オブジェクト t から時 (hour)、分 (min)、秒 (sec) を取得します。 ナノ秒 (t.nsec) とロケーション (t.loc) も直接利用します。

  2. オフセットの加算: 取得した年、月、日に、引数として渡された yearsmonthsdays をそれぞれ加算します。

    • year + years
    • month + Month(months) (ここで monthstime.Month 型にキャストされます)
    • day + days
  3. 新しい Time オブジェクトの生成と正規化: 加算された年、月、日、そして元の時、分、秒、ナノ秒、ロケーションを引数として time.Date() 関数を呼び出します。 time.Date() 関数は、内部的に日付の正規化ロジックを含んでいます。これにより、例えば「10月31日に1ヶ月を加える」といった場合に、自動的に「12月1日」として正しい日付が計算されます。これは、time.Date が与えられた年、月、日を基に、その月の最終日を超えた分を翌月に繰り越す(または前月に繰り下げる)ためです。

このアプローチにより、AddDate メソッドは複雑な日付計算ロジックを自身で持つことなく、time.Date 関数の堅牢な正規化機能を再利用しています。

コアとなるコードの変更箇所

src/pkg/time/time.go

 // AddDate returns the time corresponding to adding the
 // given number of years, months, and days to t.
 // For example, AddDate(-1, 2, 3) applied to January 1, 2011
 // returns March 4, 2010.
 //
 // AddDate normalizes its result in the same way that Date does,
 // so, for example, adding one month to October 31 yields
 // December 1, the normalized form for November 31.
 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.loc)
 }

src/pkg/time/time_test.go

 // Several ways of getting from
 // Fri Nov 18 7:56:35 PST 2011
 // to
 // Thu Mar 19 7:56:35 PST 2016
 var addDateTests = []struct {
 	years, months, days int
 }{
 	{4, 4, 1},
 	{3, 16, 1},
 	{3, 15, 30},
 	{5, -6, -18 - 30 - 12},
 }
 
 func TestAddDate(t *testing.T) {
 	t0 := Date(2011, 11, 18, 7, 56, 35, 0, UTC)
 	t1 := Date(2016, 3, 19, 7, 56, 35, 0, UTC)
 	for _, at := range addDateTests {
 		time := t0.AddDate(at.years, at.months, at.days)
 		if !time.Equal(t1) {
 			t.Errorf("AddDate(%d, %d, %d) = %v, want %v",
 				at.years, at.months, at.days,
 				time, t1)
 		}
 	}
 }

コアとなるコードの解説

AddDate メソッド (src/pkg/time/time.go)

  • func (t Time) AddDate(years int, months int, days int) Time: Time 型に AddDate というメソッドを追加しています。このメソッドは years (年数)、months (月数)、days (日数) の3つの整数を引数として受け取り、新しい Time オブジェクトを返します。レシーバ t は元の Time オブジェクトです。

  • year, month, day := t.Date(): レシーバ tDate() メソッドを呼び出し、現在の年、月、日を取得します。

  • hour, min, sec := t.Clock(): レシーバ tClock() メソッドを呼び出し、現在の時、分、秒を取得します。

  • return Date(year+years, month+Month(months), day+days, hour, min, sec, int(t.nsec), t.loc): time.Date() 関数を呼び出して新しい Time オブジェクトを構築し、それを返します。

    • year+years: 現在の年に指定された年数を加算します。
    • month+Month(months): 現在の月に指定された月数を加算します。monthsint 型なので、time.Month 型にキャストしています。
    • day+days: 現在の日に指定された日数を加算します。
    • hour, min, sec: 元の時刻の時、分、秒をそのまま使用します。AddDate は日付の加算のみを行い、時刻は変更しません。
    • int(t.nsec): 元の時刻のナノ秒をそのまま使用します。
    • t.loc: 元の時刻のロケーション(タイムゾーン)をそのまま使用します。

    この time.Date() 関数への呼び出しが、日付の正規化を自動的に処理する重要な部分です。例えば、day+days の結果がその月の最大日数を超えた場合、time.Date() は自動的に月を繰り上げ、正しい日付を計算します。

TestAddDate 関数 (src/pkg/time/time_test.go)

  • var addDateTests = []struct { ... }: AddDate メソッドのテストケースを定義するための匿名構造体のスライスです。各要素は years, months, days の組み合わせを表します。

  • func TestAddDate(t *testing.T): Goのテストフレームワークによって実行されるテスト関数です。

  • t0 := Date(2011, 11, 18, 7, 56, 35, 0, UTC): テストの基準となる開始日時 t0 を作成します。これは「2011年11月18日 7時56分35秒 UTC」です。

  • t1 := Date(2016, 3, 19, 7, 56, 35, 0, UTC): t0 に様々なオフセットを加算した結果、最終的に到達することを期待する目標日時 t1 を作成します。これは「2016年3月19日 7時56分35秒 UTC」です。

  • for _, at := range addDateTests { ... }: addDateTests スライスをループし、各テストケースを実行します。

  • time := t0.AddDate(at.years, at.months, at.days): 基準日時 t0 に対して、現在のテストケース (at) で指定された年、月、日を加算し、結果を time 変数に格納します。

  • if !time.Equal(t1) { ... }: 計算された time が期待される目標日時 t1 と等しいかどうかを Equal メソッドで比較します。Equal メソッドは、Time オブジェクトが同じ時点を表しているかどうかを比較します。

  • t.Errorf(...): もし計算結果が期待値と異なる場合、t.Errorf を呼び出してテストエラーを報告します。これにより、どの入力でどのような結果が得られ、何が期待されていたかが詳細に表示されます。

このテストは、異なる年、月、日の組み合わせ(正の値、負の値、大きな値)を与えても、AddDate が正しく日付を計算し、特に日付の正規化が期待通りに行われることを検証しています。例えば、{3, 16, 1} は3年と16ヶ月と1日を加算するケースですが、16ヶ月は1年4ヶ月に正規化され、最終的に正しい日付に到達することを確認しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • コミットメッセージ内の Go CL (Code Review) リンク: https://golang.org/cl/5465044
  • Go言語の time パッケージに関する一般的な情報源 (例: Go by Example: Time)