闰日规则每年都被打破了吗?

Is the rrule YEARLY broken for leap days?

假设我想知道什么时候用规则庆祝生日。然后频率 YEARLY 工作正常,除了闰日。事实上,它每 4 年才会出现一次。

有什么办法直接用rrule处理吗?

from datetime import datetime
from dateutil.rrule import rrule, YEARLY

n = 1
print(list(rrule(freq=YEARLY, count=n + 1, dtstart=datetime(1990, 4, 28))))
print(list(rrule(freq=YEARLY, count=n + 1, dtstart=datetime(1992, 2, 29))))

给予

[datetime.datetime(1990, 4, 28, 0, 0), datetime.datetime(1991, 4, 28, 0, 0)]
[datetime.datetime(1992, 2, 29, 0, 0), datetime.datetime(1996, 2, 29, 0, 0)]

甚至没有提到闰日这一事实 in the docs 让我怀疑这是否只是一个错误。

按年

这可能有帮助,但仅限于 2 月 28 日:

from datetime import datetime
from dateutil.rrule import rrule, YEARLY

n = 5

bday = datetime(1990, 4, 28)
print(list(rrule(freq=YEARLY,
                 byyearday=bday.timetuple().tm_yday,
                 count=n + 1,
                 dtstart=bday)))

bday = datetime(1992, 2, 29)
print(list(rrule(freq=YEARLY,
                 byyearday=bday.timetuple().tm_yday,
                 count=n + 1,
                 dtstart=bday)))

给予

[datetime.datetime(1990, 4, 28, 0, 0), datetime.datetime(1991, 4, 28, 0, 0), datetime.datetime(1992, 4, 27, 0, 0), datetime.datetime(1993, 4, 28, 0, 0), datetime.datetime(1994, 4, 28, 0, 0), datetime.datetime(1995, 4, 28, 0, 0)]
[datetime.datetime(1992, 2, 29, 0, 0), datetime.datetime(1993, 3, 1, 0, 0), datetime.datetime(1994, 3, 1, 0, 0), datetime.datetime(1995, 3, 1, 0, 0), datetime.datetime(1996, 2, 29, 0, 0), datetime.datetime(1997, 3, 1, 0, 0)]

这是设计使然,实际上在 the rrule documentation 的注释中突出提到:

Per RFC section 3.3.10, recurrence instances falling on invalid dates and times are ignored rather than coerced:

Recurrence rules may generate recurrence instances with an invalid date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM on a day where the local time is moved forward by an hour at 1:00 AM). Such recurrence instances MUST be ignored and MUST NOT be counted as part of the recurrence set.

因为 1991 年 2 月 29 日不存在,它是一个无效的日期并被跳过。

这是过时的RFC 2445, which was later supplanted by RFC 5545, which is updated by RFC 7529. RFC 7529, among other things, adds the SKIP parameter to recurrence rules, which allows you to specify OMIT (default), BACKWARD or FORWARD. dateutil pre-dates RFC 7529 (and even RFC 5545), and is still in the process of being updated. You can track the progress on issue #285的限制。

此特定问题已在 PR #522 中解决,但该 PR 仍缺少对一个回退案例的支持,并且尚未(截至 2018 年 10 月)合并。

对于 returns 每年同一天的函数的简单情况,回溯到该月的最后一天,我建议改用 relativedelta (直到带有 SKIP 的版本功能已发布):

from dateutil import relativedelta
from datetime import datetime

def yearly_rule(dtstart, count=None):
    n = 0
    while count is None or n < count:
        yield dtstart + relativedelta.relativedelta(years=n)
        n += 1

if __name__ == "__main__":
    for dt in yearly_rule(datetime(1992, 2, 29), count=5):
        print(dt)

    # Prints:
    # 1992-02-29 00:00:00
    # 1993-02-28 00:00:00
    # 1994-02-28 00:00:00
    # 1995-02-28 00:00:00
    # 1996-02-29 00:00:00

请注意,我在我的规则中使用的是 基准日期时间 (dtstart),而不是在之前的结果上加上 1 年。这样做的原因是 relativedelta 是有损的,所以将 relativedelta(years=1) 添加到 datetime(1995, 2, 28) 将得到 datetime(1996, 2, 28).