Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 use Local 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を表す Locationtime.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 パッケージにおける時刻解析、特にタイムゾーンの処理に関して、いくつかの重要な技術的改善を導入しています。

  1. ParseInLocation 関数の導入:

    • これまでの time.Parse 関数は、入力文字列にタイムゾーン情報が含まれていない場合、デフォルトでUTCとして時刻を解釈していました。また、タイムゾーンオフセットや略語が与えられた場合、time.Local(システムのローカルタイムゾーン)と照合しようとしました。
    • 新しく追加された ParseInLocation(layout, value string, loc *Location) 関数は、この挙動を変更します。
      • タイムゾーン情報がない場合、ParseInLocation は引数で指定された loc*time.Location)をデフォルトのロケーションとして使用します。これにより、開発者は特定のタイムゾーンを明示的に指定して時刻を解析できるようになります。
      • タイムゾーンオフセットや略語が与えられた場合、ParseInLocationtime.Local ではなく、指定された loc と照合しようとします。これにより、特定の地域におけるタイムゾーンの曖昧さをより正確に解決できるようになります。
  2. Parse 関数のタイムゾーン解釈のドキュメント化:

    • time.Parse 関数のドキュメントが大幅に拡充され、タイムゾーンの解釈に関する詳細なルールが明記されました。
    • タイムゾーンオフセット(例: -0700)が与えられた場合、Parse はそのオフセットが現在のロケーション(time.Local)で使用されているタイムゾーンに対応していれば、そのロケーションとゾーンを使用します。そうでなければ、与えられたオフセットで固定された「偽の」ロケーションとして時刻を記録します。
    • タイムゾーン略語(例: MST)が与えられた場合、Parse は現在のロケーションで定義されたオフセットを持つ略語を使用します。「UTC」はロケーションに関わらずUTCとして認識されます。略語が不明な場合、ゼロオフセットを持つ「偽の」ロケーションとして時刻を記録します。これにより、解析と再フォーマットはロスレスに行えますが、実際のゾーンオフセットによって表現される瞬間が異なる可能性があることが警告されています。この問題を避けるためには、数値オフセットを使用するか、ParseInLocation を使用することが推奨されています。
  3. 「間違った」タイムゾーン名の認識と曖昧さ解消の改善:

    • コミットメッセージにある「Recognize "wrong" time zone names, like daylight savings time in winter.」は、例えば冬の期間に夏時間の略語が指定された場合でも、それを認識し、より適切なタイムゾーンを推測するロジックが追加されたことを意味します。
    • zoneinfo.golookupName 関数が変更され、タイムゾーン略語とUnix時刻(UTCでの時刻)の両方に基づいて、より正確なタイムゾーン情報を検索するようになりました。
    • 特に、シドニーの例(冬のESTと夏のEST)のように、同じ略語が異なるオフセットを持つ場合に、与えられた時刻(unix 引数)に基づいて適切なタイムゾーンを特定しようとします。これは、l.lookup(unix - int64(zone.offset)) を呼び出して、その時刻における実際のタイムゾーン名とオフセットを確認することで実現されます。これにより、タイムゾーンの曖昧さがより効果的に解消されます。

これらの変更は、time.Parse の内部実装である parse 関数に集約されています。parse 関数は、defaultLocationlocal という2つの *Location 引数を受け取るようになり、Parse(UTC, Local) を、ParseInLocation(loc, loc) を渡すことで、それぞれの挙動を実現しています。

全体として、このコミットは time パッケージのタイムゾーン処理をより堅牢で正確なものにし、開発者が時刻データを扱う際の一般的な落とし穴を回避できるよう支援することを目的としています。

コアとなるコードの変更箇所

このコミットでは、主に以下の4つのファイルが変更されています。

  1. src/pkg/time/example_test.go:

    • ExampleParse()ExampleParseInLocation() という新しい例が追加されました。
    • ExampleParse() は、タイムゾーン情報がない場合に Parse がUTCを返すこと、およびタイムゾーン略語の解釈の例を示しています。
    • ExampleParseInLocation() は、ParseInLocation が指定されたロケーションをデフォルトとして使用すること、およびタイムゾーン略語が指定されたロケーションでどのように解釈されるかを示しています。
  2. src/pkg/time/format.go:

    • Parse 関数のドキュメントが大幅に更新され、タイムゾーンの解釈に関する詳細な説明が追加されました。特に、タイムゾーンオフセットと略語の扱い、および ParseInLocation の使用が推奨されるケースについて記述されています。
    • 新しい公開関数 ParseInLocation が追加されました。この関数は、Parse と同様に時刻文字列を解析しますが、タイムゾーン情報がない場合のデフォルトロケーションと、タイムゾーンオフセット/略語を照合するロケーションとして、引数で渡された *Location を使用します。
    • 既存の Parse 関数の内部実装が、新しいプライベート関数 parse(layout, value string, defaultLocation, local *Location) を呼び出すように変更されました。Parseparse(layout, value, UTC, Local) を呼び出し、ParseInLocationparse(layout, value, loc, loc) を呼び出します。
    • parse 関数内で、タイムゾーンオフセットや略語が与えられた場合のロケーション検索ロジックが変更され、time.Local ではなく local 引数で渡されたロケーションを使用するようになりました。
    • タイムゾーン情報が全くない場合の最終的なフォールバックロケーションが、UTC から defaultLocation に変更されました。
  3. src/pkg/time/time_test.go:

    • TestParse 関数に、RFC1123 レイアウトで PDT(夏時間)を含む時刻文字列のテストケースが追加されました。
    • 新しいテスト関数 TestParseInSydney() が追加されました。このテストは、ParseInLocation がシドニーの「EST」のように、同じ略語でも季節によって異なるオフセットを持つタイムゾーンを正しく区別できることを検証します。これは、February のESTが+11時間、August のESTが+10時間であることを確認しています。
  4. 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.goparse 関数

以前の 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
}
  • defaultLocationlocal 引数:
    • 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() を使って確認します。ここで重要なのは、lookupNamet.sec+internalToUnix(解析された時刻のUnixエポック秒)が渡されるようになった点です。これにより、同じ略語でも季節によってオフセットが異なる場合(例: シドニーのEST)に、より正確なタイムゾーンを特定できるようになります。
  • フォールバックロケーション: どのタイムゾーン情報も解析できなかった場合、最終的に defaultLocation が使用されます。

src/pkg/time/zoneinfo.golookupName 関数

この関数は、特定のタイムゾーン略語(例: "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 パッケージは、より複雑なタイムゾーンのシナリオ(特に略語の曖セット)をより正確に処理できるようになり、開発者が時刻データを扱う際の信頼性が向上しました。

関連リンク

参考にした情報源リンク