Using RecurrenceField¶
Getting occurrences between two dates¶
Once you’ve created a model with a RecurrenceField, you’ll
probably want to use it to figure out what dates are involved in a
particular recurrence pattern.
Note
Whether you want to use between or occurrences will depend on what sort of rules you’re dealing
with. In reality, a model like Course probably has rules like
“every Thursday for 10 weeks”, so occurrences will work fine,
since the rules have natural limits. For other uses (e.g. find me
every date this year for a club that runs every Wednesday), you’ll
want to use between.
between takes two dates (the start and end date), and returns a list
of datetime objects matching the recurrence pattern between those
dates. It is used like this (using the Course model from above):
from datetime import datetime
from myapp.models import Course
course = Course.objects.get(pk=1)
course.recurrences.between(
datetime(2010, 1, 1, 0, 0, 0),
datetime(2014, 12, 31, 0, 0, 0)
)
This won’t include occurrences if they occur on the start and end
dates specified. If you want to include those, pass in inc like
this:
course.recurrences.between(
datetime(2010, 1, 1, 0, 0, 0),
datetime(2014, 12, 31, 0, 0, 0),
inc=True
)
Warning
Slightly confusingly, between will only return you dates after
the current date, if used as above (provided those dates fall
between the two first parameters to between). Read on for how
to get all the occurrences between two dates.
To get all the occurrences between two dates (including dates that
are before the current time, but after the provided start date),
you’ll also need to set dtstart, like this:
course.recurrences.between(
datetime(2010, 1, 1, 0, 0, 0),
datetime(2014, 12, 31, 0, 0, 0),
dtstart=datetime(2010, 1, 1, 0, 0, 0),
inc=True
)
That will get you all occurrences between 1st January 2010, and 31st December 2014, including any occurrences on 1st January 2010 and 31st December 2014, if your recurrence pattern matches those dates.
The effective starting date for any recurrence pattern is essentially
the later of the first argument and dtstart. To minimize
confusion, you probably want to set them both to the same value.
Warning
Note that per default dtstart will be the first occurence in
your list if specified, according to RFC 2445. This practice
deviates from how dateutil.rrule handles dtstart and can
therefore lead to confusion. Read on for how you can control this
behavior for your own recurrence patterns.
To switch off the automatic inclusion of dtstart into the
occurence list, set include_dtstart=False as an argument for the
RecurrenceField whose behavior you want to change:
class Course(models.Model):
title = models.CharField(max_length=200)
recurrences = RecurrenceField(include_dtstart=False)
With this change any dtstart value will only be an occurence if
it matches the pattern specified in recurrences. This also works
for instantiating Recurrence objects directly:
pattern = recurrence.Recurrence(
rrules=[recurrence.Rule(recurrence.WEEKLY, byday=recurrence.MONDAY)],
include_dtstart=False).between(
datetime(2010, 1, 1, 0, 0, 0),
datetime(2014, 12, 31, 0, 0, 0),
dtstart=datetime(2010, 1, 1, 0, 0, 0),
inc=True
)
)
Getting all occurrences¶
occurrences is particularly useful where your recurrence pattern
is limited by the rules generating occurrences (e.g. “every Tuesday
for 10 weeks”, or “every Tuesday until 23rd April 2014”).
You can get a generator which you can iterate over to get all
occurrences using occurrences:
dates = course.recurrences.occurrences()
You can optionally provide dtstart to specify the first
occurrence, and dtend to specify the final occurrence.
You can index into the returned object, to (for example) get the first session of our course model:
dates = course.recurrences.occurrences()
first_instance = dates[0]
Warning
Looping over the entire generator returned by example above might
be extremely slow and resource hungry if dtstart or dtend
are not provided. Without dtstart, we implicitly are looking
for occurrences after the current date. Without dtend, we’ll
look for all occurrences up to (and including) the year 9999,
which is probably not what you want. The the code above counts all
occurrences of our course from tomorrow until 31st December, 9999.
Counting occurrences¶
The function count works fairly similarly:
course.recurrences.count()
It is roughly equivalent to:
len(list(course.recurrences.occurrences()))
Note the warning in occurrences before using
count (or converting the generator returned by occurrences()
to a list), if you are not providing both dtstart and dtend.
Getting the next or previous occurrences¶
If you want to get the next or previous occurrence in a given
pattern, you can use after or before, respectively. As with
between, you can choose whether you want to be inclusive of the
datetime passed in by setting inc. If no next or previous
occurrence exists, None is returned.
course = Course.objects.get(pk=1)
# Get the first course on or after 1st January 2010 (this won't do
# quite what you expect)
course.recurrences.after(
datetime(2010, 1, 1, 0, 0, 0),
inc=True
)
As with between, if you don’t specify a dtstart, it will
implicitly be the current time, so the above code will, to be more
precise, give you the first course on or after 1st January 2010, or
on or after the current date, whichever is later. Since you probably
don’t want that behaviour, you’ll probably want to specify
dtstart, as follows:
course = Course.objects.get(pk=1)
# Get the first course on or after 1st January 2010
course.recurrences.after(
datetime(2010, 1, 1, 0, 0, 0),
inc=True,
dtstart=datetime(2010, 1, 1, 0, 0, 0),
)
For similar reasons, using before really requires that
dtstart is provided, to give a start date to the recurrence
pattern. This makes some sense if you consider a recurrence pattern
like “every Monday, occurring 5 times”. Without dtstart, it’s
unclear what before should return - since it’s impossible to know
whether the pattern has started, and if so when. For example, if it
started 5 years ago, before should return a date approximately 5
years ago, whereas if it started two weeks ago, before should
return the last Monday (or the provided date, if inc is True,
and the provided date is a Monday).
Getting textual descriptions of patterns¶
Recurrence patterns can have multiple rules for inclusion (e.g. every week, on a Tuesday) and exclusion (e.g. except when it’s the first Tuesday of the month), together with specific dates to include or exclude (regardless of whether they’re part of the inclusion or exclusion rules).
You’ll often want to display a simple textual description of the rules involved.
To take our Course example again, you can get access to the
relevant inclusion rules by accessing the rrules member of the
RecurrenceField attribute of your model (called recurrences
in our example, though you can call it whatever you like), and to the
exclusion rules by accessing the exrules member. From there you
can get textual descriptions, like this:
course = Course.objects.get(pk=1)
text_rules_inclusion = []
for rule in course.recurrences.rrules:
text_rules_inclusion.append(rule.to_text())
Similar code would work equally well for exrules.