Как обращаться с DST и TZ в повторяющихся событиях?

Поддерживает ли 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>)

Ответ 1

@asdf: я не могу добавить код в комментарии, поэтому мне нужно опубликовать это как ответ:

Я боюсь, что с вашим решением я всегда потеряю информацию о 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>)

Как вы можете видеть, разница между результатом rrule и реальными данными, хранящимися в БД, составляет 1 час.

Ответ 2

Обратите внимание, что то, что возвращает django.utils.timezone.now(), может быть либо наивным, либо осведомленным временем datetime, в зависимости от настроек USE_TZ. То, что вы должны использовать внутренне для вычислений (например, now, которое вы предоставляете rrule.rrule), является дата-временем в формате UTC. Он может быть смещенным (т.е. datetime.now(pytz.UTC)) или наивным (т.е. datetime.utcnow()). Последнее кажется предпочтительным для хранения (см. этот блогпост).

Теперь rrule.rrule обрабатывает временные интервалы, поэтому вы наблюдаете изменение CEST-to-CET в том, что дает доход от rrule. Однако, если вы хотите всегда получать один и тот же час (например, 0 AM каждый день, независимо от DST или нет), тогда вы действительно хотите "игнорировать" изменение. Один из способов сделать это - сделать dt = dt - dt.dst(), если 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)

# 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 вместо наивно-предполагаемого UTC, как в примере, вы просто пропустили часть .replace. Быстрый обход об этих конверсиях можно найти здесь.

Ответ 3

Да, дело в том, что вы не должны хранить локальное время, когда-либо. Сохраняйте UTC и конвертируйте в местное время по запросу (т.е. На основе запроса, используя данные запроса, например заголовок Accept-Language, чтобы узнать, что вы должны использовать tz).

Что вы делаете, так это то, что вы используете локализованное datetime для вычислений (т.е. rrule.rrule()). Это субоптимально, так как вам нужно знать целевой часовой пояс, чтобы сделать это, поэтому это можно сделать только для запроса, в отличие от предварительной калькуляции реализаций rrule. Именно поэтому вы должны использовать UTC внутренне (то есть, чтобы предварительно вычислить даты), а затем преобразовать их перед отправкой пользователю. В этом случае после получения запроса необходимо будет выполнить преобразование (то есть, когда известен часовой пояс).