[インデックス 17256] ファイルの概要
このコミットは、Go言語の標準ライブラリtimeパッケージにおける時間帯(タイムゾーン)の解析機能を改善するものです。特に、"GMT-X"のようなオフセット付きGMT表記のタイムゾーンを正しく処理できるように拡張しています。
コミット
commit 727b2b6f7dbec2bed608e3c97129c3bb7bab0547
Author: Rob Pike <r@golang.org>
Date: Thu Aug 15 10:10:49 2013 +1000
time: handle GMT possibly with offset
Update #3790
Handle time zones like GMT-8.
The more general time zone-matching problem is not yet resolved.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12922043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/727b2b6f7dbec2bed608e3c97129c3bb7bab0547
元コミット内容
このコミットの元の内容は以下の通りです。
time: handle GMT possibly with offset
Update #3790
Handle time zones like GMT-8.
The more general time zone-matching problem is not yet resolved.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12922043
これは、timeパッケージがGMT(グリニッジ標準時)にオフセットが指定された形式(例: GMT-8)を処理できるようにするための変更であることを示しています。また、これはIssue 3790の更新であり、より一般的なタイムゾーンマッチングの問題はまだ解決されていないことも明記されています。
変更の背景
Go言語のtimeパッケージは、日付と時刻の操作、フォーマット、解析を扱うための重要な機能を提供します。しかし、このコミットが導入される前は、timeパッケージの解析機能が"GMT-X"や"GMT+X"のようなオフセット付きのGMTタイムゾーン文字列を正しく解釈できないという問題がありました。
具体的には、Goのtime.Parse関数は、特定のレイアウト文字列に基づいて時刻文字列を解析しますが、タイムゾーンの認識には限界がありました。Issue 3790("time: Parse doesn't handle GMT-8")で報告されたように、GMT-8のような形式のタイムゾーンを含む時刻文字列を解析しようとすると、エラーが発生するか、タイムゾーンが正しく認識されないという問題がありました。
この問題は、特に異なるタイムゾーン間で時刻データをやり取りするアプリケーションや、外部システムから取得した時刻文字列をGoで処理する必要がある場合に、互換性の問題を引き起こしていました。このコミットは、この特定のGMTオフセット形式のタイムゾーンをサポートすることで、timeパッケージの堅牢性と実用性を向上させることを目的としています。
前提知識の解説
このコミットの変更内容を理解するためには、以下の概念について理解しておく必要があります。
タイムゾーンとオフセット
- タイムゾーン(Time Zone): 地球上の特定の地域で共通して使用される標準時を指します。例えば、日本標準時(JST)、東部標準時(EST)などがあります。タイムゾーンは、夏時間(Daylight Saving Time, DST)の有無によって、同じ地域でも時期によってオフセットが変わることがあります。
- UTC (Coordinated Universal Time): 協定世界時。世界の標準時であり、原子時計に基づいて維持されます。タイムゾーンのオフセットは、UTCからの差分で表現されます。
- GMT (Greenwich Mean Time): グリニッジ標準時。かつては世界の標準時として広く使われていましたが、現在はUTCがその役割を担っています。しかし、多くのシステムや慣習では、UTCとほぼ同じ意味でGMTという用語が使われたり、
GMT+XやGMT-Xのような形式でタイムゾーンのオフセットを示すために使われたりします。 - オフセット(Offset): 特定のタイムゾーンがUTCからどれだけ進んでいるか、または遅れているかを示す時間差です。通常、時間と分で表現され、
+HHMMや-HHMM、あるいは+HHや-HHのような形式で示されます。例えば、GMT-8はUTCより8時間遅れていることを意味します。
Go言語のtimeパッケージ
Goのtimeパッケージは、時刻の表現、操作、フォーマット、解析のための豊富な機能を提供します。
time.Time構造体: 特定の時点を表すGoの基本的な型です。この構造体は、時刻の値だけでなく、その時刻がどのタイムゾーンに属しているか(Location)の情報も保持します。time.Location: タイムゾーン情報を表す型です。time.UTC(UTC)、time.Local(システムのローカルタイムゾーン)、またはtime.LoadLocationでロードされた特定のタイムゾーンなどがあります。time.FixedZone(name string, offset int): 指定された名前とUTCからの固定オフセット(秒単位)を持つLocationを返します。この関数は、夏時間などの変動がない固定オフセットのタイムゾーンを作成する際に使用されます。time.Parse(layout, value string) (Time, error): 指定されたlayout文字列に基づいてvalue文字列を解析し、time.Timeオブジェクトを返します。layout文字列は、Goの参照時刻(Mon Jan 2 15:04:05 MST 2006)の各要素に対応する形式で記述されます。time.Format(layout string) string:time.Timeオブジェクトを指定されたlayout文字列に基づいてフォーマットし、文字列を返します。
文字列解析の一般的なアプローチ
文字列から特定の情報を抽出する際には、以下のようなアプローチが取られます。
- プレフィックス/サフィックスチェック: 文字列が特定のパターンで始まるか、または終わるかをチェックします。
- 部分文字列の抽出: 文字列の一部を切り出します。
- 数値解析: 文字列内の数字を整数や浮動小数点数に変換します。Goでは
strconvパッケージがこれを提供しますが、timeパッケージ内では独自のatoi(ASCII to Integer)関数が使われることがあります。 - 正規表現: より複雑なパターンマッチングには正規表現が使用されますが、このコミットでは直接的な文字列操作と条件分岐が主に使用されています。
技術的詳細
このコミットは、主にsrc/pkg/time/format.goファイル内のparse関数とその補助関数に修正を加えています。目的は、GMT-Xのような形式のタイムゾーンを正しく解析し、対応する固定オフセットのLocationを作成することです。
atoi関数の変更
atoi関数は、文字列の先頭から整数を解析する内部ヘルパー関数です。
変更前:
if s != "" && s[0] == '-' {
neg = true
s = s[1:]
}
変更後:
if s != "" && (s[0] == '-' || s[0] == '+') {
neg = s[0] == '-'
s = s[1:]
}
この変更により、atoi関数は数値の前に+記号があっても正しく処理できるようになりました。これは、GMT+Xのような形式のオフセットを解析する際に必要となります。
parse関数のタイムゾーン解析ロジックの変更
parse関数は、時刻文字列を解析する主要な関数です。この関数内で、タイムゾーンの解析部分が大きく変更されました。
変更前は、タイムゾーン文字列がTで終わる3文字または4文字のアルファベット(例: PST, EST)を想定していました。しかし、GMT-8のような形式はこれに合致しませんでした。
変更後、parse関数は新しいヘルパー関数parseTimeZoneを呼び出すようになりました。
// 変更前 (抜粋)
// ...
// if len(value) >= 3 && value[2] == 'T' {
// p, value = value[0:3], value[3:]
// } else if len(value) >= 4 && value[3] == 'T' {
// p, value = value[0:4], value[4:]
// } else {
// err = errBad
// break
// }
// ...
// t.loc = FixedZone(zoneName, 0) // オフセットは常に0だった
// 変更後 (抜粋)
n, ok := parseTimeZone(value)
if !ok {
err = errBad
break
}
zoneName, value = value[:n], value[n:]
// ...
if len(zoneName) > 3 && zoneName[:3] == "GMT" {
offset, _ = atoi(zoneName[3:]) // Guaranteed OK by parseGMT.
offset *= 3600
}
t.loc = FixedZone(zoneName, offset)
この変更により、parse関数はparseTimeZoneから返された長さnに基づいてタイムゾーン名を抽出し、もしそのタイムゾーン名が"GMT"で始まる場合は、その後のオフセット部分をatoiで解析し、秒単位に変換してFixedZoneに渡すようになりました。
新しいヘルパー関数 parseTimeZone
parseTimeZone関数は、時刻文字列の先頭からタイムゾーン部分を解析し、その長さと成功/失敗を示すブール値を返します。
func parseTimeZone(value string) (length int, ok bool) {
if len(value) < 3 {
return 0, false
}
// GMT may have an offset.
if len(value) >= 3 && value[:3] == "GMT" {
length = parseGMT(value)
return length, true
}
// 従来の3文字/4文字のタイムゾーン (例: PST, EST) の処理
if len(value) >= 3 && value[2] == 'T' {
length = 3
} else if len(value) >= 4 && value[3] == 'T' {
length = 4
} else {
return 0, false
}
for i := 0; i < length; i++ {
if value[i] < 'A' || 'Z' < value[i] {
return 0, false
}
}
return length, true
}
この関数は、まず文字列が"GMT"で始まるかどうかをチェックします。もしそうであれば、parseGMT関数を呼び出してGMTオフセットの長さを取得します。そうでなければ、従来の3文字または4文字のアルファベットタイムゾーン(例: PST, EST)のロジックを適用します。
新しいヘルパー関数 parseGMT
parseGMT関数は、"GMT"で始まる文字列からオフセット部分を解析し、その全体の長さを返します。
func parseGMT(value string) int {
value = value[3:] // "GMT"部分をスキップ
if len(value) == 0 {
return 3 // "GMT"のみの場合
}
sign := value[0]
if sign != '-' && sign != '+' {
return 3 // "GMT"の後に符号がない場合
}
x, rem, err := leadingInt(value[1:]) // 符号の後の数値を解析
if err != nil {
return 3 // 数値解析に失敗した場合
}
if sign == '-' {
x = -x
}
// オフセットの範囲チェック (例: -14から12まで)
if x == 0 || x < -14 || 12 < x {
return 3
}
return 3 + len(value) - len(rem) // "GMT"の3文字 + オフセット部分の長さ
}
この関数は、"GMT"の後に続く符号(+または-)と数値を解析します。解析された数値は、時間オフセット(例: GMT-8の8)を表します。また、オフセットの数値が妥当な範囲(-14から12時間)にあるかどうかの基本的な検証も行います。
テストケースの追加
src/pkg/time/time_test.goに、GMT-8形式のタイムゾーンを解析できることを確認するための新しいテストケースが追加されました。
// GMT with offset.
{"GMT-8", UnixDate, "Fri Feb 5 05:00:57 GMT-8 2010", true, true, 1, 0},
このテストケースは、GMT-8というタイムゾーンを含む時刻文字列が正しく解析されることを検証します。
これらの変更により、Goのtimeパッケージは、GMT-X形式のタイムゾーンをより堅牢に処理できるようになり、特定の形式の時刻文字列の解析における互換性の問題が解消されました。
コアとなるコードの変更箇所
src/pkg/time/format.go
-
atoi関数:--- a/src/pkg/time/format.go +++ b/src/pkg/time/format.go @@ -353,8 +353,8 @@ var atoiError = errors.New("time: invalid number") // Duplicates functionality in strconv, but avoids dependency. func atoi(s string) (x int, err error) { neg := false - if s != "" && s[0] == '-' { - neg = true + if s != "" && (s[0] == '-' || s[0] == '+') { + neg = s[0] == '-' s = s[1:] } q, rem, err := leadingInt(s) -
parse関数: タイムゾーン解析ロジックの変更と、新しいヘルパー関数の呼び出し。--- a/src/pkg/time/format.go +++ b/src/pkg/time/format.go @@ -933,25 +933,12 @@ func parse(layout, value string, defaultLocation, local *Location) (Time, error) value = value[3:] break } - - if len(value) >= 3 && value[2] == 'T' { - p, value = value[0:3], value[3:] - } else if len(value) >= 4 && value[3] == 'T' { - p, value = value[0:4], value[4:] - } else { + n, ok := parseTimeZone(value) + if !ok { err = errBad break } - for i := 0; i < len(p); i++ { - if p[i] < 'A' || 'Z' < p[i] { - err = errBad - } - } - if err != nil { - break - } - // It's a valid format. - zoneName = p + zoneName, value = value[:n], value[n:] case stdFracSecond0: // stdFracSecond0 requires the exact number of digits as specified in @@ -1024,7 +1011,11 @@ func parse(layout, value string, defaultLocation, local *Location) (Time, error) } // Otherwise, create fake zone with unknown offset. - t.loc = FixedZone(zoneName, 0) + if len(zoneName) > 3 && zoneName[:3] == "GMT" { + offset, _ = atoi(zoneName[3:]) // Guaranteed OK by parseGMT. + offset *= 3600 + } + t.loc = FixedZone(zoneName, offset) return t, nil } @@ -1032,6 +1023,57 @@ func parse(layout, value string, defaultLocation, local *Location) (Time, error) return Date(year, Month(month), day, hour, min, sec, nsec, defaultLocation), nil } +// parseTimeZone parses a time zone string and returns its length. +func parseTimeZone(value string) (length int, ok bool) { + if len(value) < 3 { + return 0, false + } + // GMT may have an offset. + if len(value) >= 3 && value[:3] == "GMT" { + length = parseGMT(value) + return length, true + } + + if len(value) >= 3 && value[2] == 'T' { + length = 3 + } else if len(value) >= 4 && value[3] == 'T' { + length = 4 + } else { + return 0, false + } + for i := 0; i < length; i++ { + if value[i] < 'A' || 'Z' < value[i] { + return 0, false + } + } + return length, true +} + +// parseGMT parses a GMT time zone. The input string is known to start "GMT". +// The function checks whether that is followed by a sign and a number in the +// range -14 through 12 excluding zero. +func parseGMT(value string) int { + value = value[3:] + if len(value) == 0 { + return 3 + } + sign := value[0] + if sign != '-' && sign != '+' { + return 3 + } + x, rem, err := leadingInt(value[1:]) + if err != nil { + return 3 + } + if sign == '-' { + x = -x + } + if x == 0 || x < -14 || 12 < x { + return 3 + } + return 3 + len(value) - len(rem) +} + func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string, err error) { if value[0] != '.' { err = errBad
src/pkg/time/time_test.go
parseTests変数: 新しいテストケースの追加。--- a/src/pkg/time/time_test.go +++ b/src/pkg/time/time_test.go @@ -510,6 +510,9 @@ var parseTests = []ParseTest{ // 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}, + // GMT with offset. + {"GMT-8", UnixDate, "Fri Feb 5 05:00:57 GMT-8 2010", true, 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 // cases would succeed, but the next four would not. Go 1.1 accepts all six.
コアとなるコードの解説
atoi関数の変更
atoi関数は、文字列から整数を解析する際に、数値の前に+記号がある場合も正しく処理できるように修正されました。これにより、GMT+8のような形式のオフセットも解析可能になります。
parse関数のタイムゾーン解析ロジック
parse関数は、時刻文字列のタイムゾーン部分を解析する際に、従来の固定長(3文字または4文字)のアルファベットタイムゾーン(例: PST, EST)のチェックに加えて、GMTで始まるタイムゾーン(例: GMT-8, GMT+5)を特別に処理するように変更されました。
parseTimeZoneの呼び出し:parse関数は、まずparseTimeZoneヘルパー関数を呼び出して、タイムゾーン文字列の長さと有効性を判断します。- GMTオフセットの検出と解析:
parseTimeZoneがGMT形式のタイムゾーンを検出した場合、parseGMT関数が呼び出されます。parseGMTは、GMTの後に続く符号(+または-)と数値(オフセット時間)を抽出し、その全体の長さを返します。 FixedZoneの利用:parse関数に戻り、抽出されたタイムゾーン名がGMTで始まる場合、parseGMTで解析されたオフセット値(時間単位)を秒単位に変換し、time.FixedZone関数に渡してLocationオブジェクトを作成します。これにより、GMT-8のようなタイムゾーンが、UTCから-8時間の固定オフセットを持つLocationとして正しく表現されるようになります。
parseTimeZone関数
この新しいヘルパー関数は、タイムゾーン文字列の解析を抽象化します。
- まず、入力文字列が
GMTで始まるかどうかをチェックします。 - もし
GMTで始まる場合、parseGMT関数に処理を委譲し、その結果(タイムゾーンの長さ)を返します。 GMTで始まらない場合、従来の3文字または4文字のアルファベットタイムゾーン(例:PST,EST)のロジックを適用し、その長さを返します。これにより、既存のタイムゾーン解析機能との互換性を保ちつつ、新しいGMTオフセット形式をサポートします。
parseGMT関数
この新しいヘルパー関数は、GMT形式のタイムゾーン文字列から数値オフセットを安全に抽出します。
- 入力文字列から"GMT"の3文字をスキップします。
- 次に続く文字が
+または-の符号であるかをチェックします。 - 符号の後に続く数値を
leadingInt(atoiが内部で利用する関数)で解析します。 - 解析された数値に符号を適用し、その値が妥当な時間オフセットの範囲内(-14から12時間)にあるかを検証します。この範囲チェックは、無効なオフセット値による潜在的な問題を防止します。
- 最終的に、"GMT"の3文字と解析されたオフセット部分の長さを合計して返します。
テストケースの追加
time_test.goに追加されたテストケースは、time.ParseがGMT-8というタイムゾーンを含む文字列を正しく解析し、期待されるtime.Timeオブジェクト(特にそのLocation情報)を生成できることを確認します。これは、変更が意図通りに機能していることを保証するための重要なステップです。
これらの変更は、Goのtimeパッケージがより多様なタイムゾーン表記に対応できるようになり、特に国際的なデータやレガシーシステムとの連携において、時刻解析の信頼性を向上させます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/727b2b6f7dbec2bed608e3c97129c3bb7bab0547
- Go CL (Code Review): https://golang.org/cl/12922043
- Go Issue 3790: https://go.dev/issue/3790 (おそらく、このコミットが解決しようとした元の問題報告)
参考にした情報源リンク
- Go言語の
timeパッケージに関する公式ドキュメント: https://pkg.go.dev/time - UTCとGMTの違いに関する一般的な情報源 (例: Wikipediaなど)
- Go言語の
time.Parseレイアウトに関する情報: https://pkg.go.dev/time#Parse - Go言語の
time.FixedZoneに関する情報: https://pkg.go.dev/time#FixedZone