The Complex World of Calendars

Warren Day
tomorrowapp
Published in
4 min readMay 26, 2020

Calendars help organise our life and provide the backbone for many organisations. They are a simple and intuitive way to manage both repetitive and single occurrence events. But this simplicity is more complex than you may realise. Through creating the class scheduling app https://www.tomorrowapp.io I have dealt with many edge cases in producing a solid and consistent experience to the end-user.

Defining our requirements

Let’s take a simple scenario, you want to schedule an event every week at 2 pm. This is easy to solve and cron may be the obvious choice.

But cron falls short when you consider more complex scenarios. Let's say I want to schedule a class to happen every Saturday at 8:30 am and 6:00 pm. This is not possible with a single cron entry so you would need to build a system which can manage and process multiple entries. Not fun.

It becomes more complex still; because nothing ever goes according to plan. If the event host becomes ill or has a clash with another event, you need to be able to cancel or modify a single occurrence without affecting the overall schedule.

So we have three main requirements:

  1. Schedule an event for multiple days and times.
  2. Cancel single occurrences
  3. Move or modify single occurrences

The solution

You may not have heard of the iCalendar specification (this has nothing to do with Apple). It’s an open standard for exchanging calendar and scheduling information between users and computers. Part of this specification is RRule, and it is this which can solve our problems. The spec is used by popular apps such as Google Calendar.

An RRule or recurrence rule is a string representation of a schedule which can be parsed by a program to provide each occurrence in the form of a date.

A simple example would look as follows:

FREQ=DAILY;INTERVAL=1

A more more complex scenario would look like:

DTSTART:20190101T140000Z
RRULE:FREQ=MONTHLY;COUNT=12;INTERVAL=1
RDATE:20190201T150000Z
EXDATE:20190201T140000Z,20190401T140000Z

This rule covers all of our requirements. We have scheduled an event to occur on 1st Jan 2019 at 2 pm, repeating every month for a total of 12 months.

On February we have added an extra occurrence to take place at 3 pm. We then cancel the original 2 pm occurrence. We also cancel Aprils occurrence completely.

There is a fantastic library for working with these rules on NPM simply called rrule.js. https://www.npmjs.com/package/rrule. A demo app is also available to play around with different rules. http://jakubroztocil.github.io/rrule

Using this library we can create a rule and receive each occurrence using the .all() method.

Parsing a rule string with RRule library

Note the output contains the adjusted time, and the cancelled month does not appear.

2019–01–01T14:00:00.000Z
2019–02–01T15:00:00.000Z
2019–03–01T14:00:00.000Z
2019–05–01T14:00:00.000Z
2019–06–01T14:00:00.000Z
2019–07–01T14:00:00.000Z
2019–08–01T14:00:00.000Z
2019–09–01T14:00:00.000Z
2019–10–01T14:00:00.000Z
2019–11–01T14:00:00.000Z
2019–12–01T14:00:00.000Z

Multiple occurrences per day

The spec also supports the functionality for specifying specific days of the week and multiple times for each day through the BYDAY, BYHOUR and BYMINUTE fields. This allows the creation of much more complex rules.

For example to create a rule which occurs at 8:30 am and 6:00 pm you would use:

BYHOUR=8,18;BYMINUTE=30,0;BYSETPOS:1,4;

Notice the use of BYSETPOS here. When using “BY” properties you can think of entries like a table. So here we have:

HOUR | MINUTE
08 | 30
18 | 00

Without BYSETPOS this would create entries with all possible combinations in this order: 8:30, 8:00, 18:30, 18:00. We don't want this. So we use BYSETPOS to select the entries we want. Which in this case is one and four.

Working with timezones

Working with timezones and daylight savings make things more complicated, so it is important to always work with UTC time. The complexity of timezones is covered in detail at https://medium.com/@toastui/handling-time-zone-in-javascript-547e67aa842d

But for the sake of TL;DR follow the rule of always working with dates in UTC time. You can still display dates in the local time, but the code itself should deal with UTC.

Using UTC when creating an RRule

This provides just a single component of the overall system. Check out part two which covers database schema design. https://medium.com/tomorrowapp/the-complex-world-of-calendars-database-design-fccb3a71a74b

Reference:

--

--