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>,
}
use crate::types::{Frequency, RRule, Weekday};
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError {
MissingKey(&'static str),
UnknownToken(String),
InvalidInteger(String),
}
fn parse_byday(raw: &str) -> Result<Vec<Weekday>, ParseError> {
raw.split(',')
.map(|tok| tok.parse::<Weekday>().map_err(|_| ParseError::UnknownToken(tok.to_string())))
.collect()
}
pub fn parse_rrule(value: &str) -> Result<RRule, ParseError> {
let mut params: HashMap<&str, &str> = HashMap::new();
for part in value.split(';') {
if part.is_empty() {
continue;
}
let (key, val) = part.split_once('=').ok_or(ParseError::MissingKey("="))?;
params.insert(key, val);
}
let freq_raw = params.get("FREQ").ok_or(ParseError::MissingKey("FREQ"))?;
let freq = freq_raw
.parse::<Frequency>()
.map_err(|_| ParseError::UnknownToken(freq_raw.to_string()))?;
let interval = match params.get("INTERVAL") {
Some(v) => v.parse::<u32>().map_err(|_| ParseError::InvalidInteger(v.to_string()))?,
None => 1,
};
let count = match params.get("COUNT") {
Some(v) => Some(v.parse::<u32>().map_err(|_| ParseError::InvalidInteger(v.to_string()))?),
None => None,
};
let until = params.get("UNTIL").map(|v| v.to_string());
let by_day = match params.get("BYDAY") {
Some(v) => parse_byday(v)?,
None => Vec::new(),
};
Ok(RRule { freq, interval, count, until, by_day })
}
mod parser;
mod types;
use parser::{parse_rrule, ParseError};
use types::RRule;
fn parse_ical(input: &str) -> Result<Vec<RRule>, ParseError> {
let mut rules = Vec::new();
for line in input.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("RRULE:") {
rules.push(parse_rrule(rest)?);
}
}
Ok(rules)
}
fn main() -> Result<(), ParseError> {
let ics = "\
BEGIN:VEVENT
SUMMARY:Team standup
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE;COUNT=10
END:VEVENT";
for rule in parse_ical(ics)? {
print!("{:?} every {} unit(s)", rule.freq, rule.interval);
if !rule.by_day.is_empty() {
print!(" on {:?}", rule.by_day);
}
match (rule.count, rule.until.as_deref()) {
(Some(n), _) => println!(", {} times", n),
(None, Some(d)) => println!(", until {}", d),
(None, None) => println!(", forever"),
}
}
Ok(())
}
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
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
clap for CLI argument parsing with derive macros
Share this code
Here's the card — post it anywhere.