Time and IDs

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

#calendar#scheduling#events#bookings

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 error

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.

rrule_between(rule_text: str, from: str, until: str, limit: int) → listor an error

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).

rrule_first(rule_text: str, count: int) → listor an error

The 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 error

The 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.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/rrule.qwer$ curl -O https://quillolang.org/plugins/files/rrule.qwe

Then, in your program, all of it or only what you call:

main.qwe
use rrule.qweruse rrule_text, rrule_between from rrule      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
rrule = "0.13"chrono = "0.4"

Example

rrule.qwe
// 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

rrule.qwer
//! 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()))}
Calendar files ical.qwer · 1.0.0Calendar files (.ics) for bookings and events, added to Google Calendar, Outlook and Apple Calendar in a click.calendaricsbookingsevents6 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.calendarimporteventsbookings8 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.bookingsschedulingcalendartime7 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.datescalendarchineselocale8 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.datescalendarlocalei18n7 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.datescalendardeliverylocale8 views · 11 downloads
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On Recurrence rules: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.