Calendar recurrence
December 13, 2015 at 11:48 PM by Dr. Drang
Here are some followup™ things from yesterday’s post about OS X Calendar.
First, a couple of people asked why I made my own calendar of federal holidays when there’s a option to subscribe to one that’s already premade (and, presumably, accurate).
The simple answer is that I’m pretty sure I started using iCal back before there was such an option, although it’s possible my memory is wrong on that. The more complicated and more fully correct answer is that I don’t like Apple’s Holidays calendar because it has too many entries.
I’m not a hardcore Getting Things Done guy, but one of David Allen’s precepts that I’ve taken to heart is that my calendar should be the “hard landscape” of my schedule. To that end, I try to keep in it only those entries that absolutely affect where I’ll be, when I’ll be there, and what I’ll be doing. The corollary is that these are also the times when I can’t schedule anything else.
Some of my calendar entries are work related, like meetings, conference calls, and trips. Some are personal, like birthdays, vacations, concerts, and swim meets. Federal holidays go into the calendar because people seldom want to schedule business on those days. Other days, like Mother’s Day, Father’s Day, Easter, Good Friday, and the Daylight Saving Time changeovers, also make the cut.
Apple’s Holiday’s calendar includes too many entries that I don’t want in my calendar view. Groundhog Day is a perfectly fine day, and it’s fun to see on a calendar hanging in the kitchen, but I’m never going to need to schedule a vacation or a business trip around it. Because I can’t pick and choose among the entries in a subscribed calendar, I have to make my own. I’m not, by the way, a hater of “this day in history” calendars, but I want to be able to hide them during normal day-to-day use.1
The second followup topic is calendar recurrence rules. The DST changeover dates and many of the federal holidays do not occur on fixed days of the year, but on days governed by rules like “the last Sunday in March.” Apple’s Calendar has a good set of recurrence rules for these types of floating entries, under the
entry of the repeat popup menu. Here’s how to enter a yearly floating event.The day of the week and count values have their own popup menus, which give you most of the options you might need.
These rules also work well for common business meeting schedules, like “second Friday of every month.”
Unfortunately, Calendar’s recurrence rules can’t be used to make entries for Easter or any of the holidays related to it. The Easter algorithm isn’t especially complicated, but it is unique and requires a purpose-built function. Luckily, such functions have already been written for several programming languages.2
At this point, longtime readers of the blog will expect me to mention Reingold and Dershowitz’s masterful Calendrical Calculations, and I would never disappoint longtime readers. It is one of the great recreational programming books and is all the more fun because the programs are written in Lisp. But because my Lisp is a little rusty and I’ve never liked its input/output facilities, I decided to generate my Easter/Good Friday calendar in Python using the dateutil
module.
The dateutil
module isn’t part of the Standard Python Library, but Apple does include it with OS X, so there’s no need to install anything special. It has an easter
submodule and, better yet, an Easter setting in its recurrence rules submodule. Here’s a simple script for generating an .ics
file with all the Easters and Good Fridays through 2036.
python:
1: #!/usr/bin/env python
2:
3: from dateutil.rrule import *
4: from datetime import date
5:
6: # Easters and Good Fridays, 2016-2036, inclusive.
7: easters = list(rrule(YEARLY, byeaster=0,
8: dtstart=date(2016,1,1), until=date(2037,1,1)))
9: goodfridays = list(rrule(YEARLY, byeaster=-2,
10: dtstart=date(2016,1,1), until=date(2037,1,1)))
11:
12: entry = '''BEGIN:VEVENT
13: SUMMARY:Good Friday
14: DTSTART;VALUE=DATE:{1}
15: DTEND:VALUE=DATE:{1}
16: END:VEVENT
17: BEGIN:VEVENT
18: SUMMARY:Easter
19: DTSTART;VALUE=DATE:{0}
20: DTEND:VALUE=DATE:{0}
21: END:VEVENT'''
22:
23: print '''BEGIN:VCALENDAR
24: VERSION:2.0'''
25:
26: for e, f in zip(easters, goodfridays):
27: print entry.format(e, f)
28:
29: print 'END:VCALENDAR'
The byeaster
parameter of the rrule
function takes an integer value that sets how many days after (for positive values) or before (for negative values) Easter the generated dates will be. The generated dates are then output in the format required by the iCalendar specification. The output file, if saved to a file with an .ics
extension, can be imported into Calendar.
My “special days” calendar, which has the DST dates and non-federal holidays, also had several years worth of Easters and Good Fridays. I noticed, though, during the recent unpleasantness, that the Easters and Good Fridays I’d generated back in 2005 or so ran out in 2015. So this time I generated two decades worth instead of just one. Expect another followup post in 2036.
Another deficiency in Calendar’s recurrence rules is that they can’t generate US federal election dates. Elections here are held on the first Tuesday after the first Monday in November, and that’s just out of the reach of Calendar’s rules. But it’s easy with dateutil
.
python:
1: #!/usr/bin/env python
2:
3: from dateutil.rrule import *
4: from dateutil.relativedelta import relativedelta
5: from datetime import date
6:
7: # US Presidential and Congressional elections, 2016-2036, inclusive.
8: # First Tuesday after the first Monday in November in years divisible by 2.
9: firstmondays = list(rrule(YEARLY, interval=2,
10: dtstart=date(2016, 1, 1), until=date(2037, 1, 1),
11: byweekday=MO(+1), bymonth=11))
12: elections = [ d + relativedelta(days=+1) for d in firstmondays ]
13:
14: entry = '''BEGIN:VEVENT
15: SUMMARY:Election Day
16: DTSTART;VALUE=DATE:{0}
17: DTEND:VALUE=DATE:{0}
18: END:VEVENT'''
19:
20: print '''BEGIN:VCALENDAR
21: VERSION:2.0'''
22:
23: for day in elections:
24: print entry.format(day.strftime('%Y%m%d'))
25:
26: print 'END:VCALENDAR'
Lines 9–11 generate the first Monday (byweekday=MO(+1)
) of November (bymonth=11
) every other year (interval=2
). Line 12 then increments all the generated values by one day. The rest of the script is basically the same as the Easter script.
If you scroll through the examples in the dateutil
documentation, you’ll see a similar rrule
invocation that generates election days in one step. It looks like this:
python:
list(rrule(YEARLY, interval=2,
dtstart=date(2016, 1, 1), until=date(2037, 1, 1),
byweekday=TU, bymonthday=(2,3,4,5,6,7,8), bymonth=11))
This generates a Tuesday in November that falls on the 2nd through the 8th of the month. This is equivalent to saying “the first Tuesday after the first Monday,” but isn’t as obvious. I chose to be obvious rather than clever.