Python dateutil — ошибка с выбором ближайшего дня с помощью BYMONTHDAY

Я использую модуль dateutil Python для анализа повторяющихся правил в моем календаре. Проблема возникает со следующим правилом:

из dateutil.rrule импортировать rrulestr

def test():
    rrule = 'FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=30;UNTIL=20180331T2359'
    dtstart = datetime.datetime(2018, 1, 1, 18, 0)
    dates = list(rrulestr(rrule + ';UNTIL=', dtstart = dtstart ))

Это приводит к следующему выводу (отсутствует февраль):

datetime: 2018-01-30 18:00:00
datetime: 2018-03-30 18:00:00

Является ли это ошибкой в ​​модуле dateutil и как ее исправить? Или я что-то не так делаю?


person Edgar Navasardyan    schedule 08.02.2018    source источник
comment
Возможный дубликат rule dateutils возвращает даты с разницей в 2 месяца   -  person Paul    schedule 08.02.2018


Ответы (1)


Согласно моему ответу на этот эквивалентный вопрос, это преднамеренная функция RFC iCalendar, которую реализует dateutil, потому что dateutil реализует RFC 2445 и не поддерживает все (или большинство) функций обновленного RFC 5545. Соответствующий раздел RFC 2445:

Правила повторения могут генерировать экземпляры повторения с недопустимой датой (например, 30 февраля) или несуществующим местным временем (например, 1:30 в день, когда местное время сдвинуто на час вперед в 1:00). Такие экземпляры повторения ДОЛЖНЫ игнорироваться и НЕ ДОЛЖНЫ учитываться как часть набора повторений.

Февраль отсутствует, потому что 2018-02-30 — недопустимая дата (на самом деле это пример, указанный в RFC).

Следует отметить, что этот запрос на вытягивание реализует нужные вам функции, но он (на момент написания этой статьи) в настоящее время заблокирован в ожидании поддержки SKIP в BYWEEKNO. После того, как это будет объединено, вы сможете изменить свой RRULE:

rrule = ('FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=30;UNTIL=20180331T2359;'+
         'SKIP=BACKWARD;RSCALE=GREGORIAN')

До тех пор лучшим вариантом может быть использование BYMONTHDAY=28, а затем добавление relativedelta(day=30) к результату, например:

from dateutil.rrule import rrule, MONTHLY
from dateutil.relativedelta import relativedelta

def end_of_month(dtstart, until):
    rr = rrule(freq=MONTHLY, interval=1, bymonthday=28,
               dtstart=dtstart, until=until)

    for dt in rr:
        yield dt + relativedelta(day=30)

Это работает, потому что 28-е число существует во всех месяцах (поэтому rrule всегда будет генерировать его), а relativedelta имеет поведение "откат назад в конце месяца", которое вы ищете. Чтобы быть на 100% безопасным, вы можете вместо этого выбрать bymonthday=1, в данном случае это эквивалентно.

person Paul    schedule 08.02.2018