[インデックス 15121] ファイルの概要
このコミットは、Go言語の time
パッケージにおけるタイムゾーンの解釈と処理を改善することを目的としています。特に、time.Parse
関数のタイムゾーン解釈のドキュメント化、新しい関数 ParseInLocation
の追加、そして「間違った」タイムゾーン名の認識や、オフセットを用いたタイムゾーン名の曖昧さ解消に焦点を当てています。これにより、Go 1.0における既存のバグ動作が修正され、より正確な時刻解析が可能になります。
コミット
commit 1d9f67daf0e8ba950da75f68f1f3f2650b13cd67
Author: Russ Cox <rsc@golang.org>
Date: Sun Feb 3 23:02:12 2013 -0500
time: deal a bit better with time zones in Parse
* Document Parse's zone interpretation.
* Add ParseInLocation (API change).
* Recognize "wrong" time zone names, like daylight savings time in winter.
* Disambiguate time zone names using offset (like winter EST vs summer EST in Sydney).
The final two are backwards-incompatible changes, but I believe
they are both buggy behavior in the Go 1.0 versions; the old results
were more wrong than the new ones.
Fixes #3604.
Fixes #3653.
Fixes #4001.
R=adg
CC=golang-dev
https://golang.org/cl/7288052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1d9f67daf0e8ba950da75f68f1f3f2650b13cd67
元コミット内容
time
パッケージにおいて、Parse
関数でのタイムゾーン処理を改善する。
具体的には以下の変更が含まれる:
Parse
関数のタイムゾーン解釈に関するドキュメントの追加。ParseInLocation
関数の追加(API変更)。- 冬の期間における夏時間など、「間違った」タイムゾーン名の認識。
- オフセットを使用してタイムゾーン名の曖昧さを解消(例:シドニーにおける冬のESTと夏のEST)。
最後の2つの変更は後方互換性のない変更だが、Go 1.0バージョンでのバグ動作であると判断され、古い結果よりも新しい結果の方がより正確であるため採用された。
このコミットは以下のIssueを修正する:
- #3604
- #3653
- #4001
変更の背景
このコミットは、Go言語の time
パッケージにおけるタイムゾーン処理の既存の課題とバグに対処するために行われました。特に、以下のIssueが修正対象となっています。
-
Issue #3604:
time.Parse
should useLocal
for zone abbreviations if possible このIssueは、time.Parse
がタイムゾーンの略語(例: "PST", "EST")を解析する際に、現在のローカルタイムゾーンの情報を適切に利用すべきであるという問題提起です。以前のParse
関数は、タイムゾーン略語が与えられた場合、その略語が現在のロケーションで定義されたオフセットを持つ場合にのみそのオフセットを使用しようとしました。しかし、略語が不明な場合や、同じ略語が異なるオフセットを持つ可能性がある場合(例: シドニーのEST)、正確なタイムゾーンを特定するのが困難でした。このコミットは、ParseInLocation
の導入とParse
の内部ロジックの改善により、この問題を解決しようとしています。 -
Issue #3653:
time.Parse
should not assume zone abbreviation is correct for the given time このIssueは、time.Parse
が入力文字列に含まれるタイムゾーン略語が、その時刻に対して常に正しいと仮定してしまう問題に関するものです。例えば、冬の期間に夏時間の略語が指定された場合など、実際にはその時刻にそのタイムゾーンが適用されないケースがありました。このコミットでは、「間違った」タイムゾーン名も認識し、より適切なタイムゾーンを推測するロジックが追加されました。 -
Issue #4001:
time.Parse
should document its zone interpretation このIssueは、time.Parse
がタイムゾーン情報をどのように解釈するかについて、公式ドキュメントが不足しているという指摘です。開発者がParse
の挙動を正確に理解し、予期せぬ結果を避けるためには、そのタイムゾーン解釈ルールが明確に文書化されている必要がありました。このコミットでは、Parse
関数のドキュメントにタイムゾーン解釈に関する詳細な説明が追加され、透明性が向上しました。
これらの問題は、特に国際的なアプリケーションや、異なるタイムゾーンからのデータを取り扱うシステムにおいて、時刻の正確な解析と表示を妨げる要因となっていました。このコミットは、これらの課題を解決し、time
パッケージの堅牢性と使いやすさを向上させることを目的としています。
前提知識の解説
このコミットの変更内容を理解するためには、以下の概念について基本的な知識が必要です。
-
UTC (Coordinated Universal Time): 協定世界時。世界の標準時であり、タイムゾーンの基準となる時刻です。Go言語の
time
パッケージでは、内部的にUTCを基準として時刻を扱います。 -
タイムゾーン (Time Zone): 地球上の特定の地域で共通して使用される標準時です。UTCからのオフセット(例: UTC+9)や、夏時間(Daylight Saving Time, DST)の有無によって定義されます。
-
夏時間 (Daylight Saving Time, DST): 夏の期間に時間を1時間進める制度です。これにより、日中の明るい時間を有効活用し、エネルギー消費を抑えることを目的としています。DSTが導入されている地域では、同じタイムゾーン名でも、夏と冬でUTCからのオフセットが異なる場合があります(例: ESTが冬はUTC-5、夏はUTC-4のEDTになるなど)。
-
time.Time
構造体: Go言語で時刻を表すための基本的な型です。この構造体は、特定の瞬間(Unixエポックからの経過秒数)と、その時刻がどのタイムゾーンに属するかを示すLocation
情報を含んでいます。 -
time.Location
構造体: タイムゾーン情報を表す型です。特定のタイムゾーンのルール(UTCからのオフセット、夏時間の有無、タイムゾーン名など)をカプセル化します。time.UTC
はUTCを表すLocation
、time.Local
はシステムのローカルタイムゾーンを表すLocation
です。 -
time.Parse
関数: 文字列形式の時刻をtime.Time
型に変換するための関数です。この関数は、指定されたレイアウト文字列に基づいて入力文字列を解析します。タイムゾーン情報が文字列に含まれている場合、Parse
はその情報を利用してtime.Time
オブジェクトのLocation
を設定しようとします。タイムゾーン情報がない場合、デフォルトでUTCとして解釈されます。 -
タイムゾーンの曖昧さ: 同じタイムゾーン略語が異なるオフセットを持つ場合や、異なるタイムゾーンが同じ略語を使用する場合に発生します。例えば、オーストラリアのシドニーでは、夏時間と標準時間で両方「EST」という略語が使われることがあり、どちらのESTを指しているのかをオフセット情報なしで判断するのは困難です。
-
Unixエポック: 1970年1月1日00:00:00 UTCを基準とした時刻の表現方法です。多くのシステムで内部的に時刻を扱う際に使用されます。
これらの概念を理解することで、コミットが time.Parse
の挙動をどのように改善し、より正確な時刻解析を可能にしているかを深く把握することができます。
技術的詳細
このコミットは、Go言語の time
パッケージにおける時刻解析、特にタイムゾーンの処理に関して、いくつかの重要な技術的改善を導入しています。
-
ParseInLocation
関数の導入:- これまでの
time.Parse
関数は、入力文字列にタイムゾーン情報が含まれていない場合、デフォルトでUTCとして時刻を解釈していました。また、タイムゾーンオフセットや略語が与えられた場合、time.Local
(システムのローカルタイムゾーン)と照合しようとしました。 - 新しく追加された
ParseInLocation(layout, value string, loc *Location)
関数は、この挙動を変更します。- タイムゾーン情報がない場合、
ParseInLocation
は引数で指定されたloc
(*time.Location
)をデフォルトのロケーションとして使用します。これにより、開発者は特定のタイムゾーンを明示的に指定して時刻を解析できるようになります。 - タイムゾーンオフセットや略語が与えられた場合、
ParseInLocation
はtime.Local
ではなく、指定されたloc
と照合しようとします。これにより、特定の地域におけるタイムゾーンの曖昧さをより正確に解決できるようになります。
- タイムゾーン情報がない場合、
- これまでの
-
Parse
関数のタイムゾーン解釈のドキュメント化:time.Parse
関数のドキュメントが大幅に拡充され、タイムゾーンの解釈に関する詳細なルールが明記されました。- タイムゾーンオフセット(例:
-0700
)が与えられた場合、Parse
はそのオフセットが現在のロケーション(time.Local
)で使用されているタイムゾーンに対応していれば、そのロケーションとゾーンを使用します。そうでなければ、与えられたオフセットで固定された「偽の」ロケーションとして時刻を記録します。 - タイムゾーン略語(例:
MST
)が与えられた場合、Parse
は現在のロケーションで定義されたオフセットを持つ略語を使用します。「UTC」はロケーションに関わらずUTCとして認識されます。略語が不明な場合、ゼロオフセットを持つ「偽の」ロケーションとして時刻を記録します。これにより、解析と再フォーマットはロスレスに行えますが、実際のゾーンオフセットによって表現される瞬間が異なる可能性があることが警告されています。この問題を避けるためには、数値オフセットを使用するか、ParseInLocation
を使用することが推奨されています。
-
「間違った」タイムゾーン名の認識と曖昧さ解消の改善:
- コミットメッセージにある「Recognize "wrong" time zone names, like daylight savings time in winter.」は、例えば冬の期間に夏時間の略語が指定された場合でも、それを認識し、より適切なタイムゾーンを推測するロジックが追加されたことを意味します。
zoneinfo.go
のlookupName
関数が変更され、タイムゾーン略語とUnix時刻(UTCでの時刻)の両方に基づいて、より正確なタイムゾーン情報を検索するようになりました。- 特に、シドニーの例(冬のESTと夏のEST)のように、同じ略語が異なるオフセットを持つ場合に、与えられた時刻(
unix
引数)に基づいて適切なタイムゾーンを特定しようとします。これは、l.lookup(unix - int64(zone.offset))
を呼び出して、その時刻における実際のタイムゾーン名とオフセットを確認することで実現されます。これにより、タイムゾーンの曖昧さがより効果的に解消されます。
これらの変更は、time.Parse
の内部実装である parse
関数に集約されています。parse
関数は、defaultLocation
と local
という2つの *Location
引数を受け取るようになり、Parse
は (UTC, Local)
を、ParseInLocation
は (loc, loc)
を渡すことで、それぞれの挙動を実現しています。
全体として、このコミットは time
パッケージのタイムゾーン処理をより堅牢で正確なものにし、開発者が時刻データを扱う際の一般的な落とし穴を回避できるよう支援することを目的としています。
コアとなるコードの変更箇所
このコミットでは、主に以下の4つのファイルが変更されています。
-
src/pkg/time/example_test.go
:ExampleParse()
とExampleParseInLocation()
という新しい例が追加されました。ExampleParse()
は、タイムゾーン情報がない場合にParse
がUTCを返すこと、およびタイムゾーン略語の解釈の例を示しています。ExampleParseInLocation()
は、ParseInLocation
が指定されたロケーションをデフォルトとして使用すること、およびタイムゾーン略語が指定されたロケーションでどのように解釈されるかを示しています。
-
src/pkg/time/format.go
:Parse
関数のドキュメントが大幅に更新され、タイムゾーンの解釈に関する詳細な説明が追加されました。特に、タイムゾーンオフセットと略語の扱い、およびParseInLocation
の使用が推奨されるケースについて記述されています。- 新しい公開関数
ParseInLocation
が追加されました。この関数は、Parse
と同様に時刻文字列を解析しますが、タイムゾーン情報がない場合のデフォルトロケーションと、タイムゾーンオフセット/略語を照合するロケーションとして、引数で渡された*Location
を使用します。 - 既存の
Parse
関数の内部実装が、新しいプライベート関数parse(layout, value string, defaultLocation, local *Location)
を呼び出すように変更されました。Parse
はparse(layout, value, UTC, Local)
を呼び出し、ParseInLocation
はparse(layout, value, loc, loc)
を呼び出します。 parse
関数内で、タイムゾーンオフセットや略語が与えられた場合のロケーション検索ロジックが変更され、time.Local
ではなくlocal
引数で渡されたロケーションを使用するようになりました。- タイムゾーン情報が全くない場合の最終的なフォールバックロケーションが、
UTC
からdefaultLocation
に変更されました。
-
src/pkg/time/time_test.go
:TestParse
関数に、RFC1123
レイアウトでPDT
(夏時間)を含む時刻文字列のテストケースが追加されました。- 新しいテスト関数
TestParseInSydney()
が追加されました。このテストは、ParseInLocation
がシドニーの「EST」のように、同じ略語でも季節によって異なるオフセットを持つタイムゾーンを正しく区別できることを検証します。これは、February
のESTが+11時間、August
のESTが+10時間であることを確認しています。
-
src/pkg/time/zoneinfo.go
:lookupName
関数のシグネチャが変更され、unix int64
引数が追加されました。これにより、特定のUnix時刻におけるタイムゾーン略語の情報を検索できるようになりました。lookupName
の内部ロジックが変更され、まず与えられたUnix時刻において実際に有効であったタイムゾーン名とオフセットを検索するようになりました。これは、シドニーのESTの例のように、同じ略語でも時刻によって異なるオフセットを持つタイムゾーンを正確に特定するために重要です。- もし特定の時刻で有効なゾーンが見つからない場合、以前と同様に単なる名前の一致で検索を試みます。
これらの変更は、time
パッケージのタイムゾーン処理の正確性と柔軟性を向上させるためのものです。
コアとなるコードの解説
このコミットのコアとなる変更は、src/pkg/time/format.go
内の parse
関数と、src/pkg/time/zoneinfo.go
内の lookupName
関数に集中しています。
src/pkg/time/format.go
の parse
関数
以前の time.Parse
関数は、タイムゾーン情報がない場合にUTCをデフォルトとし、タイムゾーンオフセットや略語を time.Local
と照合していました。このコミットでは、この挙動をより柔軟にするために、内部的な parse
関数が導入されました。
func parse(layout, value string, defaultLocation, local *Location) (Time, error) {
// ... (時刻要素の解析ロジックは省略) ...
if z != nil { // タイムゾーン情報が既に解析されている場合
return Date(year, Month(month), day, hour, min, sec, nsec, z), nil
}
if zoneOffset != -1 { // 数値オフセット(例: -0700)が解析された場合
t := Date(year, Month(month), day, hour, min, sec, nsec, UTC)
t.sec -= int64(zoneOffset) // UTC時刻をオフセット分調整
// 与えられたオフセットに対応するローカルゾーンを探す
// そのゾーンが与えられた時刻に有効であれば、それを使用する
name, offset, _, _, _ := local.lookup(t.sec + internalToUnix) // ここで `local` を使用
if offset == zoneOffset && (zoneName == "" || name == zoneName) {
t.loc = local // ここで `local` を設定
return t, nil
}
// それ以外の場合、オフセットを持つ偽のゾーンを作成
// ...
}
if zoneName != "" { // タイムゾーン略語(例: MST)が解析された場合
t := Date(year, Month(month), day, hour, min, sec, nsec, UTC)
// 与えられたオフセットに対応するローカルゾーンを探す
// そのゾーンが与えられた時刻に有効であれば、それを使用する
// lookupName に t.sec+internalToUnix (Unix時刻) を渡すことで、
// その時刻における正確なタイムゾーン情報を取得しようとする
offset, _, ok := local.lookupName(zoneName, t.sec+internalToUnix) // ここで `local` と Unix時刻を使用
if ok {
t.sec -= int64(offset)
t.loc = local // ここで `local` を設定
return t, nil
}
// それ以外の場合、不明なオフセットを持つ偽のゾーンを作成
// ...
}
// タイムゾーン情報が全くない場合、defaultLocation を使用
return Date(year, Month(month), day, hour, min, sec, nsec, defaultLocation), nil
}
defaultLocation
とlocal
引数:defaultLocation
: 入力文字列にタイムゾーン情報が全く含まれていない場合に、デフォルトとして使用されるLocation
です。time.Parse
からはtime.UTC
が、time.ParseInLocation
からは引数で指定されたloc
が渡されます。local
: タイムゾーンオフセットや略語が与えられた場合に、そのオフセットや略語を照合するために使用されるLocation
です。time.Parse
からはtime.Local
が、time.ParseInLocation
からは引数で指定されたloc
が渡されます。
- タイムゾーンオフセットの処理:
zoneOffset != -1
のブロックでは、解析された数値オフセットがlocal
引数で指定されたロケーションのタイムゾーンと一致するかどうかをlocal.lookup()
を使って確認します。これにより、例えば-0800
が指定された場合に、それがシステムのローカルタイムゾーンのPSTと一致すれば、そのロケーションが使用されます。 - タイムゾーン略語の処理:
zoneName != ""
のブロックでは、解析されたタイムゾーン略語がlocal
引数で指定されたロケーションのタイムゾーンと一致するかどうかをlocal.lookupName()
を使って確認します。ここで重要なのは、lookupName
にt.sec+internalToUnix
(解析された時刻のUnixエポック秒)が渡されるようになった点です。これにより、同じ略語でも季節によってオフセットが異なる場合(例: シドニーのEST)に、より正確なタイムゾーンを特定できるようになります。 - フォールバックロケーション: どのタイムゾーン情報も解析できなかった場合、最終的に
defaultLocation
が使用されます。
src/pkg/time/zoneinfo.go
の lookupName
関数
この関数は、特定のタイムゾーン略語(例: "EST")に対応するオフセットとDST情報を検索します。変更のポイントは、unix int64
引数が追加されたことです。
// lookupName returns information about the time zone with
// the given name (such as "EST") at the given pseudo-Unix time
// (what the given time of day would be in UTC).
func (l *Location) lookupName(name string, unix int64) (offset int, isDST bool, ok bool) {
l = l.get()
// First try for a zone with the right name that was actually
// in effect at the given time. (In Sydney, Australia, both standard
// and daylight-savings time are abbreviated "EST". Using the
// offset helps us pick the right one for the given time.
// It's not perfect: during the backward transition we might pick
// either one.)
for i := range l.zone {
zone := &l.zone[i]
if zone.name == name {
// そのゾーンが与えられた時刻に有効であったかを確認
nam, offset, isDST, _, _ := l.lookup(unix - int64(zone.offset))
if nam == zone.name {
return offset, isDST, true
}
}
}
// Otherwise fall back to an ordinary name match.
for i := range l.zone {
zone := &l.zone[i]
if zone.name == name {
return zone.offset, zone.isDST, true
}
}
// Otherwise, give up.
return
}
unix
引数: この引数は、解析対象の時刻がUTCで何秒であるかを示します。- 時刻に基づく検索:
lookupName
は、まずl.zone
内の各ゾーンをループし、name
が一致するゾーンを見つけます。そして、そのゾーンのオフセットを考慮してunix
時刻を調整し、l.lookup()
を呼び出して、その調整された時刻における実際のタイムゾーン名とオフセットを取得します。もし取得したタイムゾーン名が元のzone.name
と一致すれば、それがその時刻に有効なゾーンであると判断し、そのオフセットとDST情報を返します。 - 曖昧さ解消: このロジックにより、例えばシドニーの「EST」のように、同じ略語でも夏時間と標準時間でオフセットが異なる場合に、与えられた時刻に基づいて適切なオフセットを持つゾーンを正確に選択できるようになります。
- フォールバック: もし特定の時刻で有効なゾーンが見つからない場合、以前と同様に、単に名前が一致する最初のゾーンの情報を返します。
これらの変更により、Goの time
パッケージは、より複雑なタイムゾーンのシナリオ(特に略語の曖セット)をより正確に処理できるようになり、開発者が時刻データを扱う際の信頼性が向上しました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/1d9f67daf0e8ba950da75f68f1f3f2650b13cd67
- Go Issue #3604:
time.Parse
should useLocal
for zone abbreviations if possible - https://github.com/golang/go/issues/3604 - Go Issue #3653:
time.Parse
should not assume zone abbreviation is correct for the given time - https://github.com/golang/go/issues/3653 - Go Issue #4001:
time.Parse
should document its zone interpretation - https://github.com/golang/go/issues/4001 - Gerrit Change List 7288052: https://golang.org/cl/7288052
参考にした情報源リンク
- Go言語の公式ドキュメント:
time
パッケージ (https://pkg.go.dev/time) - Go言語のIssueトラッカー (https://github.com/golang/go/issues)
- 協定世界時 (UTC) - Wikipedia (https://ja.wikipedia.org/wiki/%E5%8D%94%E5%AE%9A%E4%B8%96%E7%95%8C%E6%99%82)
- タイムゾーン - Wikipedia (https://ja.wikipedia.org/wiki/%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%BE%E3%83%BC%E3%83%B3)
- 夏時間 - Wikipedia (https://ja.wikipedia.org/wiki/%E5%A4%8F%E6%99%82%E9%96%93)