Recurrence rules
Repeating events from iCalendar RRULEs: the dates between two days in the event's time zone, daylight saving and exceptions included.
Version 1.0.0 · 4 functions · 2 crates
Functions
What use rrule.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use rrule_text, rrule_between from rrule (Quillo 0.6).
rrule_text(start: str, tz: str, rule: str) → stror an errorThe text of a rule set from its parts: start the first time, local to tz (2026-11-02T09:00:00 or 2026-11-02T09:00), tz an IANA zone (Europe/Paris, or UTC), and rule the RRULE itself (FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10). Gives DTSTART;TZID=Europe/Paris:20261102T090000 and the RRULE: line, as .ics files hold them, for rrule_between.
rrule_between(rule_text: str, from: str, until: str, limit: int) → listor an errorThe times rule_text (a DTSTART line and RRULE, RDATE and EXDATE lines, as rrule_text makes or an .ics VEVENT holds) gives from from up to until, both included: dates (2026-11-01, midnight in the rule's zone) or date-times with an offset. Each is an RFC 3339 date-time at the zone's offset that day. At most limit (1 to 10,000).
rrule_first(rule_text: str, count: int) → listor an errorThe first count times of a rule (1 to 10,000), from its start: a course's sessions, a loan's payment days.
rrule_next(rule_text: str) → str?or an errorThe next time a rule gives after now, or null when it has ended.
Get it
$ quillo install rrule # rrule.qwer into this folder$ quillo install rrule --example # and its example, rrule.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/rrule.qwer$ curl -O https://quillolang.org/plugins/files/rrule.qweThen, in your program, all of it or only what you call:
use rrule.qweruse rrule_text, rrule_between from rrule // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
rrule = "0.13"chrono = "0.4"Example
// A class schedule from a repeating rule: the sessions in a month, and the next one.//// quillo run rrule.qwe then open http://localhost:8080/novemberuse "rrule.qwer"// Yoga on Mondays and Wednesdays at 18:30 in Amsterdam, 20 sessions, no class on 11 November.let YOGA = rrule_text("2026-10-05T18:30:00", "Europe/Amsterdam", "FREQ=WEEKLY;BYDAY=MO,WE;COUNT=20")? + "\nEXDATE;TZID=Europe/Amsterdam:20261111T183000"route GET /november = rrule_between(YOGA, "2026-11-01", "2026-11-30", 100)?route GET /rules = { // Daylight saving ends on 25 October: the local time stays 18:30, the offset changes. around_dst: rrule_between(YOGA, "2026-10-19", "2026-10-29", 10)?, last_friday: rrule_first(rrule_text("2026-01-30T10:00:00", "UTC", "FREQ=MONTHLY;BYDAY=-1FR")?, 4)?, next: rrule_next(rrule_text("2026-01-01T09:00:00", "Europe/London", "FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=25")?)?, bad: rrule_text("2026-01-01T09:00:00", "Mars/Olympus", "FREQ=DAILY").error,}$ quillo run rrule.qwe$ curl 'localhost:8080/november'$ curl 'localhost:8080/rules'Source
//! Repeating events from iCalendar RRULEs ("every Monday and Wednesday at 9", "the last Friday of each month"): the dates between two days, in the event's time zone, daylight saving included.//! version: 1.0.0//!//! [dependencies]//! rrule = "0.13"//! chrono = "0.4"use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeZone};use rrule::{RRuleSet, Tz};fn set(rule_text: &str) -> Result<RRuleSet, String> { if rule_text.len() > 10_000 { return Err("a rule is at most 10,000 characters".into()); } rule_text.trim().replace("\\n", "\n").parse::<RRuleSet>().map_err(|e| format!("not a recurrence rule: {}", e))}/// A date (`2026-11-01`, midnight in `tz`) or a date-time with an offset (`2026-11-01T08:00:00Z`).fn instant(text: &str, tz: &Tz) -> Result<DateTime<Tz>, String> { let t = text.trim(); if let Ok(d) = DateTime::parse_from_rfc3339(t) { return Ok(d.with_timezone(tz)); } if let Ok(d) = NaiveDate::parse_from_str(t, "%Y-%m-%d") { let n = d.and_hms_opt(0, 0, 0).ok_or("bad date")?; return tz.from_local_datetime(&n).earliest().ok_or_else(|| format!("{} does not exist in that zone", t)); } Err(format!("`{}` is not a date (2026-11-01) or a date-time with an offset (2026-11-01T08:00:00Z)", text))}/// The text of a rule set from its parts: `start` the first time, local to `tz` (`2026-11-02T09:00:00` or `2026-11-02T09:00`),/// `tz` an IANA zone (`Europe/Paris`, or `UTC`), and `rule` the RRULE itself (`FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10`)./// Gives `DTSTART;TZID=Europe/Paris:20261102T090000` and the `RRULE:` line, as .ics files hold them, for rrule_between.pub fn rrule_text(start: &str, tz: &str, rule: &str) -> Result<String, String> { let s = start.trim(); let n = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M")) .map_err(|_| format!("`{}` is not a local date-time like 2026-11-02T09:00:00", start))?; let r = rule.trim().trim_start_matches("RRULE:"); let first = if tz.is_empty() || tz.eq_ignore_ascii_case("UTC") { format!("DTSTART:{}Z", n.format("%Y%m%dT%H%M%S")) } else { format!("DTSTART;TZID={}:{}", tz, n.format("%Y%m%dT%H%M%S")) }; let text = format!("{}\nRRULE:{}", first, r); // Parsing it checks the zone's name and the rule. set(&text)?; Ok(text)}/// The times `rule_text` (a DTSTART line and RRULE, RDATE and EXDATE lines, as rrule_text makes or an .ics VEVENT holds) gives/// from `from` up to `until`, both included: dates (`2026-11-01`, midnight in the rule's zone) or date-times with an offset./// Each is an RFC 3339 date-time at the zone's offset that day. At most `limit` (1 to 10,000).pub fn rrule_between(rule_text: &str, from: &str, until: &str, limit: i64) -> Result<Vec<String>, String> { let s = set(rule_text)?; let tz = s.get_dt_start().timezone(); let (a, b) = (instant(from, &tz)?, instant(until, &tz)?); let r = s.after(a).before(b).all(limit.clamp(1, 10_000) as u16); Ok(r.dates.iter().map(|d| d.to_rfc3339()).collect())}/// The first `count` times of a rule (1 to 10,000), from its start: a course's sessions, a loan's payment days.pub fn rrule_first(rule_text: &str, count: i64) -> Result<Vec<String>, String> { let r = set(rule_text)?.all(count.clamp(1, 10_000) as u16); Ok(r.dates.iter().map(|d| d.to_rfc3339()).collect())}/// The next time a rule gives after now, or null when it has ended.pub fn rrule_next(rule_text: &str) -> Result<Option<String>, String> { let s = set(rule_text)?; let now = chrono::Utc::now().with_timezone(&s.get_dt_start().timezone()); Ok(s.after(now).all(1).dates.first().map(|d| d.to_rfc3339()))}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 downloadsICS reader ics_parse.qwer · 1.0.0Read .ics calendar files from Google, Outlook, Apple and booking sites: each event's start, end, summary, location and repeat rule.8 views · 8 downloadsIntervals intervals.qwer · 1.0.0Time ranges that overlap: double bookings, free slots, busy spans, the most at once, bookings given rooms, and an interval index in memory for what is on when.7 views · 10 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 downloadsThread 0 posts
On Recurrence rules: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.