[インデックス 13279] ファイルの概要
このコミットは、Go言語の標準ライブラリtime
パッケージにおけるParse
関数の挙動を修正し、特に小数秒のパースに関する柔軟性を向上させるものです。具体的には、レイアウト文字列に.999
(ナノ秒までを許容する形式)が含まれる場合、入力文字列において小数秒が省略されている場合や、指定された桁数よりも多くの小数秒が与えられた場合でも、適切にパースできるように変更されています。これにより、以前はエラーとなっていたいくつかのケースが有効なパースとして扱われるようになります。
コミット
commit dcc46388df62fafe81426b8dd888ed786f7db9fa
Author: Russ Cox <rsc@golang.org>
Date: Mon Jun 4 13:09:19 2012 -0400
time: accept .999 in Parse
The recent shuffle in parsing formats exposed probably unintentional
behavior in time.Parse, namely that it was mostly ignoring ".99999"
in the format, producing the following behavior:
fmt.Println(time.Parse("03:04:05.999 MST", "12:00:00.888 PDT")) // error (.888 unexpected)
fmt.Println(time.Parse("03:04:05.999", "12:00:00")) // error (input too short)
fmt.Println(time.Parse("03:04:05.999 MST", "12:00:00 PDT")) // ok (extra bytes on input make it ok)
http://play.golang.org/p/ESJ1UYXzq2
API CHANGE:
This CL makes all three examples valid: ".999" can match an
empty string or else a fractional second with at most nine digits.
Fixes #3701.
R=r, r
CC=golang-dev
https://golang.org/cl/6267045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dcc46388df62fafe81426b8dd888ed786f7db9fa
元コミット内容
time: accept .999 in Parse
このコミットは、time.Parse
関数がレイアウト文字列中の.999
(小数秒を表すフォーマット)をより柔軟に解釈するように変更します。
最近のパースフォーマットの変更により、time.Parse
が.99999
のような小数秒指定をほとんど無視するという、おそらく意図しない挙動が露呈しました。これにより、以下のような問題が発生していました。
fmt.Println(time.Parse("03:04:05.999 MST", "12:00:00.888 PDT"))
// エラー(.888が予期しない)fmt.Println(time.Parse("03:04:05.999", "12:00:00"))
// エラー(入力が短すぎる)fmt.Println(time.Parse("03:04:05.999 MST", "12:00:00 PDT"))
// OK(入力に余分なバイトがあるためOK)
上記の挙動は、http://play.golang.org/p/ESJ1UYXzq2 で確認できます。
API変更点:
この変更により、上記の3つの例はすべて有効になります。.999
は空文字列、または最大9桁の小数秒にマッチするようになります。
この変更は、Issue #3701 を修正します。
変更の背景
Go言語のtime
パッケージは、日付と時刻の操作において非常に重要な役割を担っています。特にtime.Parse
関数は、特定のレイアウト文字列に基づいて文字列からtime.Time
オブジェクトを生成するために広く利用されます。この関数の正確性と柔軟性は、様々な形式のタイムスタンプを扱うアプリケーションにとって不可欠です。
このコミットが行われた背景には、time.Parse
関数における小数秒のパースに関する既存の挙動が、開発者の期待と異なる、あるいは直感的ではないという問題がありました。具体的には、レイアウト文字列で.999
(ナノ秒までを許容する形式)を指定した場合、入力文字列に小数秒が全く含まれない場合や、指定された.999
の桁数と入力の小数秒の桁数が一致しない場合に、パースエラーが発生するという問題です。
コミットメッセージに示されているように、以前のtime.Parse
の実装では、.999
のような小数秒のフォーマット指定が「ほとんど無視」されるような挙動をしていました。これは、例えば03:04:05.999
というレイアウトに対して12:00:00.888
という入力があった場合、.888
の部分が予期しないものとしてエラーになったり、12:00:00
のように小数秒が完全に省略された場合に「入力が短すぎる」としてエラーになったりする原因となっていました。一方で、12:00:00 PDT
のように、小数秒の後に別の要素(タイムゾーンなど)が続く場合は、なぜかパースが成功するという一貫性のない挙動も見られました。
このような挙動は、開発者が異なる形式のタイムスタンプを柔軟に扱いたい場合に、不必要なエラーハンドリングや入力文字列の事前処理を強いることになり、開発体験を損ねていました。この問題は、GoのIssue #3701として報告されており、このコミットはその問題を解決するために導入されました。目的は、time.Parse
が.999
フォーマットに対してより堅牢で直感的なパース挙動を提供することにありました。
前提知識の解説
このコミットの理解には、以下のGo言語のtime
パッケージに関する知識が不可欠です。
-
time.Parse
関数:func Parse(layout, value string) (Time, error)
この関数は、指定されたlayout
文字列の形式に従って、value
文字列をtime.Time
型にパースします。layout
文字列は、Go言語の特定の参照時刻(Mon Jan 2 15:04:05 MST 2006
)の各要素を対応する入力文字列の要素にマッピングすることで、パースのフォーマットを定義します。 -
レイアウト文字列における小数秒の表現:
time.Parse
のレイアウト文字列では、小数秒を表現するために以下の特別な値が使用されます。.000
: 秒の小数部をミリ秒(3桁)で表現します。入力文字列は厳密に3桁の小数秒を持つ必要があります。.999
: 秒の小数部をナノ秒(最大9桁)で表現します。このコミット以前は挙動に問題がありましたが、本来は小数秒が0桁から9桁までを許容する柔軟な形式を意図しています。.000000
: マイクロ秒(6桁).000000000
: ナノ秒(9桁) 同様に、.999999
や.999999999
も存在します。
-
stdFracSecond0
とstdFracSecond9
: Goのtime
パッケージの内部実装では、レイアウト文字列の各要素(年、月、日、時、分、秒、小数秒など)は、内部的な定数(std
で始まる)にマッピングされます。stdFracSecond0
:.000
のように、固定桁数の小数秒(0が続く)を表す内部定数です。stdFracSecond9
:.999
のように、可変桁数の小数秒(9が続く)を表す内部定数です。このコミットは、特にstdFracSecond9
の処理ロジックに焦点を当てています。
-
parseNanoseconds
関数: この内部関数は、入力文字列から小数秒の部分をパースし、ナノ秒単位の整数値に変換します。この関数は、パースする文字列の開始位置と、期待される最大桁数を引数として受け取ります。
これらの知識を前提として、コミットがtime.Parse
の内部ロジック、特に小数秒の処理をどのように変更したかを深く理解することができます。
技術的詳細
このコミットの技術的な核心は、src/pkg/time/format.go
内のParse
関数におけるstdFracSecond9
(レイアウト文字列中の.999
に対応)の処理ロジックの変更にあります。
変更前は、stdFracSecond0
とstdFracSecond9
の両方が、parseNanoseconds
関数を呼び出す前に、入力文字列から固定の桁数(1+ndigit
、ここでndigit
は.000
や.999
の0や9の数)を切り取っていました。これは、.999
が「最大9桁」を意味するにもかかわらず、固定桁数のパースを試みていたため、入力文字列の小数秒の桁数がレイアウトと一致しない場合に問題を引き起こしていました。
新しい実装では、stdFracSecond9
のケースが独立して処理されます。
-
小数秒の省略の許容:
if len(value) < 2 || value[0] != '.' || value[1] < '0' || '9' < value[1]
この条件は、入力文字列が小数秒の開始を示す.
を持たない場合、または.
の直後に数字が続かない場合に真となります。この場合、小数秒は「省略された」と判断され、break
によって現在のパースステップを終了し、次のレイアウト要素のパースに進みます。これにより、"12:00:00"
のような入力に対して、レイアウトが"03:04:05.999"
であってもエラーにならずにパースが成功するようになります。 -
可変桁数の小数秒のパース: 小数秒が存在する場合(
.
とそれに続く数字がある場合)、新しいロジックはfor
ループを使用して、入力文字列から可能な限り多くの数字(最大9桁)を読み取ります。for i < 9 && i+1 < len(value) && '0' <= value[i+1] && value[i+1] <= '9' { i++ }
このループは、.
の後の最初の数字から始まり、最大9桁まで、または数字以外の文字に遭遇するまで、数字をi
にカウントします。 その後、parseNanoseconds(value, 1+i)
が呼び出されます。ここで1+i
は、.
とそれに続く実際に読み取られた数字の合計桁数を示します。これにより、parseNanoseconds
は、レイアウトで指定された最大桁数ではなく、入力文字列に実際に存在する小数秒の桁数に基づいてパースを試みます。 最後に、value = value[1+i:]
によって、パースされた小数秒の部分が入力文字列から削除され、残りの文字列が次のレイアウト要素のパースに渡されます。
この変更により、time.Parse
は.999
フォーマットに対して、以下の柔軟な挙動を実現します。
- 小数秒が完全に省略されている場合(例:
12:00:00
)でもパース可能。 - 小数秒の桁数がレイアウトの
.999
の桁数(9桁)よりも少ない場合(例:12:00:00.888
)でもパース可能。 - 小数秒の桁数がレイアウトの
.999
の桁数よりも多い場合(例:12:00:00.1234567890
)でも、最初の9桁までをパースし、残りを無視してパース可能(これはstdSecond
ケースの挙動に合わせるためとコメントされている)。
テストケースの追加もこの変更の重要な部分です。src/pkg/time/time_test.go
に、.999
フォーマットが小数秒の有無や桁数に関わらず正しく動作することを確認するための新しいテストが追加されています。これにより、変更が意図した通りに機能し、将来のリグレッションを防ぐことが保証されます。
コアとなるコードの変更箇所
このコミットにおける主要なコードの変更は、src/pkg/time/format.go
ファイルのParse
関数内にあります。
--- a/src/pkg/time/format.go
+++ b/src/pkg/time/format.go
@@ -852,10 +852,24 @@ func Parse(layout, value string) (Time, error) {
// It's a valid format.
zoneName = p
- case stdFracSecond0, stdFracSecond9:
+ case stdFracSecond0:
ndigit := std >> stdArgShift
nsec, rangeErrString, err = parseNanoseconds(value, 1+ndigit)
value = value[1+ndigit:]
+
+ case stdFracSecond9:
+ if len(value) < 2 || value[0] != '.' || value[1] < '0' || '9' < value[1] {
+ // Fractional second omitted.
+ break
+ }
+ // Take any number of digits, even more than asked for,
+ // because it is what the stdSecond case would do.
+ i := 0
+ for i < 9 && i+1 < len(value) && '0' <= value[i+1] && value[i+1] <= '9' {
+ i++
+ }
+ nsec, rangeErrString, err = parseNanoseconds(value, 1+i)
+ value = value[1+i:]
}
if rangeErrString != "" {
return Time{}, &ParseError{alayout, avalue, stdstr, value, ": " + rangeErrString + " out of range"}
また、src/pkg/time/time_test.go
には、この変更の挙動を検証するための新しいテストケースが追加されています。
--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -324,6 +324,16 @@ var parseTests = []ParseTest{\n // Leading zeros in other places should not be taken as fractional seconds.\n {"zero1", "2006.01.02.15.04.05.0", "2010.02.04.21.00.57.0", false, false, 1, 1},\n {"zero2", "2006.01.02.15.04.05.00", "2010.02.04.21.00.57.01", false, false, 1, 2},\n+\n+\t// Accept any number of fractional second digits (including none) for .999...\n+\t// In Go 1, .999... was completely ignored in the format, meaning the first two\n+\t// cases would succeed, but the next four would not. Go 1.1 accepts all six.\n+\t{"", "2006-01-02 15:04:05.9999 -0700 MST", "2010-02-04 21:00:57 -0800 PST", true, false, 1, 0},\n+\t{"", "2006-01-02 15:04:05.999999999 -0700 MST", "2010-02-04 21:00:57 -0800 PST", true, false, 1, 0},\n+\t{"", "2006-01-02 15:04:05.9999 -0700 MST", "2010-02-04 21:00:57.0123 -0800 PST", true, false, 1, 4},\n+\t{"", "2006-01-02 15:04:05.999999999 -0700 MST", "2010-02-04 21:00:57.0123 -0800 PST", true, false, 1, 4},\n+\t{"", "2006-01-02 15:04:05.9999 -0700 MST", "2010-02-04 21:00:57.012345678 -0800 PST", true, false, 1, 9},\n+\t{"", "2006-01-02 15:04:05.999999999 -0700 MST", "2010-02-04 21:00:57.012345678 -0800 PST", true, false, 1, 9},\n }\n \n func TestParse(t *testing.T) {\n```
## コアとなるコードの解説
`src/pkg/time/format.go`の`Parse`関数内の変更は、`switch`文の`case`ブロックにあります。
**変更前:**
`case stdFracSecond0, stdFracSecond9:`
この行は、固定桁数の小数秒(`.000`など)と可変桁数の小数秒(`.999`など)の両方を同じロジックで処理していました。
`ndigit := std >> stdArgShift`
`nsec, rangeErrString, err = parseNanoseconds(value, 1+ndigit)`
`value = value[1+ndigit:]`
ここでは、`ndigit`(レイアウトで指定された小数秒の桁数)に基づいて、入力文字列から固定の長さ(`.`とそれに続く`ndigit`桁の数字)を切り取り、`parseNanoseconds`に渡していました。このアプローチは、`.999`のように「最大9桁」を意味するフォーマットに対して、入力がそれより短い場合や小数秒が省略されている場合に問題を引き起こしていました。
**変更後:**
`case stdFracSecond0:`
`stdFracSecond0`(固定桁数の小数秒)の処理は変更されず、以前と同じロジックが適用されます。これは、`.000`のようなフォーマットは厳密な桁数を期待するため、この挙動が適切だからです。
`case stdFracSecond9:`
`stdFracSecond9`(可変桁数の小数秒、`.999`に対応)の処理が完全に分離され、新しいロジックが導入されました。
1. **小数秒の省略チェック**:
```go
if len(value) < 2 || value[0] != '.' || value[1] < '0' || '9' < value[1] {
// Fractional second omitted.
break
}
```
この`if`文は、入力文字列`value`が小数秒の開始を示す`.`を持たないか、`.`の直後に数字が続かない場合に真となります。
* `len(value) < 2`: `.`と少なくとも1桁の数字がない場合。
* `value[0] != '.'`: 最初の文字が`.`ではない場合。
* `value[1] < '0' || '9' < value[1]`: 2番目の文字(`.`の次の文字)が数字ではない場合。
これらの条件のいずれかが満たされる場合、小数秒は入力文字列に存在しないと判断され、`break`によってこの`case`ブロックを抜けます。これにより、`"12:00:00"`のような入力に対して、レイアウトが`"03:04:05.999"`であってもエラーにならず、小数秒が0としてパースされるようになります。
2. **可変桁数の小数秒の読み取り**:
```go
// Take any number of digits, even more than asked for,
// because it is what the stdSecond case would do.
i := 0
for i < 9 && i+1 < len(value) && '0' <= value[i+1] && value[i+1] <= '9' {
i++
}
```
この`for`ループは、入力文字列`value`から`.`の後の数字を読み取ります。
* `i < 9`: 最大9桁まで読み取ります(ナノ秒の精度)。
* `i+1 < len(value)`: 入力文字列の範囲を超えないようにします。
* `'0' <= value[i+1] && value[i+1] <= '9'`: 現在の文字が数字であることを確認します。
ループが終了すると、`i`には実際に読み取られた小数秒の桁数が格納されます。このロジックにより、入力文字列の小数秒の桁数がレイアウトの`.999`の桁数(9桁)よりも少ない場合でも、存在する桁数だけを正確に読み取ることができます。また、コメントにあるように、`stdSecond`(秒全体をパースするケース)の挙動に合わせて、9桁を超える数字が入力された場合でも、最初の9桁までを読み取るようにしています。
3. **ナノ秒のパースと文字列の更新**:
```go
nsec, rangeErrString, err = parseNanoseconds(value, 1+i)
value = value[1+i:]
```
`parseNanoseconds`関数が呼び出され、`value`の現在の位置から`1+i`(`.`とそれに続く`i`桁の数字)の長さの文字列をパースしてナノ秒に変換します。
その後、`value`はパースされた小数秒の部分をスキップするように更新され、残りの文字列が次のレイアウト要素のパースに引き継がれます。
これらの変更により、`time.Parse`は`.999`フォーマットに対して、より柔軟で直感的な挙動を提供するようになり、様々な形式のタイムスタンプをより堅牢に処理できるようになりました。
## 関連リンク
* Go Issue #3701: `time.Parse` does not accept `.999` in format string for empty fractional seconds
* [https://github.com/golang/go/issues/3701](https://github.com/golang/go/issues/3701)
* Go CL 6267045: `time: accept .999 in Parse`
* [https://golang.org/cl/6267045](https://golang.org/cl/6267045)
* Go Playground Example (before fix):
* [http://play.golang.org/p/ESJ1UYXzq2](http://play.golang.org/p/ESJ1UYXzq2)
## 参考にした情報源リンク
* Go言語の公式ドキュメント: `time`パッケージ
* [https://pkg.go.dev/time](https://pkg.go.dev/time)
* Go言語の`time.Parse`関数のレイアウトに関する解説
* [https://pkg.go.dev/time#Parse](https://pkg.go.dev/time#Parse)
* Go言語のソースコード: `src/time/format.go`
* [https://github.com/golang/go/blob/master/src/time/format.go](https://github.com/golang/go/blob/master/src/time/format.go)
* Go言語のソースコード: `src/time/time_test.go`
* [https://github.com/golang/go/blob/master/src/time/time_test.go](https://github.com/golang/go/blob/master/src/time/time_test.go)
* Go言語のIssueトラッカー
* [https://github.com/golang/go/issues](https://github.com/golang/go/issues)
* Go言語のCode Reviewシステム (Gerrit)
* [https://go-review.googlesource.com/](https://go-review.googlesource.com/)
* Go言語の`time`パッケージの内部実装に関する一般的な情報源(例: ブログ記事、技術解説など)
* (特定のURLは挙げませんが、`Go time.Parse internal`などで検索すると関連情報が見つかります。)
* Go言語のリリースノート(Go 1.1の変更点としてこの修正が言及されている可能性があります)
* [https://golang.org/doc/go1.1](https://golang.org/doc/go1.1) (Go 1.1のリリースノートを確認すると、この変更がAPI変更として記載されていることが確認できます。)
# [インデックス 13279] ファイルの概要
このコミットは、Go言語の標準ライブラリ`time`パッケージにおける`Parse`関数の挙動を修正し、特に小数秒のパースに関する柔軟性を向上させるものです。具体的には、レイアウト文字列に`.999`(ナノ秒までを許容する形式)が含まれる場合、入力文字列において小数秒が省略されている場合や、指定された桁数よりも多くの小数秒が与えられた場合でも、適切にパースできるように変更されています。これにより、以前はエラーとなっていたいくつかのケースが有効なパースとして扱われるようになります。
## コミット
commit dcc46388df62fafe81426b8dd888ed786f7db9fa Author: Russ Cox rsc@golang.org Date: Mon Jun 4 13:09:19 2012 -0400
time: accept .999 in Parse
The recent shuffle in parsing formats exposed probably unintentional
behavior in time.Parse, namely that it was mostly ignoring ".99999"
in the format, producing the following behavior:
fmt.Println(time.Parse("03:04:05.999 MST", "12:00:00.888 PDT")) // error (.888 unexpected)
fmt.Println(time.Parse("03:04:05.999", "12:00:00")) // error (input too short)
fmt.Println(time.Parse("03:04:05.999 MST", "12:00:00 PDT")) // ok (extra bytes on input make it ok)
http://play.golang.org/p/ESJ1UYXzq2
API CHANGE:
This CL makes all three examples valid: ".999" can match an
empty string or else a fractional second with at most nine digits.
Fixes #3701.
R=r, r
CC=golang-dev
https://golang.org/cl/6267045
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/dcc46388df62fafe81426b8dd888ed786f7db9fa](https://github.com/golang/go/commit/dcc46388df62fafe81426b8dd888ed786f7db9fa)
## 元コミット内容
`time: accept .999 in Parse`
このコミットは、`time.Parse`関数がレイアウト文字列中の`.999`(小数秒を表すフォーマット)をより柔軟に解釈するように変更します。
最近のパースフォーマットの変更により、`time.Parse`が`.99999`のような小数秒指定をほとんど無視するという、おそらく意図しない挙動が露呈しました。これにより、以下のような問題が発生していました。
1. `fmt.Println(time.Parse("03:04:05.999 MST", "12:00:00.888 PDT"))` // エラー(.888が予期しない)
2. `fmt.Println(time.Parse("03:04:05.999", "12:00:00"))` // エラー(入力が短すぎる)
3. `fmt.Println(time.Parse("03:04:05.999 MST", "12:00:00 PDT"))` // OK(入力に余分なバイトがあるためOK)
上記の挙動は、[http://play.golang.org/p/ESJ1UYXzq2](http://play.golang.org/p/ESJ1UYXzq2) で確認できます。
**API変更点:**
この変更により、上記の3つの例はすべて有効になります。`.999`は空文字列、または最大9桁の小数秒にマッチするようになります。
この変更は、Issue #3701 を修正します。
## 変更の背景
Go言語の`time`パッケージは、日付と時刻の操作において非常に重要な役割を担っています。特に`time.Parse`関数は、特定のレイアウト文字列に基づいて文字列から`time.Time`オブジェクトを生成するために広く利用されます。この関数の正確性と柔軟性は、様々な形式のタイムスタンプを扱うアプリケーションにとって不可欠です。
このコミットが行われた背景には、`time.Parse`関数における小数秒のパースに関する既存の挙動が、開発者の期待と異なる、あるいは直感的ではないという問題がありました。具体的には、レイアウト文字列で`.999`(ナノ秒までを許容する形式)を指定した場合、入力文字列に小数秒が全く含まれない場合や、指定された`.999`の桁数と入力の小数秒の桁数が一致しない場合に、パースエラーが発生するという問題です。
コミットメッセージに示されているように、以前の`time.Parse`の実装では、`.999`のような小数秒のフォーマット指定が「ほとんど無視」されるような挙動をしていました。これは、例えば`03:04:05.999`というレイアウトに対して`12:00:00.888`という入力があった場合、`.888`の部分が予期しないものとしてエラーになったり、`12:00:00`のように小数秒が完全に省略された場合に「入力が短すぎる」としてエラーになったりする原因となっていました。一方で、`12:00:00 PDT`のように、小数秒の後に別の要素(タイムゾーンなど)が続く場合は、なぜかパースが成功するという一貫性のない挙動も見られました。
このような挙動は、開発者が異なる形式のタイムスタンプを柔軟に扱いたい場合に、不必要なエラーハンドリングや入力文字列の事前処理を強いることになり、開発体験を損ねていました。この問題は、GoのIssue #3701として報告されており、このコミットはその問題を解決するために導入されました。目的は、`time.Parse`が`.999`フォーマットに対してより堅牢で直感的なパース挙動を提供することにありました。
## 前提知識の解説
このコミットの理解には、以下のGo言語の`time`パッケージに関する知識が不可欠です。
1. **`time.Parse`関数**:
`func Parse(layout, value string) (Time, error)`
この関数は、指定された`layout`文字列の形式に従って、`value`文字列を`time.Time`型にパースします。`layout`文字列は、Go言語の特定の参照時刻(`Mon Jan 2 15:04:05 MST 2006`)の各要素を対応する入力文字列の要素にマッピングすることで、パースのフォーマットを定義します。
2. **レイアウト文字列における小数秒の表現**:
`time.Parse`のレイアウト文字列では、小数秒を表現するために以下の特別な値が使用されます。
* **`.000`**: 秒の小数部をミリ秒(3桁)で表現します。入力文字列は厳密に3桁の小数秒を持つ必要があります。
* **`.999`**: 秒の小数部をナノ秒(最大9桁)で表現します。このコミット以前は挙動に問題がありましたが、本来は小数秒が0桁から9桁までを許容する柔軟な形式を意図しています。
* **`.000000`**: マイクロ秒(6桁)
* **`.000000000`**: ナノ秒(9桁)
同様に、`.999999`や`.999999999`も存在します。
3. **`stdFracSecond0`と`stdFracSecond9`**:
Goの`time`パッケージの内部実装では、レイアウト文字列の各要素(年、月、日、時、分、秒、小数秒など)は、内部的な定数(`std`で始まる)にマッピングされます。
* `stdFracSecond0`: `.000`のように、固定桁数の小数秒(0が続く)を表す内部定数です。
* `stdFracSecond9`: `.999`のように、可変桁数の小数秒(9が続く)を表す内部定数です。このコミットは、特に`stdFracSecond9`の処理ロジックに焦点を当てています。
4. **`parseNanoseconds`関数**:
この内部関数は、入力文字列から小数秒の部分をパースし、ナノ秒単位の整数値に変換します。この関数は、パースする文字列の開始位置と、期待される最大桁数を引数として受け取ります。
これらの知識を前提として、コミットが`time.Parse`の内部ロジック、特に小数秒の処理をどのように変更したかを深く理解することができます。
## 技術的詳細
このコミットの技術的な核心は、`src/pkg/time/format.go`内の`Parse`関数における`stdFracSecond9`(レイアウト文字列中の`.999`に対応)の処理ロジックの変更にあります。
変更前は、`stdFracSecond0`と`stdFracSecond9`の両方が、`parseNanoseconds`関数を呼び出す前に、入力文字列から固定の桁数(`1+ndigit`、ここで`ndigit`は`.000`や`.999`の0や9の数)を切り取っていました。これは、`.999`が「最大9桁」を意味するにもかかわらず、固定桁数のパースを試みていたため、入力文字列の小数秒の桁数がレイアウトと一致しない場合に問題を引き起こしていました。
新しい実装では、`stdFracSecond9`のケースが独立して処理されます。
1. **小数秒の省略の許容**:
`if len(value) < 2 || value[0] != '.' || value[1] < '0' || '9' < value[1]`
この条件は、入力文字列が小数秒の開始を示す`.`を持たない場合、または`.`の直後に数字が続かない場合に真となります。この場合、小数秒は「省略された」と判断され、`break`によって現在のパースステップを終了し、次のレイアウト要素のパースに進みます。これにより、`"12:00:00"`のような入力に対して、レイアウトが`"03:04:05.999"`であってもエラーにならずにパースが成功するようになります。
2. **可変桁数の小数秒のパース**:
小数秒が存在する場合(`.`とそれに続く数字がある場合)、新しいロジックは`for`ループを使用して、入力文字列から可能な限り多くの数字(最大9桁)を読み取ります。
`for i < 9 && i+1 < len(value) && '0' <= value[i+1] && value[i+1] <= '9' { i++ }`
このループは、`.`の後の最初の数字から始まり、最大9桁まで、または数字以外の文字に遭遇するまで、数字を`i`にカウントします。
その後、`parseNanoseconds(value, 1+i)`が呼び出されます。ここで`1+i`は、`.`とそれに続く実際に読み取られた数字の合計桁数を示します。これにより、`parseNanoseconds`は、レイアウトで指定された最大桁数ではなく、入力文字列に実際に存在する小数秒の桁数に基づいてパースを試みます。
最後に、`value = value[1+i:]`によって、パースされた小数秒の部分が入力文字列から削除され、残りの文字列が次のレイアウト要素のパースに渡されます。
この変更により、`time.Parse`は`.999`フォーマットに対して、以下の柔軟な挙動を実現します。
* 小数秒が完全に省略されている場合(例: `12:00:00`)でもパース可能。
* 小数秒の桁数がレイアウトの`.999`の桁数(9桁)よりも少ない場合(例: `12:00:00.888`)でもパース可能。
* 小数秒の桁数がレイアウトの`.999`の桁数よりも多い場合(例: `12:00:00.1234567890`)でも、最初の9桁までをパースし、残りを無視してパース可能(これは`stdSecond`ケースの挙動に合わせるためとコメントされている)。
テストケースの追加もこの変更の重要な部分です。`src/pkg/time/time_test.go`に、`.999`フォーマットが小数秒の有無や桁数に関わらず正しく動作することを確認するための新しいテストが追加されています。これにより、変更が意図した通りに機能し、将来のリグレッションを防ぐことが保証されます。
## コアとなるコードの変更箇所
このコミットにおける主要なコードの変更は、`src/pkg/time/format.go` ファイルの`Parse`関数内にあります。
```diff
--- a/src/pkg/time/format.go
+++ b/src/pkg/time/format.go
@@ -852,10 +852,24 @@ func Parse(layout, value string) (Time, error) {
// It's a valid format.
zoneName = p
- case stdFracSecond0, stdFracSecond9:
+ case stdFracSecond0:
ndigit := std >> stdArgShift
nsec, rangeErrString, err = parseNanoseconds(value, 1+ndigit)
value = value[1+ndigit:]
+
+ case stdFracSecond9:
+ if len(value) < 2 || value[0] != '.' || value[1] < '0' || '9' < value[1] {
+ // Fractional second omitted.
+ break
+ }
+ // Take any number of digits, even more than asked for,
+ // because it is what the stdSecond case would do.
+ i := 0
+ for i < 9 && i+1 < len(value) && '0' <= value[i+1] && value[i+1] <= '9' {
+ i++
+ }
+ nsec, rangeErrString, err = parseNanoseconds(value, 1+i)
+ value = value[1+i:]
}
if rangeErrString != "" {
return Time{}, &ParseError{alayout, avalue, stdstr, value, ": " + rangeErrString + " out of range"}
また、src/pkg/time/time_test.go
には、この変更の挙動を検証するための新しいテストケースが追加されています。
--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -324,6 +324,16 @@ var parseTests = []ParseTest{\n // Leading zeros in other places should not be taken as fractional seconds.\n {"zero1", "2006.01.02.15.04.05.0", "2010.02.04.21.00.57.0", false, false, 1, 1},\n {"zero2", "2006.01.02.15.04.05.00", "2010.02.04.21.00.57.01", false, false, 1, 2},\n+\n+\t// Accept any number of fractional second digits (including none) for .999...\n+\t// In Go 1, .999... was completely ignored in the format, meaning the first two\n+\t// cases would succeed, but the next four would not. Go 1.1 accepts all six.\n+\t{"", "2006-01-02 15:04:05.9999 -0700 MST", "2010-02-04 21:00:57 -0800 PST", true, false, 1, 0},\n+\t{"", "2006-01-02 15:04:05.999999999 -0700 MST", "2010-02-04 21:00:57 -0800 PST", true, false, 1, 0},\n+\t{"", "2006-01-02 15:04:05.9999 -0700 MST", "2010-02-04 21:00:57.0123 -0800 PST", true, false, 1, 4},\n+\t{"", "2006-01-02 15:04:05.999999999 -0700 MST", "2010-02-04 21:00:57.0123 -0800 PST", true, false, 1, 4},\n+\t{"", "2006-01-02 15:04:05.9999 -0700 MST", "2010-02-04 21:00:57.012345678 -0800 PST", true, false, 1, 9},\n+\t{"", "2006-01-02 15:04:05.999999999 -0700 MST", "2010-02-04 21:00:57.012345678 -0800 PST", true, false, 1, 9},\n }\n \n func TestParse(t *testing.T) {\n```
## コアとなるコードの解説
`src/pkg/time/format.go`の`Parse`関数内の変更は、`switch`文の`case`ブロックにあります。
**変更前:**
`case stdFracSecond0, stdFracSecond9:`
この行は、固定桁数の小数秒(`.000`など)と可変桁数の小数秒(`.999`など)の両方を同じロジックで処理していました。
`ndigit := std >> stdArgShift`
`nsec, rangeErrString, err = parseNanoseconds(value, 1+ndigit)`
`value = value[1+ndigit:]`
ここでは、`ndigit`(レイアウトで指定された小数秒の桁数)に基づいて、入力文字列から固定の長さ(`.`とそれに続く`ndigit`桁の数字)を切り取り、`parseNanoseconds`に渡していました。このアプローチは、`.999`のように「最大9桁」を意味するフォーマットに対して、入力がそれより短い場合や小数秒が省略されている場合に問題を引き起こしていました。
**変更後:**
`case stdFracSecond0:`
`stdFracSecond0`(固定桁数の小数秒)の処理は変更されず、以前と同じロジックが適用されます。これは、`.000`のようなフォーマットは厳密な桁数を期待するため、この挙動が適切だからです。
`case stdFracSecond9:`
`stdFracSecond9`(可変桁数の小数秒、`.999`に対応)の処理が完全に分離され、新しいロジックが導入されました。
1. **小数秒の省略チェック**:
```go
if len(value) < 2 || value[0] != '.' || value[1] < '0' || '9' < value[1] {
// Fractional second omitted.
break
}
```
この`if`文は、入力文字列`value`が小数秒の開始を示す`.`を持たないか、`.`の直後に数字が続かない場合に真となります。
* `len(value) < 2`: `.`と少なくとも1桁の数字がない場合。
* `value[0] != '.'`: 最初の文字が`.`ではない場合。
* `value[1] < '0' || '9' < value[1]`: 2番目の文字(`.`の次の文字)が数字ではない場合。
これらの条件のいずれかが満たされる場合、小数秒は入力文字列に存在しないと判断され、`break`によってこの`case`ブロックを抜けます。これにより、`"12:00:00"`のような入力に対して、レイアウトが`"03:04:05.999"`であってもエラーにならず、小数秒が0としてパースされるようになります。
2. **可変桁数の小数秒の読み取り**:
```go
// Take any number of digits, even more than asked for,
// because it is what the stdSecond case would do.
i := 0
for i < 9 && i+1 < len(value) && '0' <= value[i+1] && value[i+1] <= '9' {
i++
}
```
この`for`ループは、入力文字列`value`から`.`の後の数字を読み取ります。
* `i < 9`: 最大9桁まで読み取ります(ナノ秒の精度)。
* `i+1 < len(value)`: 入力文字列の範囲を超えないようにします。
* `'0' <= value[i+1] && value[i+1] <= '9'`: 現在の文字が数字であることを確認します。
ループが終了すると、`i`には実際に読み取られた小数秒の桁数が格納されます。このロジックにより、入力文字列の小数秒の桁数がレイアウトの`.999`の桁数(9桁)よりも少ない場合でも、存在する桁数だけを正確に読み取ることができます。また、コメントにあるように、`stdSecond`(秒全体をパースするケース)の挙動に合わせて、9桁を超える数字が入力された場合でも、最初の9桁までを読み取るようにしています。
3. **ナノ秒のパースと文字列の更新**:
```go
nsec, rangeErrString, err = parseNanoseconds(value, 1+i)
value = value[1+i:]
```
`parseNanoseconds`関数が呼び出され、`value`の現在の位置から`1+i`(`.`とそれに続く`i`桁の数字)の長さの文字列をパースしてナノ秒に変換します。
その後、`value`はパースされた小数秒の部分をスキップするように更新され、残りの文字列が次のレイアウト要素のパースに引き継がれます。
これらの変更により、`time.Parse`は`.999`フォーマットに対して、より柔軟で直感的な挙動を提供するようになり、様々な形式のタイムスタンプをより堅牢に処理できるようになりました。
## 関連リンク
* Go Issue #3701: `time.Parse` does not accept `.999` in format string for empty fractional seconds
* [https://github.com/golang/go/issues/3701](https://github.com/golang/go/issues/3701)
* Go CL 6267045: `time: accept .999 in Parse`
* [https://golang.org/cl/6267045](https://golang.org/cl/6267045)
* Go Playground Example (before fix):
* [http://play.golang.org/p/ESJ1UYXzq2](http://play.golang.org/p/ESJ1UYXzq2)
## 参考にした情報源リンク
* Go言語の公式ドキュメント: `time`パッケージ
* [https://pkg.go.dev/time](https://pkg.go.dev/time)
* Go言語の`time.Parse`関数のレイアウトに関する解説
* [https://pkg.go.dev/time#Parse](https://pkg.go.dev/time#Parse)
* Go言語のソースコード: `src/time/format.go`
* [https://github.com/golang/go/blob/master/src/time/format.go](https://github.com/golang/go/blob/master/src/time/format.go)
* Go言語のソースコード: `src/time/time_test.go`
* [https://github.com/golang/go/blob/master/src/time/time_test.go](https://github.com/golang/go/blob/master/src/time/time_test.go)
* Go言語のIssueトラッカー
* [https://github.com/golang/go/issues](https://github.com/golang/go/issues)
* Go言語のCode Reviewシステム (Gerrit)
* [https://go-review.googlesource.com/](https://go-review.googlesource.com/)
* Go言語の`time`パッケージの内部実装に関する一般的な情報源(例: ブログ記事、技術解説など)
* (特定のURLは挙げませんが、`Go time.Parse internal`などで検索すると関連情報が見つかります。)
* Go言語のリリースノート(Go 1.1の変更点としてこの修正が言及されている可能性があります)
* [https://golang.org/doc/go1.1](https://golang.org/doc/go1.1) (Go 1.1のリリースノートを確認すると、この変更がAPI変更として記載されていることが確認できます。)