【问题标题】:Dates and time zone codes around a DST changeDST 更改前后的日期和时区代码
【发布时间】:2014-11-19 05:38:16
【问题描述】:

我正在测试如何在夏令时更改前后计算和显示日期(使用时区代码)。

在英国,2014 年 3 月 30 日凌晨 1 点,我们进入 DST,然后从 GMT 转到 BST。时间从2014-03-30 00:59:59 GMT跳转到2014-03-30 02:00:00 BST

我遇到了一个奇怪的问题,用以下代码复制它:

import pytz
from datetime import datetime, time, timedelta

def is_dst(d, tz):
    assert d.tzinfo is None  # we want a naive datetime to localize
    return tz.localize(d).dst() != timedelta(0)

start_datetime = datetime(2014, 03, 30, 0, 0, 0)
tz = pytz.timezone('Europe/London')

# Increment using timedelta
print 'This doesn\'t work:'
d = start_datetime
for i in range(5):
    print str(d) + ' ' + tz.tzname(d, is_dst=is_dst(d, tz))
    d += timedelta(minutes=30)  # Add 30 minutes

# Increment by adding seconds to epoch
print 'This works:'
epoch = datetime.utcfromtimestamp(0)
timestamp = (start_datetime - epoch).total_seconds()
for i in range(5):
    d = datetime.fromtimestamp(timestamp)
    print str(d) + ' ' + tz.tzname(d, is_dst=is_dst(d, tz))
    timestamp += 30 * 60  # Add 30 minutes

输出是:

This doesn't work:
2014-03-30 00:00:00 GMT
2014-03-30 00:30:00 GMT
2014-03-30 01:00:00 GMT <- invalid time
2014-03-30 01:30:00 GMT <- invalid time
2014-03-30 02:00:00 BST
This works:
2014-03-30 00:00:00 GMT
2014-03-30 00:30:00 GMT
2014-03-30 02:00:00 BST
2014-03-30 02:30:00 BST
2014-03-30 03:00:00 BST

我已经在输出上标记了无效时间的位置。挂钟上不存在这些时间,2014 年 3 月 30 日没有凌晨 1 点或 1:30,所以我不确定为什么要显示它。

相同的过程但以略微不同的方式完成会产生正确的结果。这是为什么呢?

