如何处理重复事件中的 DST 和 TZ?

Posted

技术标签:

【中文标题】如何处理重复事件中的 DST 和 TZ?【英文标题】:How to handle DST and TZ in recurring events? 【发布时间】:2012-09-12 07:47:48 【问题描述】:

dateutil rrule 是否支持 DST 和 TZ?需要类似于 iCalendar RRULE 的东西。

如果不是 - 如何解决此问题(安排重复事件和 DST 偏移更改)

进口

>>> from django.utils import timezone
>>> import pytz
>>> from datetime import timedelta
>>> from dateutil import rrule
>>> now = timezone.now()
>>> pl = pytz.timezone("Europe/Warsaw")

timedelta 的问题(需要具有相同的本地时间,但不同的 DST 偏移量):

>>> pl.normalize(now)
datetime.datetime(2012, 9, 20, 1, 16, 58, 226000, tzinfo=<DstTzInfo 'Europe/Warsaw' CEST+2:00:00 DST>)    
>>> pl.normalize(now+timedelta(days=180))
datetime.datetime(2013, 3, 19, 0, 16, 58, 226000, tzinfo=<DstTzInfo 'Europe/Warsaw' CET+1:00:00 STD>)

rrule 的问题(每次发生的每个本地小时都需要相同):

>>> r = rrule.rrule(3,dtstart=now,interval=180,count=2)
>>> pl.normalize(r[0])
datetime.datetime(2012, 9, 20, 1, 16, 58, tzinfo=<DstTzInfo 'Europe/Warsaw' CEST+2:00:00 DST>)
>>> pl.normalize(r[1])
datetime.datetime(2013, 3, 19, 0, 16, 58, tzinfo=<DstTzInfo 'Europe/Warsaw' CET+1:00:00 STD>)

【问题讨论】:

关于夏令时和时区的最佳做法,***.com/q/2532729/1167333 提供了一个很好的最佳做法总结 【参考方案1】:

是的,关键是您永远不应该存储本地时间。存储 UTC 并按需转换为本地时间(即,基于每个请求,使用请求数据,如 Accept-Language 标头,以了解您应该使用什么 tz)。

您正在做的是使用本地化的日期时间进行计算(即rrule.rrule())。这是次优的,因为您需要知道目标时区才能做到这一点,因此只能根据请求完成,而不是预先计算 rrule 实现。这就是为什么您应该在内部使用 UTC(即预先计算日期时间),然后在发送给用户之前对其进行转换。在这种情况下,只需在收到请求后(即目标时区已知)进行转换。

【讨论】:

【参考方案2】:

@asdf:我无法向 cmets 添加代码,所以我需要将其发布为答案:

恐怕用你的解决方案我总是会丢失 DST 信息,因此半年的重复将是 1 小时的休息时间。

根据您的回答,我发现这可能是正确的解决方案:

>>> from datetime import datetime
>>> import pytz
>>> from dateutil import rrule
>>> # this is raw data I get from the DB, according to django docs I store it in UTC
>>> raw = datetime.utcnow().replace(tzinfo=pytz.UTC)
>>> # in addition I need to store the timezone so I can do dst the calculations
>>> tz = pytz.timezone("Europe/Warsaw")
>>> # this means that the actual local time would be
>>> local = raw.astimezone(tz)
>>> # but rrule doesn't take into account DST and local time, so I must convert aware datetime to naive
>>> naive = local.replace(tzinfo=None)
>>> # standard rrule
>>> r = rrule.rrule(rrule.DAILY,interval=180,count=10,dtstart=naive)
>>> for dt in r:
>>>     # now we must get back to aware datetime - since we are using naive (local) datetime, 
        # we must convert it back to local timezone
...     print tz.localize(dt)

这就是我认为您的解决方案可能会失败的原因:

>>> from datetime import datetime
>>> from dateutil import rrule
>>> import pytz
>>> now = datetime.utcnow()
>>> pl = pytz.timezone("Europe/Warsaw")
>>> r = rrule.rrule(rrule.DAILY, dtstart=now, interval=180, count=2)
>>> now
datetime.datetime(2012, 9, 21, 9, 21, 57, 900000)
>>> for dt in r:
...     local_dt = dt.replace(tzinfo=pytz.UTC).astimezone(pl)
...     print local_dt - local_dt.dst()
...     
2012-09-21 10:21:57+02:00
2013-03-20 10:21:57+01:00
>>> # so what is the actual local time we store in the DB ?
>>> now.replace(tzinfo=pytz.UTC).astimezone(pl)
datetime.datetime(2012, 9, 21, 11, 21, 57, 900000, tzinfo=<DstTzInfo 'Europe/Warsaw' CEST+2:00:00 DST>)

如您所见,规则结果与我们存储在数据库中的真实数据之间存在 1 小时的差异。

【讨论】:

这似乎是对的,但我不敢相信没有更好的方法来实现它。 一整天都在敲我的脑袋——谢谢@g00fy! 哇,不错!不过,由于我们对规则的约束如此之大,我想我会做一个 PR,或者如果我不能 PR,我会实施我自己的。【参考方案3】:

请注意,django.utils.timezone.now() 返回的内容可以是原始日期时间,也可以是已知日期时间,具体取决于您的 USE_TZ 设置。您应该在内部用于计算(例如,您提供给 rrule.rrule 的 now)是基于 UTC 的日期时间。它可以是一个偏移感知的(即datetime.now(pytz.UTC)),也可以是一个幼稚的(即datetime.utcnow())。后者似乎更适合存储(参见this blogpost)。

现在,rrule.rrule 处理时区,这就是您观察 rrule 产生的 CEST 到 CET 变化的原因。但是,如果您想要始终获得相同的时间(例如,每天凌晨 0 点,无论是否为 DST),那么您实际上想要“忽略”更改。如果dt 是一个有意识的日期时间,那么这样做的一种方法是使用dt = dt - dt.dst()

您可以这样做:

from datetime import datetime
from dateutil import rrule
import pytz
now = datetime.utcnow()
pl = pytz.timezone("Europe/Warsaw")
r = rrule.rrule(rrule.DAILY, dtstart=now, interval=180, count=2)

# will yield naive datetimes, assumed UTC
for dt in r:
    # convert from naive-UTC to aware-local
    local_dt = dt.replace(tzinfo=pytz.UTC).astimezone(pl)
    # account for the dst difference
    print local_dt - local_dt.dst()

这会打印两个日期时间,每个日期时间都在不同的时区(嗯,不同的 DST 设置),都代表相同的挂钟时间。 如果您要处理有意识的UTC-datetimes 而不是像示例中那样的naive-assumed-UTC,您只需跳过 .replace 部分。 可以在here 找到有关这些转换的快速备忘单。

【讨论】:

以上是关于如何处理重复事件中的 DST 和 TZ?的主要内容,如果未能解决你的问题,请参考以下文章

如何处理谷歌表格脚本中的重复项?

我应该如何处理逻辑编程中的重复更新?

如何处理片段中的onClick [重复]

Java:如何处理 servlet 中的多个会话 [重复]

如何处理错误:ER_DUP_ENTRY:nodejs 中的重复条目

如何处理java.lang.ArrayIndexOutOfBoundsException:java中的0 [重复]