Parse iCal (RFC 5545) calendar strings into typed Elm data.
import Date
import Ical.Parser as Parser
holidayFeedToString : String -> String
holidayFeedToString icsString =
case Parser.parse icsString of
Ok cal ->
cal.events
|> List.filterMap toHoliday
|> List.sortBy
(.date >> Date.toRataDie)
|> List.map
(\h ->
Date.format "EEE, MMM d" h.date
++ " - "
++ h.name
)
|> String.join "\n"
Err err ->
"Parse error: " ++ err
toHoliday :
Parser.Event
-> Maybe { name : String, date : Date.Date }
toHoliday event =
case ( event.summary, event.time ) of
( Just name, Parser.AllDay { start } ) ->
Just { name = name, date = start }
_ ->
Nothing
-- holidayFeedToString usHolidaysFeed
--
-- Thu, Jan 1 - New Year's Day
-- Mon, Jan 19 - MLK Day
-- ...See the examples/ folder
for a full runnable script that fetches and parses a live Google Calendar feed.
Parsing is strict: malformed content lines, invalid dates and times, bad
RRULE values, and a TZID without a matching VTIMEZONE all return Err.
Parsed types are transparent record aliases so you can read fields directly.
A parsed iCal calendar. Access fields directly:
case Parser.parse icsString of
Ok cal ->
List.length cal.events
Err err ->
0A parsed iCal event.
The uid, stamp, and time fields are required by
RFC 5545 Section 3.6.1.
Parsing will fail if DTSTART is missing from a VEVENT.
The end inside time is always populated — from DTEND, DURATION (which are
mutually exclusive
per the spec), or the RFC default when neither is present:
The time span of an event. The variant determines what type of values
start and end carry, enforcing the
RFC 5545
requirement that DTSTART and DTEND share the same value type.
The end is always populated. When neither DTEND nor DURATION is present in
the source data, the parser applies the RFC 5545 default: DATE events end on
the following day, and DATE-TIME events end at the same instant they start.
AllDay: VALUE=DATE; the event spans whole days.WithTime: a resolved instant in time (UTC or TZID resolved via VTIMEZONE).FloatingTime: a local date-time with no timezone; interpreted in the
viewer's local time.A typed reference to a specific recurrence instance as used by EXDATE,
RDATE, and RECURRENCE-ID.
The constructor matches the value type from the iCalendar data:
OnDate for VALUE=DATEAtTime for resolved DATE-TIME values (UTC or TZID-backed)AtFloatingTime for floating local DATE-TIME valuesA resolved date-time instant.
The timeZoneName is Nothing for UTC datetimes, or a
IANA Time Zone Database name
(e.g. Just "America/New_York") for TZID-resolved datetimes.
For TZID values, localDateTime retains the original wall-clock datetime and
timeZoneContext keeps the parsed timezone definition so recurrence expansion
can stay in local calendar dates. Most consumers can ignore those fields and
just use posix.
A date-time without timezone information. Represents a "floating" local date-time that is interpreted in the viewer's local time.
Opaque timezone data retained for parsed TZID date-times so recurrence expansion can resolve future local wall-clock occurrences correctly.
Most code will only need ResolvedTime.posix and
ResolvedTime.timeZoneName.
A parsed ORGANIZER property.
An uninterpreted property: name, parameters, and raw value string.
Unknown properties and extension properties (X-*) are preserved here.
A parsed ATTENDEE property.
Attendee role per RFC 5545 Section 3.2.16.
Attendee participation status per RFC 5545 Section 3.2.12.
A parsed alarm (VALARM) attached to an event.
Each alarm has an action (Display or Audio), a trigger (when the alarm
fires), and an optional description.
event.alarms
|> List.map .trigger
-- [ TriggerDuration (SecondsFromStart (-900)), ... ]The type of alarm.
When the alarm fires.
TriggerDuration — a relative offset from the event start or end.TriggerAbsolute — a fixed UTC time (used with VALUE=DATE-TIME).A relative alarm trigger as a signed offset in seconds. Negative values mean before, positive values mean after.
-- 15 minutes before start
SecondsFromStart (-15 * 60)
-- at the moment the event ends
SecondsFromEnd 0A parsed iCal journal entry (RFC 5545 Section 3.6.3). Unlike events, journals have no time span — just an optional date or datetime stamp and text content. They represent dated notes or log entries.
The time of a journal entry. Since journals have no time span (no DTEND or DURATION), this is a single point — either a date or a datetime.
A single occurrence of an event. For non-recurring events, there is one
occurrence whose time matches event.time. For recurring events, each
occurrence has its own time shifted to the recurrence date while preserving
the original event's duration.
Expand events into concrete occurrences within a date range, merging
RECURRENCE-ID overrides automatically. Override events (those with a
recurrenceId) replace the matching occurrence from the master event that
shares their uid.
case Parser.parse icsString of
Ok cal ->
Parser.expand { start = jan1, end = dec31 } cal.events
Err err ->
[]For a single event, wrap it in a list:
Parser.expand { start = jan1, end = dec31 } [ event ]Get the next N occurrences starting from a given date, merging RECURRENCE-ID overrides automatically.
case Parser.parse icsString of
Ok cal ->
Parser.expandNext 10 today cal.events
Err err ->
[]For a single event, wrap it in a list:
Parser.expandNext 5 today [ event ]
Parse an iCal string into a Calendar.