rust 144 lines · 3 tabs

Parse iCalendar RRULE and Recurrence Data into Typed Rust Enums

Shared by codesnips Sep 2026
3 tabs
use std::str::FromStr;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Frequency {
    Daily,
    Weekly,
    Monthly,
    Yearly,
}

impl FromStr for Frequency {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "DAILY" => Ok(Frequency::Daily),
            "WEEKLY" => Ok(Frequency::Weekly),
            "MONTHLY" => Ok(Frequency::Monthly),
            "YEARLY" => Ok(Frequency::Yearly),
            _ => Err(()),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Weekday {
    Mo,
    Tu,
    We,
    Th,
    Fr,
    Sa,
    Su,
}

impl FromStr for Weekday {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "MO" => Ok(Weekday::Mo),
            "TU" => Ok(Weekday::Tu),
            "WE" => Ok(Weekday::We),
            "TH" => Ok(Weekday::Th),
            "FR" => Ok(Weekday::Fr),
            "SA" => Ok(Weekday::Sa),
            "SU" => Ok(Weekday::Su),
            _ => Err(()),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RRule {
    pub freq: Frequency,
    pub interval: u32,
    pub count: Option<u32>,
    pub until: Option<String>,
    pub by_day: Vec<Weekday>,
}
3 files · rust Explain with highlit

This snippet parses a small subset of the iCalendar format, focusing on the RRULE recurrence grammar, into strongly typed Rust values instead of leaving them as loose strings. Turning FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE;COUNT=10 into a typed struct lets the rest of an application reason about recurrences without re-parsing text everywhere, and it pushes all the fragile string handling into one well-tested boundary.

The types tab defines the domain model. Frequency is a closed enum with a FromStr implementation, so an unknown FREQ value fails loudly rather than silently defaulting. Weekday maps the two-letter iCalendar codes to variants, and RRule collects the parsed parameters, using Option for count and until because a rule may terminate by a fixed count, a date, or never. Modeling these as distinct optional fields makes the mutually-exclusive-ish nature explicit at the call site.

The parser tab does the actual work. ParseError is a single error type covering the failure modes — missing keys, unknown tokens, malformed integers — so callers get one Result to match on. parse_rrule splits the value on ;, then each part on =, building a small key/value view before interpreting it. Splitting parsing from interpretation keeps the semicolon/equals mechanics separate from the meaning of each parameter. Frequency is required, so its absence is a hard error, while INTERVAL falls back to 1 per the spec. parse_byday shows how repeated comma-separated tokens fold into a Vec<Weekday> with map and collect::<Result<_, _>>(), which short-circuits on the first bad token — a common Rust idiom for validating a collection.

The main tab drives an end-to-end read: parse_ical scans lines, ignores everything except RRULE: prefixes, and returns typed rules. It demonstrates the ergonomic payoff — the ? operator threads ParseError up cleanly, and the final match over count, until, and the default case reads like the business rule it represents. A pitfall worth noting is that this handles only unfolded single-line properties; real iCalendar allows line folding and timezone-qualified DTSTART values, which a production parser would normalize first.


Related snips

Share this code

Here's the card — post it anywhere.

Parse iCalendar RRULE and Recurrence Data into Typed Rust Enums — share card
Link copied