2018/12/01
[Python] サマータイムのある地域で時刻計算する時は、一度 UTC に変換する
「サマータイムのある地域で」とタイトルに入れたのですが、地域に関わらず、時刻計算する時は UTC に変換してから行った方が良いです。 特にサマータイムのある地域では問題が起こりやすいので、注意が必要ということです。 ### 問題が起こる例 2018年の ニューヨークは 11/4 2:00 am までがサマータイムでした。 ``` 2018/11/4 2:00 am より前 EDT -04:00 2018/11/4 1:00 am 以降 EST -05:00 ``` このような時刻をまたぐ計算を aware のまま行うと、結果がおかしくなります。 ```python >>> import datetime, pytz >>> dt = datetime.datetime(2018, 11, 4, 7, 0, 0, 0) # 2018/11/4 7:00 am native >>> dt_aware = pytz.timezone('America/New_York').localize(dt) >>> dt_aware datetime.datetime(2018, 11, 4, 7, 0, tzinfo=) >>> str(dt_aware) '2018-11-04 07:00:00-05:00' ``` 上記の `dt_aware` から 1日前を計算すると以下のようになります。 ```python >>> calculated = dt_aware - datetime.timedelta(days=1) >>> calculated datetime.datetime(2018, 11, 3, 7, 0, tzinfo= ) >>> str(calculated) '2018-11-03 07:00:00-05:00' ``` ぱっと見正しそうですが、11月3日なのに EST になっていて、間違っています。 これを回避する方法を以下に挙げておきます。 基本的にはタイトルにあるように、UTC に変換するのが一番ですが、全てのケースで使えるわけではありません。 なお、タイムゾーンをコンストラクタで指定すると結果がおかしくなるので、必ず `localize()` を使用します。 > 参考 > > [[Python] datetime のコンストラクタでタイムゾーンは指定しない方がよい | 穀風](https://kokufu.blogspot.com/2018/11/python-datetime.html) ### 回避策1: もう一度 タイムゾーンを適用する `datetime` オブジェクトの中の [Unix time](https://ja.wikipedia.org/wiki/UNIX%E6%99%82%E9%96%93) は保持されているので、タイムゾーンを再度適用すると正しい結果になります。 ```python >>> calculated2 = calculated.astimezone(pytz.timezone('America/New_York')) >>> calculated2 datetime.datetime(2018, 11, 3, 8, 0, tzinfo= ) >>> str(calculated2) '2018-11-03 08:00:00-04:00' ``` これでも大抵の場合はうまくいきます。 ただ、本来存在しない時刻が途中に出てくるのがイマイチです。 ### 回避策2: UTC に変換してから計算する 計算する時には必ず UTC にし 、その後、再度タイムゾーンを適用します。 ```python >>> dt_aware_utc = dt_aware.astimezone(pytz.utc) >>> dt_aware_utc datetime.datetime(2018, 11, 4, 12, 0, tzinfo=) >>> calculated_utc = dt_aware_utc - datetime.timedelta(days=1) >>> calculated_utc datetime.datetime(2018, 11, 3, 12, 0, tzinfo= ) >>> calculated3 = calculated_utc.astimezone(pytz.timezone('America/New_York')) >>> calculated3 datetime.datetime(2018, 11, 3, 8, 0, tzinfo= ) >>> str(calculated3) '2018-11-03 08:00:00-04:00' ``` 少々面倒ですが、常に UTC で計算するようにすると、タイムゾーンに関する諸問題を適切に回避できる方法かと思います。 ### 回避策3: native に変換してから計算する 大抵の場合は UTC に変換する方法を使用しておけば良いと思います。 ただ、UTC に直す方法がうまく機能しない場合があります。 例えば、「その地域で3日前の17時が Unix time ではどう表されるか」を知りたい場合等 です。 そういった場合は、native にしてしまうという方法があります。 ```python >>> dt_native = dt_aware.replace(tzinfo=None) # dt と同等 >>> dt_native datetime.datetime(2018, 11, 4, 7, 0) >>> calculated_native = dt_native - datetime.timedelta(days=3) >>> calculated_native datetime.datetime(2018, 11, 1, 7, 0) >>> calculated_native2 = calculated_native.replace(hour=17, minute=0, second=0, microsecond=0) >>> calculated_native2 datetime.datetime(2018, 11, 1, 17, 0) >>> calculated4 = pytz.timezone('America/New_York').localize(calculated_native2) >>> calculated4 datetime.datetime(2018, 11, 1, 17, 0, tzinfo=) >>> str(calculated4) '2018-11-01 17:00:00-04:00' >>> int(calculated4.timestamp()) # Unix time (Python 3.3 以降) 1541106000 ``` かなり面倒ですね。 この場合は native な datetime オブジェクトではなく、date オブジェクトに直して、また datetime オブジェクトに戻すという手もつかえます 。 また、最初に書いたタイムゾーンを適用し直すという方法も使えますが、タイムゾーンを適用するタイミングによって結果が変わるので注意が必要です。 ### まとめ どの方法を使うせよ、**計算結果に対し自動的にオフセットが変わることは無い** ということを念頭においておくと良いでしょう。 その上で、基本的には UTC を基準にした aware で計算するのがベスト 。 native を扱わなければいけないケースもありますが、タイムゾーンが落ちることでミスが発生しやすくなります。 どうしても必要な場合、変換はギリギリまで行わず、計算後は aware に戻すようにすると良いかと思います。
0 件のコメント:
コメントを投稿