テクノロジー · 実用ガイド
Python ZoneInfo:実在する夏時間の重複時刻を拒否せず、欠けた時刻を検出する
UTC 変換結果との直接比較では、繰り返される時間帯の両方が無効と判定されることがあります。まず現地時刻の往復変換を確認し、曖昧さの解消と瞬間を表すキーの選択を分けます。

タイムゾーン付きの datetime でも、その現地の時計表示が実際に存在したとは限りません。Python は春の時計の進みで飛ばされた時間帯にも ZoneInfo datetime を作成できます。逆の誤りもあります。datetime を UTC に変換した値と直接比較する簡略な方法では、繰り返される時間帯の正当な値を存在しないと判定することがあります。
現地の時計表示が存在するか、利用者がどちらの回を意味したか、選んだ瞬間をどう保存・比較するか。この三つを分けて判断します。以下の確認方法は、時計を戻した際の二つの正当な値を残し、欠けた現地時刻を勝手に移動させずに拒否します。
1. 誤って拒否する比較を見つける
pytz-deprecation-shim の移行ガイドには、dt.astimezone(timezone.utc) == dt を使う標準ライブラリの存在確認ヘルパーがあります。その近くにある dateutil の手順は別の方法です。fold を変えると UTC オフセットが変わる正当な重複時刻では、この簡略な等価比較は失敗します。PEP 495 は、その異なるゾーン間の比較が意図的に False になると定めています。したがって False だけでは欠落時間帯だと証明できません。
CPython 3.12.14 で限定的に実行したテストでは、Europe/London の 2024-10-27 01:30 は fold=0 なら UTC 00:30、fold=1 なら UTC 01:30 に変換されました。どちらも実在する瞬間です。それぞれの UTC 変換結果との直接比較は、両方とも False でした。これはヘルパーの回帰ケースであり、ZoneInfo が重複時間帯を表現できないという意味ではありません。
2. 往復後の現地の時計表示を比較する
tzinfo が ZoneInfo インスタンスである datetime dt について、まず aware な値を要求します。dt.tzinfo が存在し、dt.utcoffset() が None ではないことを確認します。back = dt.astimezone(timezone.utc).astimezone(dt.tzinfo) を計算し、次に exists = back.replace(tzinfo=None) == dt.replace(tzinfo=None) とします。ここでは往復変換後の現地の日付と時計フィールドを意図的に比較しており、元の値を別のゾーンの値と直接比べていません。
exists が False なら入力を拒否するか、別途明記した製品ポリシーを適用します。dt を自動的に back に置き換えて検証と呼ばないでください。それでは存在しなかった入力を変更してしまいます。すでに dateutil を使っている場合は、文書化された datetime_exists(dt) と datetime_ambiguous(dt) を利用し、独自実装の保守を避けられます。採用する実装に同じテストケースを適用してください。
3. 存在を確認してから曖昧さを解消する
exists が True の後、dt.replace(fold=1-dt.fold).utcoffset() が dt.utcoffset() と異なれば、その ZoneInfo 値は曖昧です。先の回(fold=0)か後の回(fold=1)かを尋ねる、既存の選択を保持する、または明示的に選びます。fold を設定すること自体は検証ではありません。この存在確認は現地の時計表示が起きたかを調べるもので、重複時間帯以外で与えられたすべての fold ラベルを厳密に検証するものではありません。
上流の記録が UTC やオフセットで瞬間をすでに特定しているなら、その情報を保持し、オフセットのない現地文字列から推測し直さないでください。ZoneInfo は既存の瞬間を変換するときに fold を適切に設定します。利用者が入力する現地の予約時刻では、得られた瞬間とともにゾーンキーと曖昧さの扱いを保存します。補正を一時間に固定しないでください。遷移の幅と規則は選んだゾーンデータに依存します。
4. 四つの回帰ケースを残す
ZoneInfo('Europe/London') を使い、通常の 2024-10-26 01:30、2024-10-27 01:30 の二つの fold 値、欠けた 2024-03-31 01:30 を検証しました。往復による存在確認は順に True、True、True、False でした。存在確認を通った後だけ曖昧さを調べると、順に False、True、True、False でした。欠けた値は UTC から戻すと現地 02:30 となり、元の時計表示を維持できませんでした。
これらは過去の日付のテストであり、将来のすべての遷移の予測ではありません。アプリで使う Python バージョン、ゾーンキー、タイムゾーンデータの出所を記録し、実際の提供地域のケースを追加します。ZoneInfo はシステムのデータを読み、必要に応じて第一者提供の tzdata パッケージを使います。データ不足や無効なゾーンキーは別の初期化問題であり、夏時間の欠落として利用者の入力を拒否してはいけません。
5. 検証後に瞬間を表すキーを使う
入力が存在し、どちらの回かも決まったら、業務上の同一性が瞬間を意味する場合は dt.astimezone(timezone.utc) を使います。テストでは、同じ ZoneInfo オブジェクトを共有するロンドンの二つの重複値を集合に入れると、現地キーは一つになりました。先に UTC に変換すれば二つでした。現地値同士の差はゼロ秒、UTC 値同士の差は 3,600 秒でした。比較の意味が異なるため、保存方法は同等ではありません。
最後の変換は、イベントの時系列順、瞬間による重複排除、実際の経過時間の計算に役立ちます。現地時刻で繰り返す予定は、意図的に時計表示に従う場合があるため、その元のルールを残してください。今からキーを変えても、以前の衝突で上書きされたイベントは再構築できません。元の記録が残っていれば、そこから復元します。通常時刻、二つの重複時刻、欠落時刻がすべて期待どおりに処理されて初めて修正を受け入れます。
