Syncing a Google Spreadsheet to a Calendar
The problem
In Aistemos we manage our holiday (vacation) bookings using a simple Google Apps spreadsheet. It's not perfect, but it's simple and lets everyone manage their own entries.
Incidentally, the value in the Days column is calculated with the NETWORKDAYS() function:
=NETWORKDAYS(B2,C2,'Bank holidays'!$A$1:$A$10)) - IF(D2="Yes", 0.5, 0.0)
That's all great, but it's quite hard to see when people are off by reading a list, so it would be great if the data in the sheet was somehow synced to a shared calendar, so people could see who was away when.
In theory, that's pretty easy - the Google Spreadsheet API is pretty easy to use, and there's an API to the Google Calendar system, how hard can it be?
The gotchas
The first gotcha is that it's not actually possible to create an all day, multi-day event using the version 2 API. There are some hacks but they all have serious disadvantages when it comes to trying to update, remove, or view the events created.
So, I decided to try with the v3 API, which does support multi-day all-day events, but it's not really properly documented (I think it's still in Beta), especially the in-app scripting part of it.
Another gotcha is that you have to enable the v3 API in two places before you can use it. If you don't you will get some error about some object not being available.
Using the v3 API you add / update / delete calendar entries by calling functions with small JavaScript object fragments. This is quite a bit more humane than the v2 API, which uses a chain of badly-named methods. The fragments look like:
{ "summary": "Bob's Holiday", "location": "Sunny place", "start": { "date": "2014-05-23" }, "end": { "date": "2014-05-26" } }
Which, despite what it looks like will create a multi-day event that starts on the 23rd, and ends on the 25th, so first we need to add one to the end date, with date.setDate(date.getDate() + 1); [src].
The next challenge is around timezones, there's some complex and unpredictable relationship between the timezone of the spreadsheet user, the timezone of the date in the spreadsheet cell, and the timezone of the calendar. The only sensible way to resolve all these is just to pull the raw un-normalised date numbers from the Javascript Date object and make up an ISO 8601 date string manually [src]:
Utilities.formatString("%04d-%02d-%02d", date.getFullYear(), date.getMonth()+1, date.getDate());
If we don't do that you get hard to trace off-by-one errors in the calendar, caused by daylight savings changes pushing the date one day later than you expected, sometimes.
Once we've done this, we can add dates to the calendar with Calendar.Events.insert() [src], capture the event ID and store it in an invisible column G of the sheet, so we can delete and update the entries later if the sheet changes... except the update method is buggy, and only seems to work once after you've created a new event, so I ended up removing and re-inserting on changes.
Note the delete call is actually called remove() in Javascript, I guess because otherwise it would collide with a keyword? Either way, a note to that effect would have helped. [src]
You have to be careful when re-inserting to generate a new ID, incase the event has been deleted in the meantime. Otherwise the changes are made to the deleted event, which doesn't help.
In order to see if an event needs (re)creating, we look in column G, find the event ID, and do a search with Calendar.Events.get(), but if you pass a null as the second argument you seem to get a Calendar object back, instead of an Event. This is quite dangerous, so have to guard against that. [src]
The solution
You can see the final script here. Just needs editing to match your IDs and sheet name, and adding to your spreadsheets project scripts (Tools | Script editor).
Lastly we just need to hook up a timer function to the projects Triggers with the Resources | Current project's triggers menu item. I added an hourly timer event to the timerEvent() function:
Future work
There's a fairly major bug where if someone deletes a row from the spreadsheet that doesn't cause the calendar event to be deleted. I'm fairly sure it's possible to handle with the onEdit() trigger, but it would involve a load more messing with the script API, and I can't be bothered right now. I think it doesn't happen often enough to be a huge problem for us.












