Today I purchased train tickets to FOSDEM on NS International. The site has
a handy option to “Add to Agenda”, which generates an icalendar
file which one
can add to their calendar agenda.
I downloaded it and tried to import it into my calendar tool of choice, khal. It crashed. I’m a software developer who’s deeply into calendaring and I’ve written plenty of things that deal with icalendar, so I had to dig in deeper.
To start with, here’s the icalendar object. I’ve slightly changed times and cities for privacy’s sake (feel free to entirely skip it):
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//NS Internationaal B.V.//sales-v2//en
BEGIN:VTIMEZONE
TZID:Europe/Brussels
LAST-MODIFIED:20221105T024525Z
TZURL:http://www.tzurl.org/zoneinfo/Europe/Brussels
X-LIC-LOCATION:Europe/Brussels
X-PROLEPTIC-TZNAME:LMT
BEGIN:STANDARD
TZNAME:BMT
TZOFFSETFROM:+0017
TZOFFSETTO:+0017
DTSTART:18800101T000000
END:STANDARD
BEGIN:STANDARD
TZNAME:WET
TZOFFSETFROM:+0017
TZOFFSETTO:+0000
DTSTART:18920501T001730
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0000
TZOFFSETTO:+0100
DTSTART:19141108T000000
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19161001T010000
RDATE:19421102T030000
RDATE:19431004T030000
RDATE:19440917T030000
RDATE:19450916T030000
RDATE:19461007T030000
RDATE:19770925T030000
RDATE:19781001T030000
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19170917T030000
RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYDAY=3MO;BYMONTH=9
END:STANDARD
BEGIN:STANDARD
TZNAME:WET
TZOFFSETFROM:+0100
TZOFFSETTO:+0000
DTSTART:19181111T120000
RDATE:19191005T000000
RDATE:19201024T000000
RDATE:19211026T000000
RDATE:19391119T030000
END:STANDARD
BEGIN:STANDARD
TZNAME:WET
TZOFFSETFROM:+0100
TZOFFSETTO:+0000
DTSTART:19221008T000000
RRULE:FREQ=YEARLY;UNTIL=19271001T230000Z;BYDAY=SU;BYMONTHDAY=2,3,4,5,6,7,8;
BYMONTH=10
END:STANDARD
BEGIN:STANDARD
TZNAME:WET
TZOFFSETFROM:+0100
TZOFFSETTO:+0000
DTSTART:19281007T030000
RRULE:FREQ=YEARLY;UNTIL=19381002T020000Z;BYDAY=SU;BYMONTHDAY=2,3,4,5,6,7,8;
BYMONTH=10
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0100
TZOFFSETTO:+0100
DTSTART:19770101T000000
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19790930T030000
RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYDAY=-1SU;BYMONTH=9
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19160501T000000
RDATE:19400520T030000
RDATE:19430329T020000
RDATE:19440403T020000
RDATE:19450402T020000
RDATE:19460519T020000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19170416T020000
RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYDAY=3MO;BYMONTH=4
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:WEST
TZOFFSETFROM:+0000
TZOFFSETTO:+0100
DTSTART:19190301T230000
RDATE:19200214T230000
RDATE:19210314T230000
RDATE:19220325T230000
RDATE:19230421T230000
RDATE:19240329T230000
RDATE:19250404T230000
RDATE:19260417T230000
RDATE:19270409T230000
RDATE:19280414T230000
RDATE:19290421T020000
RDATE:19300413T020000
RDATE:19310419T020000
RDATE:19320403T020000
RDATE:19330326T020000
RDATE:19340408T020000
RDATE:19350331T020000
RDATE:19360419T020000
RDATE:19370404T020000
RDATE:19380327T020000
RDATE:19390416T020000
RDATE:19400225T020000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0200
TZOFFSETTO:+0200
DTSTART:19440903T000000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19770403T020000
RRULE:FREQ=YEARLY;UNTIL=19800406T010000Z;BYDAY=1SU;BYMONTH=4
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:4e3e3186-28c6-42d4-81cf-eb4f56d587f1
DTSTAMP:20230122T142045Z
SUMMARY:Trip from Utrecht to Bruxelles Central/Brussel Centraal
DESCRIPTION:Trip from Utrecht to Bruxelles Central/Brussel Centraal
LOCATION:Utrecht
URL:https://www.nsinternational.com/en/traintickets-v3/
DTSTART;TZID=Europe/Brussels:20230203T151500
DTEND;TZID=Europe/Brussels:20230203T180600
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:PT-2H
END:VALARM
END:VEVENT
BEGIN:VEVENT
UID:28d25a55-84cd-4564-910b-088066fd5244
DTSTAMP:20230122T142045Z
SUMMARY:Trip from Bruxelles Central/Brussel Centraal to Utrecht
DESCRIPTION:Trip from Bruxelles Central/Brussel Centraal to Utrecht
LOCATION:Bruxelles Central/Brussel Centraal
URL:https://www.nsinternational.com/en/traintickets-v3/
DTSTART;TZID=Europe/Brussels:20230206T095000
DTEND;TZID=Europe/Brussels:20230206T125000
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:PT-2H
END:VALARM
END:VEVENT
END:VCALENDAR
The exact exception was:
ValueError: Invalid iCalendar duration: PT-2H
This seems to be the line indicating when the alarm for the event should be triggered (two hours before the event in this case):
TRIGGER;RELATED=START:PT-2H
I actually wrote a library to parse exactly this (durations as expressed in
icalendar components) a few months ago, icalendar_duration. I fed this value
(PT-2H
) into my library, and received confirmation that it indeed had an
invalid format. At least now I knew the icalendar file was bad and not a bug in
the calendar application itself.
It wasn’t entirely obvious what was wrong about it to me, so I went to check the icalendar specification. Section 3.3.6 specifies the format for duration:
Format Definition: This value type is defined by the following notation:
dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
dur-date = dur-day [dur-time]
dur-time = "T" (dur-hour / dur-minute / dur-second)
dur-week = 1*DIGIT "W"
dur-hour = 1*DIGIT "H" [dur-minute]
dur-minute = 1*DIGIT "M" [dur-second]
dur-second = 1*DIGIT "S"
dur-day = 1*DIGIT "D"
I had to read this over and over again for like 10 minutes until I figured out
what was wrong: The “minus” symbol goes before the P
(this P
indicates the
beginning of a duration, as opposed to an absolute time). That makes sense,
otherwise we’d be able to write things like P2DT-2H
, which means “two days
and minus two hours”, which is a super awkward way of expressing time.
So the issue seems to be a trivial; it’s simply a bug in how NS International
is encoding the minus sign. I’m not sure how different calendar applications
parse this. I assume at least one parses this correctly (e.g.: the one with
which the devs tested this). A common behaviour should be for an application to
ignore the alarm itself and treat the rest of the event as valid. This is
definitely what khal
should be doing.
This issue could likely be solved by a developer at NS in a short time. The hard part will be reaching a developer with access to these source in order to fix this. I’ll try and see if I can reach such a person, and the purpose of this write-up is to have a quick link I can share with the hopes of it reaching them.
Hopefully, next time that I, or some other train-traveler wants to import their train-trip into an agenda, it actually works!