I calendar parsing
1

Configure Feed

Select the types of activity you want to include in your feed.

add documentation

+95
+95
src/gcal.gleam
··· 7 7 import gtz 8 8 import splitter 9 9 10 + /// A parsed iCal calendar. 11 + /// 12 + /// Contains the calendar version, producer ID, the detected timezone used for 13 + /// resolving floating-time events, and a list of parsed events. 14 + /// 10 15 pub type Calendar { 11 16 Calendar( 17 + /// The iCal version, typically `"2.0"`. 12 18 version: String, 19 + /// The producer identifier, e.g. `"-//Apple Inc.//macOS 14.0//EN"`. 13 20 prodid: String, 21 + /// The timezone used for resolving floating-time events. Either extracted 22 + /// from the `X-WR-TIMEZONE` property, the value passed to 23 + /// `parse_with_timezone`, or `"UTC"` as a fallback. 14 24 timezone: String, 25 + /// All parsed VEVENT components. 15 26 events: List(Event), 16 27 ) 17 28 } 18 29 30 + /// A single calendar event (VEVENT). 31 + /// 32 + /// Common properties are extracted into named fields. All original properties 33 + /// are preserved in `raw` for access to anything not explicitly parsed. 34 + /// 19 35 pub type Event { 20 36 Event( 37 + /// The globally unique identifier for this event. 21 38 uid: String, 39 + /// The event title. Empty string if not present. 22 40 summary: String, 41 + /// The start time as an unambiguous timestamp. Returns `unix_epoch` if 42 + /// missing or unparseable. 23 43 dtstart: timestamp.Timestamp, 44 + /// The end time as an unambiguous timestamp. Returns `unix_epoch` if 45 + /// missing or unparseable. 24 46 dtend: timestamp.Timestamp, 47 + /// True when the event uses date-only values (`VALUE=DATE`), indicating 48 + /// an all-day event. 25 49 is_all_day: Bool, 50 + /// All original properties for this event, including DTSTART, DTEND, 51 + /// LOCATION, DESCRIPTION, ATTENDEE, etc. 26 52 raw: List(Property), 27 53 ) 28 54 } 29 55 56 + /// A single iCal property line, consisting of a name, optional parameters, 57 + /// and a value. 58 + /// 59 + /// For example: `DTSTART;TZID=Europe/Stockholm:20230101T100000` becomes: 60 + /// `Property("DTSTART", [Parameter("TZID", "Europe/Stockholm")], "20230101T100000")` 61 + /// 30 62 pub type Property { 31 63 Property(name: String, params: List(Parameter), value: String) 32 64 } 33 65 66 + /// A key-value parameter attached to a property. 67 + /// 68 + /// Parameters appear before the colon in a property line, separated by 69 + /// semicolons. For example, `TZID=Europe/Stockholm` in: 70 + /// `DTSTART;TZID=Europe/Stockholm:20230101T100000` 71 + /// 34 72 pub type Parameter { 35 73 Parameter(name: String, value: String) 36 74 } 37 75 76 + /// An error that can occur during parsing. 38 77 pub type Error { 78 + /// The input could not be parsed. The string contains a description of 79 + /// what went wrong. 39 80 ParseError(String) 40 81 } 41 82 ··· 69 110 ) 70 111 } 71 112 113 + /// Parse an iCal string into a `Calendar`. 114 + /// 115 + /// Floating-time events (those without a `Z` suffix or `TZID` parameter) are 116 + /// resolved using the `X-WR-TIMEZONE` property if present, otherwise UTC. 117 + /// 118 + /// ```gleam 119 + /// let assert Ok(calendar) = gcal.parse(ical_string) 120 + /// calendar.events 121 + /// ``` 122 + /// 72 123 pub fn parse(input: String) -> Result(Calendar, Error) { 73 124 parse_with_timezone(input, "") 74 125 } 75 126 127 + /// Parse an iCal string into a `Calendar`, using the given timezone for 128 + /// floating-time events. 129 + /// 130 + /// Floating-time events are those without a `Z` suffix and without a `TZID` 131 + /// parameter. This function resolves them to the provided timezone instead 132 + /// of using `X-WR-TIMEZONE` or UTC. 133 + /// 134 + /// Events with an explicit `TZID` parameter are always resolved using that 135 + /// timezone, regardless of this argument. 136 + /// 137 + /// ```gleam 138 + /// let assert Ok(calendar) = 139 + /// gcal.parse_with_timezone(ical_string, "Europe/Stockholm") 140 + /// ``` 141 + /// 76 142 pub fn parse_with_timezone( 77 143 input: String, 78 144 tz: String, ··· 253 319 |> string.replace("\\\\", "\\") 254 320 } 255 321 322 + /// Find a property by name in an event's raw properties. 323 + /// 324 + /// Returns `Ok(Property)` if found, `Error(Nil)` otherwise. 325 + /// 326 + /// ```gleam 327 + /// let assert Ok(location_prop) = gcal.get_property(event, "LOCATION") 328 + /// location_prop.value 329 + /// ``` 330 + /// 256 331 pub fn get_property(event: Event, name: String) -> Result(Property, Nil) { 257 332 list.find(event.raw, fn(prop) { prop.name == name }) 258 333 } 259 334 335 + /// Find a parameter value by name in a property's parameters. 336 + /// 337 + /// Returns `Ok(String)` with the parameter value if found, `Error(Nil)` 338 + /// otherwise. 339 + /// 340 + /// ```gleam 341 + /// let assert Ok(prop) = gcal.get_property(event, "DTSTART") 342 + /// let assert Ok(tzid) = gcal.get_parameter(prop, "TZID") 343 + /// ``` 344 + /// 260 345 pub fn get_parameter(prop: Property, name: String) -> Result(String, Nil) { 261 346 list.find_map(prop.params, fn(param) { 262 347 case param.name == name { ··· 368 453 } 369 454 } 370 455 456 + /// Parse a datetime property value into a `Timestamp`. 457 + /// 458 + /// Handles three iCal datetime formats: 459 + /// - UTC: `20230101T100000Z` 460 + /// - Timezone-aware: `20230101T100000` with `TZID=Europe/Stockholm` parameter 461 + /// - Date-only: `VALUE=DATE:20230101` (treated as midnight UTC) 462 + /// - Floating: `20230101T100000` (resolved using `fallback_tz`, or UTC) 463 + /// 464 + /// Returns `timestamp.unix_epoch` for empty or unparseable values. 465 + /// 371 466 @internal 372 467 pub fn parse_datetime( 373 468 prop: Property,