[インデックス 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
オブジェクト t
に Duration
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 % 1e9
が 1
だった場合、t.nsec
は 1000000000
(1e9) になります。
元の条件 t.nsec > 1e9
は 1000000000 > 1000000000
となり、これは false
です。
したがって、秒への繰り上げ処理 t.sec++
とナノ秒の正規化 t.nsec -= 1e9
が実行されず、t.nsec
が 1000000000
のままになってしまうというバグがありました。
修正後の条件 if t.nsec >= 1e9
は 1000000000 >= 1000000000
となり、これは true
です。
これにより、秒への繰り上げとナノ秒の正規化が正しく行われ、t.nsec
は 0
に、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.nsec
が 1,000,000,000
(10億) を厳密に超える場合にのみ真となります。
しかし、t.nsec
がちょうど 1,000,000,000
になった場合(例えば、999,999,999
に 1
を加算した場合)、この条件は偽となり、秒への繰り上げ処理がスキップされていました。
修正後の if t.nsec >= 1e9 {
は、t.nsec
が 1,000,000,000
以上の場合に真となるため、ちょうど 1,000,000,000
になった場合でも正しく秒への繰り上げとナノ秒の正規化(t.nsec -= 1e9
により 0
になる)が行われるようになります。
src/pkg/time/time_test.go
の追加テスト
TestAddToExactSecond
関数は、Time.Add
メソッドが秒の境界条件で正しく動作することを保証するために追加されました。
t1 := Now()
: 現在の時刻を取得します。t2 := t1.Add(Second - Duration(t1.Nanosecond()))
:t1.Nanosecond()
:t1
のナノ秒部分を取得します。Duration(t1.Nanosecond())
: そのナノ秒部分をDuration
型に変換します。Second - Duration(t1.Nanosecond())
: これは、現在の時刻のナノ秒部分を打ち消し、ちょうど1秒を加算することで、結果の時刻のナノ秒部分が0
になり、秒が繰り上がるようなDuration
を生成します。 例えば、t1
がHH:MM:SS.999999999
であれば、Second - 999999999ns
は1ns
となり、t1
に1ns
を加算するとHH:MM:(SS+1).000000000
となることを期待します。
sec := (t1.Second() + 1) % 60
: 期待される秒の値を計算します。現在の秒に1を加え、60で割った余り(0-59)を取ります。if t2.Second() != sec || t2.Nanosecond() != 0 { ... }
:t2.Second() != sec
:t2
の秒が期待される秒と異なる場合。t2.Nanosecond() != 0
:t2
のナノ秒が0
でない場合(つまり、正規化が正しく行われなかった場合)。 これらの条件のいずれかが真であれば、テストは失敗し、エラーメッセージが出力されます。
このテストは、Time.Add
がナノ秒の繰り上げ処理を正確に行い、nsec
フィールドが常に [0, 999999999]
の範囲に正規化されることを保証します。
関連リンク
- Go言語
time
パッケージのドキュメント: https://pkg.go.dev/time - Go言語のソースコードリポジトリ: https://github.com/golang/go
- Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/5448121 (コミットメッセージに記載されているCLリンク)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- ソフトウェア開発における境界条件テストに関する一般的な知識