ICS reader
Read .ics calendar files from Google, Outlook, Apple and booking sites: each event's start, end, summary, location and repeat rule.
Version 1.0.0 · 2 functions · 1 crate
Functions
What use ics_parse.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use ics_parse, ics_parse_file from ics_parse (Quillo 0.6).
ics_parse(text: str) → anyor an errorThe calendars in text: { name, timezone, events } for the first one (most files hold one), each event { uid, summary, description, location, start, end, duration, all_day, timezone, status, url, organizer, rrule, recurrence }. Times are as written: 2026-11-02T09:30:00Z in UTC, 2026-11-02T09:30:00 local to timezone, 2026-12-25 for an all-day event (whose end is the day after it, as .ics says). recurrence holds a repeating event's DTSTART and RRULE lines for the rrule plugin; \, and \n in texts are read as a comma and a line break.
ics_parse_file(path: str) → anyor an errorThe events of an .ics file, as ics_parse gives them, read from path.
Get it
$ quillo install ics_parse # ics_parse.qwer into this folder$ quillo install ics_parse --example # and its example, ics_parse.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/ics_parse.qwer$ curl -O https://quillolang.org/plugins/files/ics_parse.qwe$ curl -O https://quillolang.org/plugins/files/ics_parse_sample.icsThen, in your program, all of it or only what you call:
use ics_parse.qweruse ics_parse, ics_parse_file from ics_parse // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
ical = { version = "0.11", default-features = false, features = ["ical"] }Example
// Importing bookings from a calendar file, as Google Calendar, Outlook and booking sites export them.//// quillo run ics_parse.qwe then open http://localhost:8080/bookingsuse "ics_parse.qwer"route GET /bookings { let cal = ics_parse_file("ics_parse_sample.ics")? { calendar: cal.name, events: cal.events | map((e) => { title: e.summary, start: e.start, end: e.end, zone: e.timezone, where: e.location, all_day: e.all_day }) }}route GET /details = ics_parse_file("ics_parse_sample.ics")?.events[0]route GET /repeating = ics_parse_file("ics_parse_sample.ics")?.events | filter((e) => e.recurrence != null) | map((e) => { title: e.summary, rule: e.rrule, recurrence: e.recurrence })route POST /import = ics_parse(request.body)?.events | map((e) => e.summary)$ quillo run ics_parse.qwe$ curl 'localhost:8080/bookings'$ curl 'localhost:8080/repeating'Source
//! Read .ics calendar files (Google Calendar, Outlook, Apple, booking sites): each event's start, end, summary, location and repeat rule, for importing bookings and showing feeds.//! version: 1.0.0//!//! [dependencies]//! ical = { version = "0.11", default-features = false, features = ["ical"] }use ical::parser::ical::component::IcalEvent;use ical::property::Property;use serde_json::{json, Value};/// Calendars up to 20 MB, and 100,000 events, are read.const MAX_TEXT: usize = 20 * 1024 * 1024;const MAX_EVENTS: usize = 100_000;fn unescape(s: &str) -> String { let mut out = String::with_capacity(s.len()); let mut it = s.chars(); while let Some(c) = it.next() { if c == '\\' { match it.next() { Some('n') | Some('N') => out.push('\n'), Some(o) => out.push(o), None => {} } } else { out.push(c); } } out}fn param(p: &Property, name: &str) -> Option<String> { p.params.as_ref()?.iter().find(|(k, _)| k.eq_ignore_ascii_case(name)).and_then(|(_, v)| v.first().cloned())}/// `20261102T093000Z` as `2026-11-02T09:30:00Z`, `20261102T093000` as `2026-11-02T09:30:00`, `20261225` as `2026-12-25`.fn when(v: &str) -> (String, bool) { let v = v.trim(); let b = v.as_bytes(); if !v.is_ascii() { return (v.to_string(), false); } if b.len() == 8 && v.chars().all(|c| c.is_ascii_digit()) { return (format!("{}-{}-{}", &v[0..4], &v[4..6], &v[6..8]), true); } if b.len() >= 15 && b[8] == b'T' { let z = if v.ends_with('Z') { "Z" } else { "" }; return (format!("{}-{}-{}T{}:{}:{}{}", &v[0..4], &v[4..6], &v[6..8], &v[9..11], &v[11..13], &v[13..15], z), false); } (v.to_string(), false)}fn event(e: &IcalEvent) -> Value { let get = |n: &str| e.properties.iter().find(|p| p.name.eq_ignore_ascii_case(n)); let text = |n: &str| get(n).and_then(|p| p.value.as_deref()).map(unescape); let (start, all_day, tz) = match get("DTSTART") { Some(p) => { let (w, d) = when(p.value.as_deref().unwrap_or("")); (Some(w), d || param(p, "VALUE").is_some_and(|v| v.eq_ignore_ascii_case("DATE")), param(p, "TZID")) } None => (None, false, None), }; let end = get("DTEND").or_else(|| get("DUE")).and_then(|p| p.value.as_deref()).map(|v| when(v).0); // A repeating event's lines, ready for the rrule plugin's rrule_between. let rule_lines: Vec<String> = e.properties.iter() .filter(|p| ["DTSTART", "RRULE", "RDATE", "EXDATE"].iter().any(|n| p.name.eq_ignore_ascii_case(n))) .map(|p| { let params: String = p.params.iter().flatten().map(|(k, v)| format!(";{}={}", k, v.join(","))).collect(); format!("{}{}:{}", p.name, params, p.value.as_deref().unwrap_or("")) }).collect(); let repeats = get("RRULE").is_some() || get("RDATE").is_some(); json!({ "uid": text("UID"), "summary": text("SUMMARY"), "description": text("DESCRIPTION"), "location": text("LOCATION"), "start": start, "end": end, "duration": text("DURATION"), "all_day": all_day, "timezone": tz, "status": text("STATUS"), "url": text("URL"), "organizer": get("ORGANIZER").map(|p| json!({ "name": param(p, "CN"), "email": p.value.as_deref().map(|v| v.trim_start_matches("mailto:").trim_start_matches("MAILTO:")) })), "rrule": text("RRULE"), "recurrence": if repeats { Value::String(rule_lines.join("\n")) } else { Value::Null }, })}/// The calendars in `text`: `{ name, timezone, events }` for the first one (most files hold one), each event/// `{ uid, summary, description, location, start, end, duration, all_day, timezone, status, url, organizer, rrule, recurrence }`./// Times are as written: `2026-11-02T09:30:00Z` in UTC, `2026-11-02T09:30:00` local to `timezone`, `2026-12-25` for an all-day/// event (whose `end` is the day after it, as .ics says). `recurrence` holds a repeating event's DTSTART and RRULE lines for/// the rrule plugin; `\,` and `\n` in texts are read as a comma and a line break.pub fn ics_parse(text: &str) -> Result<Value, String> { if text.len() > MAX_TEXT { return Err("calendars up to 20 MB".into()); } let mut p = ical::IcalParser::new(std::io::BufReader::new(text.as_bytes())); let cal = p.next().ok_or("no BEGIN:VCALENDAR in the text")?.map_err(|e| format!("not an iCalendar file: {}", e))?; let prop = |n: &str| cal.properties.iter().find(|p| p.name.eq_ignore_ascii_case(n)).and_then(|p| p.value.as_deref()).map(unescape); let events: Vec<Value> = cal.events.iter().take(MAX_EVENTS).map(event).collect(); Ok(json!({ "name": prop("X-WR-CALNAME"), "timezone": prop("X-WR-TIMEZONE"), "events": events }))}/// The events of an .ics file, as ics_parse gives them, read from `path`.pub fn ics_parse_file(path: &str) -> Result<Value, String> { let size = std::fs::metadata(path).map_err(|e| format!("{}: {}", path, e))?.len(); if size as usize > MAX_TEXT { return Err(format!("{}: calendars up to 20 MB", path)); } ics_parse(&std::fs::read_to_string(path).map_err(|e| format!("{}: {}", path, e))?)}Related
ical.qwer · 1.0.0Calendar files (.ics) for bookings and events, added to Google Calendar, Outlook and Apple Calendar in a click.6 views · 6 downloadsRecurrence rules rrule.qwer · 1.0.0Repeating events from iCalendar RRULEs: the dates between two days in the event's time zone, daylight saving and exceptions included.7 views · 8 downloadsChinese calendar chinese_calendar.qwer · 1.0.0The Chinese lunisolar calendar: a date's lunar year, month and day in Chinese, leap months included, the year's stem and branch, zodiac animal and element, Chinese New Year, lunar dates back to Gregorian, and the 24 solar terms.8 views · 7 downloadsHijri calendar hijri.qwer · 1.0.0Islamic (Hijri) dates: a Gregorian date in Umm al-Qura, civil or observational reckoning, written in English and Arabic; a Hijri date back to the Gregorian; a month's first and last days; and the year's holy days.7 views · 7 downloadsPublic holidays holidays.qwer · 1.0.0Public holidays of 27 countries for any year from 1970 to 2099, computed rather than listed: Easter and its feasts, "the last Monday of May", Japan's equinoxes, with the day each is taken when it falls on a weekend.8 views · 11 downloadsUUID tools uuid_tools.qwer · 1.0.0UUIDs beyond the random uuid(): version 7 sorted by time for database keys, versions 5 and 3 that give the same id for the same name, and any UUID read with its version and time.7 views · 6 downloadsThread 0 posts
On ICS reader: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.