[インデックス 17022] ファイルの概要
このコミットは、Go言語のtime
パッケージにおける日付と時刻のフォーマット処理に関するバグ修正と改善を目的としています。具体的には、月名や曜日名の短縮形(例: "Jan", "Mon")が、その直後に小文字が続く文字列と誤ってマッチしてしまう問題を解決します。これにより、「Janet」のような文字列が「Januaryet」と誤認識されるといった、意図しないマッチングを防ぎます。
コミット
commit af8426eebe6c1681f4a3e7a6619f9e3abe9704e8
Author: Rob Pike <r@golang.org>
Date: Mon Aug 5 10:53:46 2013 +1000
time: match month and day names only when not followed immediately by a lower-case letter
Avoids seeing "Janet" as "Januaryet".
Fixes #6020.
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/12448044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/af8426eebe6c1681f4a3e7a6619f9e3abe9704e8
元コミット内容
time: match month and day names only when not followed immediately by a lower-case letter
Avoids seeing "Janet" as "Januaryet".
Fixes #6020.
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/12448044
変更の背景
この変更は、Go言語のtime
パッケージが日付や時刻の文字列を解析する際に発生していた、特定の誤認識バグ(Issue #6020)を修正するために導入されました。
従来のtime
パッケージのフォーマットパーサーは、月名や曜日名の短縮形(例: "Jan" for January, "Mon" for Monday)を識別する際に、その文字列の直後に続く文字の種別を十分に考慮していませんでした。このため、例えば「Janet」という文字列があった場合、「Jan」が「January」の短縮形として誤って認識され、残りの「et」が続くことで「Januaryet」のような不正確な解析結果を生み出す可能性がありました。
このような誤認識は、特にユーザーが入力した自由形式のテキストや、特定の命名規則を持つデータソースから日付情報を抽出する際に問題となります。正確な日付・時刻の解析は、アプリケーションのロババスト性と信頼性にとって不可欠であるため、この問題は優先的に対処されるべき課題でした。
このコミットは、このような誤認識を防ぐために、月名や曜日名の短縮形をマッチングする際に、その直後の文字が小文字でないことを確認するロジックを追加することで、パーサーの精度を向上させています。
前提知識の解説
このコミットを理解するためには、以下のGo言語のtime
パッケージに関する知識が役立ちます。
time
パッケージ: Go言語の標準ライブラリの一部で、日付と時刻の操作、フォーマット、解析を提供します。- レイアウト文字列 (Layout String):
time
パッケージでは、日付と時刻のフォーマットや解析のパターンを定義するために、特定の参照時刻(Mon Jan 2 15:04:05 MST 2006
)に基づいたレイアウト文字列を使用します。例えば、"Jan"
は月名の短縮形(例: "Jan", "Feb")を表し、"Mon"
は曜日名の短縮形(例: "Mon", "Tue")を表します。 time.Parse
関数: 指定されたレイアウトと文字列からtime.Time
オブジェクトを解析する関数です。time.Format
関数:time.Time
オブジェクトを指定されたレイアウトに従って文字列にフォーマットする関数です。stdMonth
,stdWeekDay
:time
パッケージ内部で定義されている定数で、それぞれ月名の短縮形と曜日名の短縮形を表すレイアウト要素に対応します。- パーサーの貪欲性 (Greedy Parsing): 一般的な正規表現や文字列パーサーでは、可能な限り長い文字列にマッチしようとする「貪欲性」という特性があります。この特性が、今回の問題のように意図しない部分文字列のマッチングを引き起こすことがあります。
このコミットの核心は、time
パッケージの内部的なパーシングロジック、特にnextStdChunk
関数がどのようにレイアウト文字列を解析し、標準的な日付/時刻要素を識別しているかという点にあります。
技術的詳細
このコミットの技術的な核心は、time
パッケージの内部パーサーが、月名や曜日名の短縮形を識別する際に、その直後の文字が小文字であるかどうかをチェックする新しいロジックを導入した点にあります。
具体的には、以下の変更が行われました。
-
startsWithLowerCase
関数の追加: この新しいヘルパー関数は、与えられた文字列の最初の文字が小文字('a'から'z')であるかどうかを判定します。これは、月名や曜日名の短縮形(例: "Jan", "Mon")が、その直後に続く小文字によって誤って拡張されるのを防ぐために使用されます。func startsWithLowerCase(str string) bool { if len(str) == 0 { return false } c := str[0] return 'a' <= c && c <= 'z' }
-
nextStdChunk
関数におけるstartsWithLowerCase
の利用:nextStdChunk
関数は、レイアウト文字列から標準的な日付/時刻要素(例: 月、日、曜日など)を識別する内部関数です。この関数内で、月名の短縮形(stdMonth
)と曜日名の短縮形(stdWeekDay
)をマッチングするロジックに、startsWithLowerCase
関数によるチェックが追加されました。-
月名(
stdMonth
)の処理: 以前は、レイアウト文字列が"Jan"で始まる場合、無条件にstdMonth
としてマッチングしていました。変更後は、layout[i+3:]
("Jan"の直後から始まる文字列)が小文字で始まらない場合にのみstdMonth
としてマッチングするようになりました。これにより、「Janet」のようなケースで「Jan」が月名として誤認識されることを防ぎます。 -
曜日名(
stdWeekDay
)の処理: 同様に、以前は"Mon"で始まる場合、無条件にstdWeekDay
としてマッチングしていました。変更後は、layout[i+3:]
("Mon"の直後から始まる文字列)が小文字で始まらない場合にのみstdWeekDay
としてマッチングするようになりました。
-
これらの変更により、パーサーはより厳密なマッチングを行うようになり、"Janet"が"Januaryet"と誤認識されるような、意図しない部分文字列のマッチングが回避されます。これは、パーサーのロバスト性を高め、より正確な日付・時刻の解析を保証します。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、src/pkg/time/format.go
ファイルとsrc/pkg/time/time_test.go
ファイルに集中しています。
src/pkg/time/format.go
startsWithLowerCase
関数の追加:@@ -102,6 +102,16 @@ const ( // std0x records the std values for "01", "02", ..., "06". var std0x = [...]int{stdZeroMonth, stdZeroDay, stdZeroHour12, stdZeroMinute, stdZeroSecond, stdYear} +// startsWithLowerCase reports whether the the string has a lower-case letter at the beginning. +// Its purpose is to prevent matching strings like "Month" when looking for "Mon". +func startsWithLowerCase(str string) bool { + if len(str) == 0 { + return false + } + c := str[0] + return 'a' <= c && c <= 'z' +} + // nextStdChunk finds the first occurrence of a std string in // layout and returns the text before, the std string, and the text after. func nextStdChunk(layout string) (prefix string, std int, suffix string) { @@ -112,7 +122,9 @@ func nextStdChunk(layout string) (prefix string, std int, suffix string) { if len(layout) >= i+7 && layout[i:i+7] == "January" { return layout[0:i], stdLongMonth, layout[i+7:] } - return layout[0:i], stdMonth, layout[i+3:] + if !startsWithLowerCase(layout[i+3:]) { + return layout[0:i], stdMonth, layout[i+3:] + } } case 'M': // Monday, Mon, MST @@ -121,7 +133,9 @@ func nextStdChunk(layout string) (prefix string, std int, suffix string) { if len(layout) >= i+6 && layout[i:i+6] == "Monday" { return layout[0:i], stdLongWeekDay, layout[i+6:] } - return layout[0:i], stdWeekDay, layout[i+3:] + if !startsWithLowerCase(layout[i+3:]) { + return layout[0:i], stdWeekDay, layout[i+3:] + } } if layout[i:i+3] == "MST" { return layout[0:i], stdTZ, layout[i+3:]
src/pkg/time/time_test.go
-
新しいテストケースの追加:
formatTests
とparseTests
に、月名や曜日名の短縮形が小文字に続く場合に誤認識されないことを確認するテストケースが追加されました。@@ -413,6 +413,8 @@ var formatTests = []FormatTest{ {"am/pm", "3pm", "9pm"}, {"AM/PM", "3PM", "9PM"}, {"two-digit year", "06 01 02", "09 02 04"}, + // Three-letter months and days must not be followed by lower-case letter. + {"Janet", "Hi Janet, the Month is January", "Hi Janet, the Month is February"}, // Time stamps, Fractional seconds. {"Stamp", Stamp, "Feb 4 21:00:57"}, {"StampMilli", StampMilli, "Feb 4 21:00:57.012"}, @@ -505,6 +507,8 @@ var parseTests = []ParseTest{ // Leading zeros in other places should not be taken as fractional seconds. {"zero1", "2006.01.02.15.04.05.0", "2010.02.04.21.00.57.0", false, false, 1, 1}, {"zero2", "2006.01.02.15.04.05.00", "2010.02.04.21.00.57.01", false, false, 1, 2}, + // Month and day names only match when not followed by a lower-case letter. + {"Janet", "Hi Janet, the Month is January: Jan _2 15:04:05 2006", "Hi Janet, the Month is February: Feb 4 21:00:57 2010", false, true, 1, 0}, // Accept any number of fractional second digits (including none) for .999... // In Go 1, .999... was completely ignored in the format, meaning the first two
コアとなるコードの解説
このコミットの主要な変更は、time
パッケージのformat.go
ファイルに新しいヘルパー関数startsWithLowerCase
が追加され、既存のnextStdChunk
関数内でその関数が利用されるようになった点です。
startsWithLowerCase
関数
func startsWithLowerCase(str string) bool {
if len(str) == 0 {
return false
}
c := str[0]
return 'a' <= c && c <= 'z'
}
この関数は非常にシンプルで、与えられた文字列str
が空でない場合、その最初の文字c
が小文字('a'から'z'の範囲)であるかどうかを判定します。この関数の目的は、月名や曜日名の短縮形(例: "Jan", "Mon")が、その直後に続く小文字によって誤って「Month」のような単語の一部として認識されるのを防ぐことです。
nextStdChunk
関数における変更
nextStdChunk
関数は、time
パッケージの内部で、日付/時刻のレイアウト文字列を解析し、"Jan"や"Mon"のような標準的な要素を識別するために使用されます。この関数内のcase 'J'
(January/Janの処理)とcase 'M'
(Monday/Mon/MSTの処理)のブロックに、startsWithLowerCase
関数を用いた条件が追加されました。
変更前:
// 月名の短縮形 "Jan" のマッチング
return layout[0:i], stdMonth, layout[i+3:]
// 曜日名の短縮形 "Mon" のマッチング
return layout[0:i], stdWeekDay, layout[i+3:]
変更前は、"Jan"や"Mon"という文字列がレイアウト中に見つかると、その直後の文字が何であっても、無条件にそれぞれstdMonth
やstdWeekDay
としてマッチングしていました。これが、「Janet」のような文字列で「Jan」が月名として誤認識される原因となっていました。
変更後:
// 月名の短縮形 "Jan" のマッチング
if !startsWithLowerCase(layout[i+3:]) {
return layout[0:i], stdMonth, layout[i+3:]
}
// 曜日名の短縮形 "Mon" のマッチング
if !startsWithLowerCase(layout[i+3:]) {
return layout[0:i], stdWeekDay, layout[i+3:]
}
変更後では、layout[i+3:]
(つまり、マッチングしようとしている"Jan"や"Mon"の直後から始まる文字列)の最初の文字が小文字でない場合にのみ、stdMonth
やstdWeekDay
としてマッチングするようになりました。
例えば、入力文字列が「Janet」で、パーサーが「Jan」を検出した場合、layout[i+3:]
は「et」となります。startsWithLowerCase("et")
はtrue
を返すため、この条件!startsWithLowerCase(...)
はfalse
となり、「Jan」はstdMonth
としてマッチングされなくなります。これにより、「Janet」が「Januaryet」と誤認識されることが防がれます。
この修正は、パーサーの「貪欲性」を適切に制御し、より正確な文字列解析を可能にすることで、time
パッケージの堅牢性を向上させています。
関連リンク
- Go GitHub Commit: https://github.com/golang/go/commit/af8426eebe6c1681f4a3e7a6619f9e3abe9704e8
- Go CL (Code Review): https://golang.org/cl/12448044
- Go Issue #6020: https://github.com/golang/go/issues/6020
参考にした情報源リンク
- Go GitHub Repository: https://github.com/golang/go
- Go
time
package documentation: https://pkg.go.dev/time - Go
time
package layout strings: https://pkg.go.dev/time#pkg-constants