##
6.9.6 `tzinfo` オブジェクト

`tzinfo` は抽象基底クラスです。つまり、このクラスは直接 インスタンス化して利用しません。具体的なサブクラスを導出し、 (少なくとも) 利用したい `datetime` のメソッドが必要と する `tzinfo` の標準メソッドを実装してやる必要があります。 `datetime` モジュールでは、`tzinfo` の具体的な サブクラスは何ら提供していません。

`tzinfo` (の具体的なサブクラス) のインスタンスは `datetime` および `time` オブジェクトのコンストラクタに 渡すことができます。 後者のオブジェクトでは、データメンバをローカル時刻におけるものとして 見ており、`tzinfo` オブジェクトはローカル時刻の UTC からの オフセット、タイムゾーンの名前、DST オフセットを、渡された 日付および時刻オブジェクトからの相対で示すためのメソッドを 提供します。

pickle 化についての特殊な要求事項: `tzinfo` のサブクラスは 引数なしで呼び出すことのできる `__init__` メソッドを持たねば なりません。そうでなければ、pickle 化することはできますがおそらく unpickle 化することはできないでしょう。これは技術的な側面からの 要求であり、将来緩和されるかもしれません。

`tzinfo` の具体的なサブクラスでは、以下のメソッドを 実装する必要があります。厳密にどのメソッドが必要なのかは、 aware な `datetime` オブジェクトがこのサブクラスの インスタンスをどのように使うかに依存します。不確かならば、 単に全てを実装してください。

`utcoffset`( | self, dt)

  ローカル時間の UTC からのオフセットを、UTC から東向きを正とした分で 返します。ローカル時間が UTC の西側にある場合、この値は負になります。 このメソッドは UTC からのオフセットの総計を返すように意図されている ので注意してください; 例えば、 `tzinfo` オブジェクトが タイムゾーンと DST 修正の両方を表現する場合、`utcoffset()` はそれらの合計を返さなければなりません。UTC オフセットが未知である 場合、`None` を返してください。そうでない場合には、 返される値は -1439 から 1439 の両端を含む値 (1440 = 24*60 ; つまり、オフセットの大きさは 1 日より短くなくてはなりません) が分で指定された `timedelta` オブジェクトでなければなりません。 ほとんどの `utcoffset()` 実装は、おそらく以下の二つのうちの一つに 似たものになるでしょう:

          return CONSTANT                 # fixed-offset class
          return CONSTANT + self.dst(dt)  # daylight-aware class

  `utcoffset()` が `None` を返さない場合、 `dst()` も `None` を返してはなりません。

  `utcoffset()` のデフォルトの実装は `NotImplementedError` を送出します。

`dst`( | self, dt)

  夏時間 (DST) 修正を、UTC から東向きを正とした分で 返します。DST 情報が未知の場合、`None` が返されます。 DST が有効でない場合には `timedelta(0)` を返します。 DST が有効の場合、オフセットは `timedelta` オブジェクト で返します (詳細は`utcoffset()` を参照してください)。 DST オフセットが利用可能な場合、この値は `utcoffset()` が返すUTC からのオフセットには既に加算されているため、 DST を個別に取得する必要がない限り `dst()` を使って 問い合わせる必要はないので注意してください。 例えば、`datetime.timetuple()` は `tzinfo` メンバ の `dst()` メソッドを呼んで `tm_isdst` フラグが セットされているかどうか判断し、`tzinfo.fromutc()` は `dst()` タイムゾーンを移動する際に DST による変更 があるかどうかを調べます。

  標準および夏時間の両方をモデル化している `tzinfo` サブクラスの インスタンス tz は以下の式:

  `tz.utcoffset(dt) - tz.dst(dt)`

  が、`dt.tzinfo==tz` 全ての `datetime` オブジェクト dt について常に同じ結果を返さなければならないという点で、 一貫性を持っていなければなりません。 正常に実装された `tzinfo` のサブクラスでは、この式は タイムゾーンにおける "標準オフセット (standard offset)" を表し、 特定の日や時刻の事情ではなく地理的な位置にのみ依存していなくては なりません。`datetime.astimezone()` の実装はこの事実に 依存していますが、違反を検出することができません; 正しく実装するのはプログラマの責任です。`tzinfo` の サブクラスでこれを保証することができない場合、`tzinfo.fromutc()` の実装をオーバライドして、`astimezone()` に関わらず 正しく動作するようにしてもかまいません。

  ほとんどの `dst()` 実装は、おそらく以下の二つのうちの一つに 似たものになるでしょう:

          return timedelta(0)   # a fixed-offset class:  doesn't account for DST

        or

          # Code to set dston and dstoff to the time zone's DST transition
          # times based on the input dt.year, and expressed in standard local
          # time.  Then

          if dston <= dt.replace(tzinfo=None) < dstoff:
              return timedelta(hours=1)
          else:
              return timedelta(0)

  デフォルトの `dst()` 実装は `NotImplementedError` を送出します。

