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

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

このコミットは、Go言語の標準ライブラリである time パッケージにおける Time.Add メソッドのバグ修正と、それに関連するテストケースの追加に関するものです。具体的には、ナノ秒の計算において、ちょうど1秒になる境界条件での丸め処理が正しく行われるように修正されています。

コミット

commit fdb09d289a149214caf4afb82f5b9280c7ca59cb
Author: Hector Chu <hectorchu@gmail.com>
Date:   Sat Dec 10 21:55:38 2011 +0000

    time: fix Time.Add
    
    R=rsc, r
    CC=golang-dev
    https://golang.org/cl/5448121

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

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

元コミット内容

このコミットの目的は、time パッケージの Time.Add メソッドにおけるバグを修正することです。このバグは、Time オブジェクトに Duration を加算する際に、ナノ秒部分がちょうど1秒(1e9ナノ秒)になった場合に、秒への繰り上げ処理が正しく行われないというものでした。修正は、ナノ秒のチェック条件を > 1e9 から >= 1e9 に変更することで行われ、これにより境界条件が適切に処理されるようになりました。また、この修正を検証するための新しいテストケース TestAddToExactSecond が追加されています。

変更の背景

Go言語の time パッケージは、日付と時刻を扱うための基本的な機能を提供します。Time 型は特定の時点を表し、Duration 型は時間の長さを表します。Time.Add(d Duration) メソッドは、Time オブジェクトに Duration を加算し、新しい Time オブジェクトを返します。

このメソッドの実装では、秒とナノ秒のフィールドをそれぞれ更新し、ナノ秒が1秒(10億ナノ秒)を超えた場合に秒に繰り上げる、または負になった場合に秒から借りるという正規化処理が行われます。

元のコードでは、ナノ秒が 1e9 (10億) より大きい場合にのみ秒への繰り上げを行っていました。しかし、ナノ秒がちょうど 1e9 になった場合(例えば、999,999,999ナノ秒に1ナノ秒を加算して1,000,000,000ナノ秒、つまり1秒になった場合)には、この条件に合致せず、秒への繰り上げが行われませんでした。その結果、nsec フィールドが 1e9 のままとなり、Time オブジェクトの内部状態が不正になる可能性がありました。これは、Time 型の nsec フィールドが [0, 999999999] の範囲に正規化されるべきであるという設計上の不整合を引き起こします。

このバグは、特に時間の厳密な計算が求められるアプリケーションにおいて、予期せぬ結果や誤った時刻表現につながる可能性がありました。

前提知識の解説

Go言語の time パッケージ

Go言語の time パッケージは、日付と時刻の操作、測定、表示のための機能を提供します。主要な型は以下の通りです。

  • Time: 特定の時点(日付と時刻)を表します。内部的には、Unixエポック(1970年1月1日UTC)からの経過秒数とナノ秒数で表現されます。
  • Duration: 時間の長さを表します。int64 型でナノ秒単位の値を保持します。

Time.Add メソッド

func (t Time) Add(d Duration) Time は、Time オブジェクト tDuration d を加算した新しい Time オブジェクトを返します。このメソッドは、t の秒とナノ秒のフィールドを d に基づいて更新します。

時間の正規化(秒とナノ秒)

Time オブジェクトは、秒とナノ秒の組み合わせで時間を表現します。ナノ秒のフィールドは通常、0 から 999,999,999 の範囲に正規化されます。つまり、10億ナノ秒は1秒として秒のフィールドに繰り上げられます。この正規化は、時間の計算において正確性と一貫性を保つために重要です。

境界条件(Boundary Conditions)

ソフトウェア開発において、境界条件とは、入力値が有効範囲の端にある場合の条件を指します。例えば、数値の最大値や最小値、配列の最初や最後の要素、ループの開始や終了条件などがこれに該当します。境界条件の処理は、バグの温床となりやすく、特に「以上/以下」(>=, <=) と「より大きい/より小さい」(>, <) の使い分けは厳密な注意が必要です。今回のバグは、まさにこの境界条件の誤った処理に起因していました。

技術的詳細

Time.Add メソッドの内部実装は以下のようになっています。

func (t Time) Add(d Duration) Time {
	t.sec += int64(d / 1e9)  // Durationを秒に変換して加算
	t.nsec += int32(d % 1e9) // Durationの残りのナノ秒を加算

	// ナノ秒の正規化
	if t.nsec >= 1e9 { // 修正箇所: > から >= に変更
		t.sec++
		t.nsec -= 1e9
	} else if t.nsec < 0 {
		t.sec--
		t.nsec += 1e9
	}
	return t
}

元のコードでは、if t.nsec > 1e9 という条件でした。 ここで、t.nsec が例えば 999999999 で、d % 1e91 だった場合、t.nsec1000000000 (1e9) になります。 元の条件 t.nsec > 1e91000000000 > 1000000000 となり、これは false です。 したがって、秒への繰り上げ処理 t.sec++ とナノ秒の正規化 t.nsec -= 1e9 が実行されず、t.nsec1000000000 のままになってしまうというバグがありました。

