如何检查日期时间值是否有效(而不是在 DST 更改引起的时间间隔内)?

How to check if a datetime value is valid (and not in time gap caused by DST change)?

提问人:Niko Fohr 提问时间:10/17/2023 最后编辑:FObersteinerNiko Fohr 更新时间:10/18/2023 访问量:53

问:

夏令时 (DST) 更改通常每年发生两次。在秋季,时钟向后移动,这会产生折叠,这意味着给定的当地时间具有两种可能的含义。这在 PEP-495 中有所涵盖。在春季,时钟向前移动,这在可能的本地时间值中产生间隙,通常长度为一小时。

例如,在“欧洲/伦敦”时区中,2023-03-26 接近 DST 更改的可能分钟数为:00:58、00:59、02:00、02:01;值 01:00 到 01:59 不存在。

以下是应检测为有效(可能)和不可能的情况示例:

import datetime as dt 
from zoneinfo import ZoneInfo 

timestamp_possible = dt.datetime.strptime(
        "2023-03-26 00:30:00", "%Y-%m-%d %H:%M:%S"
    ).replace(tzinfo=ZoneInfo("Europe/London"))

timestamp_impossible = dt.datetime.strptime(
        "2023-03-26 01:30:00", "%Y-%m-%d %H:%M:%S"
    ).replace(tzinfo=ZoneInfo("Europe/London"))

以下是我如何使用 pandas.to_datetime 来做到这一点,但我不完全确定它为什么有效,以及这是否是 pandas 的实现细节(可能会改变?)或保证行为。

def is_valid(timestamp):
    return pd.to_datetime(timestamp).to_pydatetime(timestamp) == timestamp

这给了

>>> is_valid(timestamp_possible)
True
>>> is_valid(timestamp_impossible)
False

问题

检测给定对象是否有效或是否达到“不存在的时间值”差距的最简单方法是什么?在 Python 标准库中有什么方法可以做到这一点吗?datetime.datetime

python datetime 时区 DST

评论


答:

1赞 Niko Fohr 10/17/2023 #1

我仔细研究了 PEP-495,在“严格无效时间检查”部分实际上有一个非常有用的功能:

def utcoffset(dt, raise_on_gap=True, raise_on_fold=False):
    u = dt.utcoffset()
    v = dt.replace(fold=not dt.fold).utcoffset()
    if u == v:
        return u
    if (u < v) == dt.fold:
        if raise_on_fold:
            raise AmbiguousTimeError
    else:
        if raise_on_gap:
            raise MissingTimeError
    return u

修改它很容易,使其返回一个标志,该标志告诉时间戳是否在间隙处:

import enum

class DstFlag(enum.IntEnum):
    NONE = 0
    FOLD = 1
    GAP = 2
    
def get_dst_flag(timestamp: dt.datetime) -> DstFlag:
    u = timestamp.utcoffset()
    v = timestamp.replace(fold=not timestamp.fold).utcoffset()
    if u == v:
       return DstFlag.NONE 
    if (u < v) == timestamp.fold:
       return DstFlag.FOLD
    return DstFlag.GAP
    

这看起来不是很容易理解,但无论如何它看起来运行良好。一些测试值:(u < v) == timestamp.fold

# Spring DST Change 2023-03-26 01:00 UTC+0 (01:00 local time)
# 01:00 (UTC+) -> 02:00 (UTC+1)
dt_normal1 = dt.datetime.strptime(
        "2023-03-26 00:00:00", "%Y-%m-%d %H:%M:%S"
    ).replace(tzinfo=ZoneInfo("Europe/London"))
    
dt_withgap = dt.datetime.strptime(
        "2023-03-26 01:30:00", "%Y-%m-%d %H:%M:%S"
    ).replace(tzinfo=ZoneInfo("Europe/London"))
    
dt_normal2 = dt.datetime.strptime(
        "2023-03-26 03:00:00", "%Y-%m-%d %H:%M:%S"
    ).replace(tzinfo=ZoneInfo("Europe/London"))
    

# Fall DST Change 2023-10-29 01:00 UTC+0 (02:00 local time)
# 02:00 (UTC+1) -> 01:00 (UTC+0)
dt_normal3 = dt.datetime.strptime(
        "2023-10-29 00:00:00", "%Y-%m-%d %H:%M:%S"
    ).replace(tzinfo=ZoneInfo("Europe/London"))
    
dt_withfold = dt.datetime.strptime(
        "2023-10-29 01:30:00", "%Y-%m-%d %H:%M:%S"
    ).replace(tzinfo=ZoneInfo("Europe/London"))
    
dt_normal4 = dt.datetime.strptime(
        "2023-10-29 03:00:00", "%Y-%m-%d %H:%M:%S"
    ).replace(tzinfo=ZoneInfo("Europe/London"))

和测试返回值:

>>> get_dst_flag(dt_normal1)
<DstFlag.NONE: 0>
>>> get_dst_flag(dt_withgap)
<DstFlag.GAP: 2>
>>> get_dst_flag(dt_normal2)
<DstFlag.NONE: 0>
>>> get_dst_flag(dt_normal3)
<DstFlag.NONE: 0>
>>> get_dst_flag(dt_withfold)
<DstFlag.FOLD: 1>
>>> get_dst_flag(dt_normal4)
<DstFlag.NONE: 0>