`tzname`( | self, dt)

  `datetime` オブジェクト dt に対応するタイムゾーン名 を文字列で返します。 `datetime` モジュールでは文字列名について何も定義しておらず、 特に何かを意味するといった要求仕様もまったくありません。 例えば、"GMT"、"UTC"、 "-500"、 "-5:00"、 "EDT"、 "US/Eastern"、 "America/New York" は全て有効な応答となります。 文字列名が未知の場合には `None` を返してください。 `tzinfo` のサブクラスでは、 特に、`tzinfo` クラスが夏時間について記述している場合のように、 渡された dt の特定の値によって異なった名前を返したい 場合があるため、文字列値ではなくメソッドとなっていることに注意してください。

  デフォルトの `tzname()` 実装は `NotImplementedError` を送出します。

以下のメソッドは `datetime` や `time` オブジェクトにおいて、 同名のメソッドが呼び出された際に応じて呼び出されます。`datetime` オブジェクトは自身を引数としてメソッドに渡し、`time` オブジェクトは 引数として `None` をメソッドに渡します。従って、`tzinfo` の サブクラスにおけるメソッドは引数 dt が `None` の場合と、 `datetime` の場合を受理するように用意しなければなりません。

`None` が渡された場合、最良の応答方法を決めるのはクラス設計者次第 です。例えば、このクラスが `tzinfo` プロトコルと関係をもたない ということを表明させたければ、`None` が適切です。 標準時のオフセットを見つける他の手段がない場合には、 標準 UTC オフセットを返すために `utcoffset(None)` を使うともっと便利かもしれません。

`datetime` オブジェクトが `datetime` メソッド の応答として返された場合、`dt.tzinfo` は self と同じオブジェクトになります。ユーザが直接 `tzinfo` メソッド を呼び出さないかぎり、`tzinfo` メソッドは `dt.tzinfo` と self が同じであることに依存します。 その結果 `tzinfo` メソッドは dt がローカル時間であると 解釈するので、他のタイムゾーンでのオブジェクトの振る舞いについて 心配する必要がありません。

`fromutc`( | self, dt)

  デフォルトの `datetime.astimezone()` 実装で呼び出されます。 `datetime.astimezone()` から呼ばれた場合、`dt.tzinfo` は self であり、 dt の日付および時刻データメンバは UTC 時刻を表しているものとして見えます。`fromutc()` の目的は、self のローカル時刻に等しい `datetime` オブジェクト を返すことにより日付と時刻データメンバを修正することにあります。

  ほとんどの `tzinfo` サブクラスではデフォルトの `fromutc()` 実装を問題なく継承できます。デフォルトの実装は、固定オフセットのタイムゾーン や、標準時と夏時間の両方について記述しているタイムゾーン、そして DST 移行時刻が年によって異なる場合でさえ、扱えるくらい強力なものです。 デフォルトの `fromutc()` 実装が全ての場合に対して正しく 扱うことができないような例は、標準時の (UTCからの) オフセットが 引数として渡された特定の日や時刻に依存するもので、これは政治的な理由に よって起きることがあります。 デフォルトの `astimezone()` や `fromutc()` の実装は、 結果が標準時オフセットの変化にまたがる何時間かの中にある場合、 期待通りの結果を生成しないかもしれません。

  エラーの場合のためのコードを除き、デフォルトの `fromutc()` の 実装は以下のように動作します:

        def fromutc(self, dt):
            # raise ValueError error if dt.tzinfo is not self
            dtoff = dt.utcoffset()
            dtdst = dt.dst()
            # raise ValueError if dtoff is None or dtdst is None
            delta = dtoff - dtdst  # this is self's standard offset
            if delta:
                dt += delta   # convert to standard local time
                dtdst = dt.dst()
                # raise ValueError if dtdst is None
            if dtdst:
                return dt + dtdst
            else:
                return dt

