I am using python's dateutil module to parse recurring rules in my calendar. A problem arises with the following rrule:
from dateutil.rrule import 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 ))
This results in the following output (missing February):
datetime: 2018-01-30 18:00:00
datetime: 2018-03-30 18:00:00
Is this a bug in dateutil
module and how should I fix it? Or am I doing something wrong?
Per my answer on this equivalent question, this is a deliberate feature of the iCalendar RFC that dateutil
is implementing, because dateutil
implements RFC 2445 and does not support all (or most) of the features of the updated RFC 5545. The relevant section of RFC 2445:
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.
February is missing because 2018-02-30
is an invalid date (it's actually the example specified in the RFC).
One thing to note is that this pull request implements the functionality you want, but it is (as of this writing) currently blocked waiting for support of SKIP
in BYWEEKNO
. After that is merged, you will be able to modify your RRULE:
rrule = ('FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=30;UNTIL=20180331T2359;'+
'SKIP=BACKWARD;RSCALE=GREGORIAN')
Until then, your best option may be to use a BYMONTHDAY=28
and then add a relativedelta(day=30)
to the result, e.g.:
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)
This works because the 28th exists in all months (so the rrule
will always generate it) and relativedelta
has the "fall backwards at end of month" behavior that you are looking for. To be 100% safe, you can choose bymonthday=1
instead, it is equivalent in this case.