RRULE expansion in Ruby

On the Square Appointments team, we often need to deal with events that repeat — from simple cases (like a weekly lunch meeting) to more complex ones (like a haircut every other month on the second-to-last Tuesday, except for September 19th). Fortunately, the iCalendar specification provides a compact way to represent all of these cases. The challenge for us is to turn a text RRULE into a set of actual date occurrences that we can use in our application. Given Ruby’s vast gem ecosystem, we expected to find something right away to perform this task for us, but couldn’t find anything that was exactly what we needed.

The existing Ruby tools didn’t quite fit

The Ruby ecosystem already contained great gems that deal with recurrences — ice_cube is probably the most popular. ice_cube allows you to create recurrences via a fluent Ruby interface, and can convert a recurrence created in Ruby into its RRULE text equivalent. Unfortunately for us, our problem is a little different. We’re primarily processing RRULEs embedding in recurring events imported from Google Calendar and turning them into a set of Ruby Time objects — essentially the reverse of the problem ice_cube is designed to solve. ice_cube does provide some ability to parse RRULEs but doesn’t support some options (like BYSETPOS, the ability to specify things like “every second and fourth Wednesday in a month”), and we couldn’t see a way to incorporate the features we needed without significant rework of the gem. Since we deal with external data from Google that isn’t under our control and could potentially contain any valid RRULE, we needed to support as close to the full set of the iCalendar spec as possible.

There were libraries that did exactly what we needed — but unfortunately, they were in Python and JS. And if you’ve ever written date/time code, you know that it’s very easy to get it wrong — off-by-one errors abound (is your list of months 0-indexed or 1-indexed?). Rewriting one of these libraries into Ruby without introducing subtle errors seemed like a daunting task.

Exhaustive tests to the rescue

Luckily, we were trying to solve a well-bounded problem that lent itself quite well to a data-driven test suite — and we had an already-written set of tests that our mobile team had used months before to test yet another RRULE parsing implementation in Objective-C. This test suite covered just about every combination of RRULE options we could think of.

This test suite gave us the confidence to do a line-by-line rewrite of Python’s dateutil library (adding in some Ruby idioms when they were obviously applicable). I’ll admit that when writing this first version of our new library, I didn’t understand most of what I was transcribing — but it didn’t matter. As long as the tests passed, I knew that it worked. Once we’d gotten to green, we were then able to refactor into something much more understandable and idiomatically Ruby, confident that our extensive tests would keep us safe.

The final product

We’ve just released our internal library as rrule — a small gem focused entirely on the specific task of generating Time instances from a given RRULE and recurrence start date. It allows you to do things like:

rule = RRule::Rule.new(‘FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH’, dtstart: Time.parse(‘Tue Sep 2 06:00:00 PDT 1997’), tzid: ‘America/New_York’)
=> [Tue, 02 Sep 1997 09:00:00 EDT -04:00,
Thu, 04 Sep 1997 09:00:00 EDT -04:00,
Tue, 16 Sep 1997 09:00:00 EDT -04:00,
Thu, 18 Sep 1997 09:00:00 EDT -04:00,
Tue, 30 Sep 1997 09:00:00 EDT -04:00,
Thu, 02 Oct 1997 09:00:00 EDT -04:00,
Tue, 14 Oct 1997 09:00:00 EDT -04:00,
Thu, 16 Oct 1997 09:00:00 EDT -04:00]

We’ve been using it in production for about a year at Square, so we’re confident in its accuracy. Right now it supports all of the cases that we’ve encountered in Google data, but we’d like to eventually go further and support the entire iCalendar spec (which can specify repeats down to the level of seconds). If you’re writing a Ruby app and are dealing with RRULEs, we hope that rrule might be helpful to you. You can find the source code at https://github.com/square/ruby-rrule.