以下に `tzinfo` クラスの使用例を示します:

    from datetime import tzinfo, timedelta, datetime

    ZERO = timedelta(0)
    HOUR = timedelta(hours=1)

    # A UTC class.

    class UTC(tzinfo):
        """UTC"""

        def utcoffset(self, dt):
            return ZERO

        def tzname(self, dt):
            return "UTC"

        def dst(self, dt):
            return ZERO

    utc = UTC()

    # A class building tzinfo objects for fixed-offset time zones.
    # Note that FixedOffset(0, "UTC") is a different way to build a
    # UTC tzinfo object.

    class FixedOffset(tzinfo):
        """Fixed offset in minutes east from UTC."""

        def __init__(self, offset, name):
            self.__offset = timedelta(minutes = offset)
            self.__name = name

        def utcoffset(self, dt):
            return self.__offset

        def tzname(self, dt):
            return self.__name

        def dst(self, dt):
            return ZERO

    # A class capturing the platform's idea of local time.

    import time as _time

    STDOFFSET = timedelta(seconds = -_time.timezone)
    if _time.daylight:
        DSTOFFSET = timedelta(seconds = -_time.altzone)
    else:
        DSTOFFSET = STDOFFSET

    DSTDIFF = DSTOFFSET - STDOFFSET

    class LocalTimezone(tzinfo):

        def utcoffset(self, dt):
            if self._isdst(dt):
                return DSTOFFSET
            else:
                return STDOFFSET

        def dst(self, dt):
            if self._isdst(dt):
                return DSTDIFF
            else:
                return ZERO

        def tzname(self, dt):
            return _time.tzname[self._isdst(dt)]

        def _isdst(self, dt):
            tt = (dt.year, dt.month, dt.day,
                  dt.hour, dt.minute, dt.second,
                  dt.weekday(), 0, -1)
            stamp = _time.mktime(tt)
            tt = _time.localtime(stamp)
            return tt.tm_isdst > 0

    Local = LocalTimezone()

    # A complete implementation of current DST rules for major US time zones.

    def first_sunday_on_or_after(dt):
        days_to_go = 6 - dt.weekday()
        if days_to_go:
            dt += timedelta(days_to_go)
        return dt

    # In the US, DST starts at 2am (standard time) on the first Sunday in April.
    DSTSTART = datetime(1, 4, 1, 2)
    # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
    # which is the first Sunday on or after Oct 25.
    DSTEND = datetime(1, 10, 25, 1)

    class USTimeZone(tzinfo):

        def __init__(self, hours, reprname, stdname, dstname):
            self.stdoffset = timedelta(hours=hours)
            self.reprname = reprname
            self.stdname = stdname
            self.dstname = dstname

        def __repr__(self):
            return self.reprname

        def tzname(self, dt):
            if self.dst(dt):
                return self.dstname
            else:
                return self.stdname

        def utcoffset(self, dt):
            return self.stdoffset + self.dst(dt)

        def dst(self, dt):
            if dt is None or dt.tzinfo is None:
                # An exception may be sensible here, in one or both cases.
                # It depends on how you want to treat them.  The default
                # fromutc() implementation (called by the default astimezone()
                # implementation) passes a datetime with dt.tzinfo is self.
                return ZERO
            assert dt.tzinfo is self

            # Find first Sunday in April & the last in October.
            start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
            end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))

            # Can't compare naive to aware objects, so strip the timezone from
            # dt first.
            if start <= dt.replace(tzinfo=None) < end:
                return HOUR
            else:
                return ZERO

    Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT")
    Central  = USTimeZone(-6, "Central",  "CST", "CDT")
    Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
    Pacific  = USTimeZone(-8, "Pacific",  "PST", "PDT")

標準時間 (standard time) および夏時間 (daylight time) の両方を 記述している `tzinfo` のサブクラスでは、回避不能の難解な問題が年に 2 度あるので注意してください。具体的な例として、東部アメリカ時刻 (US Eastern, UTC -5000) を考えます。EDT は 4 月の最初の日曜日 の 1:59 (EST) 以後に開始し、10 月の最後の日曜日の 1:59 (EDT) に 終了します:

        UTC   3:MM  4:MM  5:MM  6:MM  7:MM  8:MM
        EST  22:MM 23:MM  0:MM  1:MM  2:MM  3:MM
        EDT  23:MM  0:MM  1:MM  2:MM  3:MM  4:MM

      start  22:MM 23:MM  0:MM  1:MM  3:MM  4:MM

        end  23:MM  0:MM  1:MM  1:MM  2:MM  3:MM

DST の開始の際 ("start" の並び) ローカルの壁時計は 1:59 から 3:00 に飛びます。この日は 2:MM の形式をとる時刻は実際には無意味と なります。従って、`astimezone(Eastern)` は DST が開始する 日には `hour==2` となる結果を返すことはありません。 `astimezone()` がこのことを保証するようにするには、 `tzinfo.dst()` メソッドは "失われた時間" (東部時刻における 2:MM) が夏時間に存在することを考えなければなりません。

DST が終了する際 ("end" の並び) では、問題はさらに悪化します: 1 時間の間、ローカルの壁時計ではっきりと時刻をいえなくなります: それは夏時間の最後の 1 時間です。東部時刻では、その日の UTC での 5:MM に夏時間は終了します。ローカルの壁時計は 1:59 (夏時間) から 1:00 (標準時) に再び巻き戻されます。ローカルの時刻に おける 1:MM はあいまいになります。`astimezone()` は二つの UTC 時刻を同じローカルの時刻に対応付けることで ローカルの時計の振る舞いをまねます。 東部時刻の例では、5:MM および 6:MM の形式をとる UTC 時刻は 両方とも、東部時刻に変換された際に 1:MM に対応づけられます。 `astimezone()` がこのことを保証するようにするには、 `tzinfo.dst()` は "繰り返された時間" が標準時に存在する ことを考慮しなければなりません。このことは、例えばタイムゾーンの標準の ローカルな時刻に DST への切り替え時刻を表現することで簡単に設定する ことができます。

このようなあいまいさを許容できないアプリケーションは、 ハイブリッドな `tzinfo` サブクラスを使って問題を回避しなければ なりません; UTC や、他のオフセットが固定された `tzinfo` の サブクラス (EST (-5 時間の固定オフセット) のみを表すクラスや、 EDT (-4 時間の固定オフセット) のみを表すクラス) を使う限り、あいまいさは 発生しません。