【问题讨论】:

    标签: python datetime timezone pytz


    【解决方案1】:

    这里是@Matt Johnson's answer根据my comments修改的:

    from datetime import datetime, timedelta
    import pytz
    
    tz = pytz.timezone('Europe/London')
    #NOTE: is_dst=None asserts that the local time exists and unambiguous
    start_datetime = tz.localize(datetime(2014, 03, 30, 0, 0, 0), is_dst=None)
    
    # increment using timedelta
    print 'This works:'
    d = start_datetime.astimezone(pytz.utc) # use UTC to do arithmetic
    for _ in range(5):
        local = d.astimezone(tz) # use local timezone for display only
        print("\t{:%F %T %Z%z}".format(local))
        d += timedelta(minutes=30) # works in UTC
    
    # increment by adding seconds to epoch
    print 'This works too:'
    epoch = datetime(1970, 1, 1, tzinfo=pytz.utc)
    timestamp = (start_datetime - epoch).total_seconds()
    for i in range(5):
        local = datetime.fromtimestamp(timestamp, tz)
        print("\t{:%F %T %Z%z}".format(local))
        timestamp += 30 * 60  # add 30 minutes
    

    【讨论】:

    • 谢谢。我一直很感激你对细节的关注。因此,如果我理解正确,您是在提倡始终使用 UTC 进行数学运算,对吗?总的来说,我认为这是一个很好的做法,但我认为normalize 可以缓解大部分问题。我很欣赏如果传递了无效的本地时间,传递is_dst=None 将导致NonExistentTimeError。我很惊讶这是必需的。无论该设置如何,我都希望出现异常。无论如何,谢谢!
    • @MattJohnson:您的代码中的实际错误在my last comment 中描述,其他一切都只是吹毛求疵。
    • @MattJohnson: normalize 也有效(它本质上是astimezone() 的变体,适用于无效日期)。偏好处于故障模式:如果我在代码中忘记了astimezone(),那么最糟糕的情况是我看到的是UTC日期而不是本地日期(日期时间是已知的;输出中有明确的+0000)。如果你忘记了normalize(),那么你可能会走错时间。
    • 对,所以假设输入值是明确的,那么一切都很好。但是如果输入是可变的,那么检查不明确或无效的时间、引发错误或让用户选择他们想要的不明确值的实例就很重要。
    • @MattJohnson:在秋季尝试围绕 DST 转换的代码。将结果与我的代码进行比较(我没有这样做,但我认为您的代码应该会失败)。
    【解决方案2】:

    实际上,两个 部分的代码都不正确。在我的机器上运行你的确切代码(在美国太平洋时区),底部返回:

    2014-03-29 17:00:00 GMT
    2014-03-29 17:30:00 GMT
    2014-03-29 18:00:00 GMT
    2014-03-29 18:30:00 GMT
    2014-03-29 19:00:00 GMT
    

    这是因为fromtimestamp 在未指定时使用计算机的本地 时区。如果我只是将fromtimestamp 切换到utcfromtimestamp,它将使用一个幼稚的值 - 然后将结果放入正确的时区,但它给出的结果与第一部分相同 - 显示两个无效时间。

    使用 pytz timezone 实例中的 normalize 方法解决了该问题。不幸的是,您将 d 保留为 naive 日期时间,因此如果不先对其进行本地化,就无法对其进行规范化,完成后您将不得不再次使其变得幼稚。这有效,但会产生一些混乱的代码:

    # Increment using timedelta
    print 'This works:'
    d = start_datetime
    for i in range(5):
        print str(d) + ' ' + tz.tzname(d, is_dst=is_dst(d, tz))
        d += timedelta(minutes=30)  # Add 30 minutes
        d = tz.normalize(tz.localize(d)).replace(tzinfo=None)
    
    # Increment by adding seconds to epoch
    print 'This works too:'
    epoch = datetime.utcfromtimestamp(0)
    timestamp = (start_datetime - epoch).total_seconds()
    for i in range(5):
        d = tz.normalize(datetime.fromtimestamp(timestamp, pytz.utc).astimezone(tz)).replace(tzinfo=None)
        print str(d) + ' ' + tz.tzname(d, is_dst=is_dst(d, tz))
        timestamp += 30 * 60  # Add 30 minutes
    

    输出:

    This works:
    2014-03-30 00:00:00 GMT
    2014-03-30 00:30:00 GMT
    2014-03-30 02:00:00 BST
    2014-03-30 02:30:00 BST
    2014-03-30 03:00:00 BST
    This works too:
    2014-03-30 00:00:00 GMT
    2014-03-30 00:30:00 GMT
    2014-03-30 02:00:00 BST
    2014-03-30 02:30:00 BST
    2014-03-30 03:00:00 BST
    

    当然,整个事情可以通过提前使用 aware 日期时间来简化:

    import pytz
    from datetime import datetime, time, timedelta
    
    tz = pytz.timezone('Europe/London')
    start_datetime = tz.localize(datetime(2014, 03, 30, 0, 0, 0))
    
    # Increment using timedelta
    print 'This works:'
    d = start_datetime
    for i in range(5):
        print str(d) + ' ' + d.tzname()
        d = tz.normalize(d + timedelta(minutes=30))  # Add 30 minutes
    
    # Increment by adding seconds to epoch
    print 'This works too:'
    epoch = datetime.fromtimestamp(0, pytz.utc)
    timestamp = (start_datetime - epoch).total_seconds()
    for i in range(5):
        d = datetime.fromtimestamp(timestamp, pytz.utc).astimezone(tz)
        print str(d) + ' ' + d.tzname()
        timestamp += 30 * 60  # Add 30 minutes
    

    输出:

    This works:
    2014-03-30 00:00:00+00:00 GMT
    2014-03-30 00:30:00+00:00 GMT
    2014-03-30 02:00:00+01:00 BST
    2014-03-30 02:30:00+01:00 BST
    2014-03-30 03:00:00+01:00 BST
    This works too:
    2014-03-30 00:00:00+00:00 GMT
    2014-03-30 00:30:00+00:00 GMT
    2014-03-30 02:00:00+01:00 BST
    2014-03-30 02:30:00+01:00 BST
    2014-03-30 03:00:00+01:00 BST
    

    请注意,您不再需要 is_dst 函数,因为您现在可以直接从感知 datetime 实例中获取 tzname

    【讨论】:

    • 太好了,感谢您对 normalize 的提醒!关于在有意识的datetime 上使用tzname,似乎只调用self.tzinfo.tzname(self)tznameis_dst 参数留给Nonepytz docs 说这会在不明确的日期引发错误。歧义是否只发生在幼稚的datetimes
    • 是的,文档在这一点上并不清楚。整个部分主要是关于根据timezone 实例检查幼稚的datetime 值。如果您在具有感知 datetimetimezone 实例上调用 tzname,您将收到错误“Not naive datetime (tzinfo is already set)”。感知值不能真正模棱两可,因为它们知道它们与 UTC 的偏移量。还请考虑,如果您知道您传递的是一个模棱两可的幼稚值,您可以将 is_dst=True/False 传递给 localize 方法。如果你没有通过,它假定False
    • tz.localize() together 可能会返回不存在的时间。如果您确定输入的原始日期时间对象指的是存在于 tz 时区中的时间,则使用 is_dst=None 来断言。
    • 你可以使用datetime.fromtimestamp(timestamp, tz)
    • @MichaelWaterfall:请注意,第一个(“混乱”)代码示例不正确。如果开始日期是 2014-10-26,则会失败
    猜你喜欢
    • 2011-12-24
    • 1970-01-01
    • 1970-01-01
    • 2014-04-15
    • 1970-01-01
    • 2011-06-06
    • 2017-05-02
    • 1970-01-01
    • 2015-03-16
    相关资源
    最近更新 更多