修正後の条件 if t.nsec >= 1e91000000000 >= 1000000000 となり、これは true です。 これにより、秒への繰り上げとナノ秒の正規化が正しく行われ、t.nsec0 に、t.sec は1増加します。

追加されたテストケース TestAddToExactSecond は、この境界条件を明示的に検証しています。 t1 := Now() で現在の時刻を取得し、t2 := t1.Add(Second - Duration(t1.Nanosecond())) という計算を行っています。 Second - Duration(t1.Nanosecond()) は、現在の時刻のナノ秒部分をちょうど次の秒の開始点に合わせるための Duration を計算しています。例えば、現在の時刻が X秒 Yナノ秒 であれば、Second - Yナノ秒 を加算することで、結果の時刻のナノ秒部分が 0 になり、秒が1つ繰り上がることを期待します。 このテストは、t2.Nanosecond()0 になり、t2.Second()t1.Second() の次の秒になっていることを確認することで、Time.Add が境界条件で正しく動作するかを検証しています。

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

src/pkg/time/time.go ファイルの Time.Add メソッド内の条件式が変更されました。

--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -548,7 +548,7 @@ func (d Duration) Hours() float64 {
 func (t Time) Add(d Duration) Time {
 	t.sec += int64(d / 1e9)
 	t.nsec += int32(d % 1e9)
-	if t.nsec > 1e9 {
+	if t.nsec >= 1e9 {
 		t.sec++
 		t.nsec -= 1e9
 	} else if t.nsec < 0 {

また、src/pkg/time/time_test.go に新しいテストケースが追加されました。

--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -655,6 +655,17 @@ func TestDaysIn(t *testing.T) {
 	}
 }
 
+func TestAddToExactSecond(t *testing.T) {
+	// Add an amount to the current time to round it up to the next exact second.
+	// This test checks that the nsec field still lies within the range [0, 999999999].
+	t1 := Now()
+	t2 := t1.Add(Second - Duration(t1.Nanosecond()))
+	sec := (t1.Second() + 1) % 60
+	if t2.Second() != sec || t2.Nanosecond() != 0 {
+		t.Errorf("sec = %d, nsec = %d, want sec = %d, nsec = 0", t2.Second(), t2.Nanosecond(), sec)
+	}
+}
+
 func BenchmarkNow(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		Now()

コアとなるコードの解説

src/pkg/time/time.go の変更

Time.Add メソッドは、Time オブジェクトの内部表現である sec (秒) と nsec (ナノ秒) フィールドを更新します。 t.sec += int64(d / 1e9): 加算する Duration d を秒単位に変換し、既存の秒に加算します。 t.nsec += int32(d % 1e9): Duration d のナノ秒部分(1秒未満の端数)を既存のナノ秒に加算します。

問題のあった行は if t.nsec > 1e9 { でした。 この条件は、t.nsec1,000,000,000 (10億) を厳密に超える場合にのみ真となります。 しかし、t.nsec がちょうど 1,000,000,000 になった場合(例えば、999,999,9991 を加算した場合)、この条件は偽となり、秒への繰り上げ処理がスキップされていました。 修正後の if t.nsec >= 1e9 { は、t.nsec1,000,000,000 以上の場合に真となるため、ちょうど 1,000,000,000 になった場合でも正しく秒への繰り上げとナノ秒の正規化(t.nsec -= 1e9 により 0 になる)が行われるようになります。

src/pkg/time/time_test.go の追加テスト

TestAddToExactSecond 関数は、Time.Add メソッドが秒の境界条件で正しく動作することを保証するために追加されました。

  1. t1 := Now(): 現在の時刻を取得します。
  2. t2 := t1.Add(Second - Duration(t1.Nanosecond())):
    • t1.Nanosecond(): t1 のナノ秒部分を取得します。
    • Duration(t1.Nanosecond()): そのナノ秒部分を Duration 型に変換します。
    • Second - Duration(t1.Nanosecond()): これは、現在の時刻のナノ秒部分を打ち消し、ちょうど1秒を加算することで、結果の時刻のナノ秒部分が 0 になり、秒が繰り上がるような Duration を生成します。 例えば、t1HH:MM:SS.999999999 であれば、Second - 999999999ns1ns となり、t11ns を加算すると HH:MM:(SS+1).000000000 となることを期待します。
  3. sec := (t1.Second() + 1) % 60: 期待される秒の値を計算します。現在の秒に1を加え、60で割った余り(0-59)を取ります。
  4. if t2.Second() != sec || t2.Nanosecond() != 0 { ... }:
    • t2.Second() != sec: t2 の秒が期待される秒と異なる場合。
    • t2.Nanosecond() != 0: t2 のナノ秒が 0 でない場合(つまり、正規化が正しく行われなかった場合)。 これらの条件のいずれかが真であれば、テストは失敗し、エラーメッセージが出力されます。

このテストは、Time.Add がナノ秒の繰り上げ処理を正確に行い、nsec フィールドが常に [0, 999999999] の範囲に正規化されることを保証します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • ソフトウェア開発における境界条件テストに関する一般的な知識