use crate::error::{Error, Result};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Frequency {
Secondly,
Minutely,
Hourly,
Daily,
Weekly,
Monthly,
Yearly,
}
impl Frequency {
pub fn parse(s: &str) -> Result<Self> {
match s.to_uppercase().as_str() {
"SECONDLY" => Ok(Frequency::Secondly),
"MINUTELY" => Ok(Frequency::Minutely),
"HOURLY" => Ok(Frequency::Hourly),
"DAILY" => Ok(Frequency::Daily),
"WEEKLY" => Ok(Frequency::Weekly),
"MONTHLY" => Ok(Frequency::Monthly),
"YEARLY" => Ok(Frequency::Yearly),
other => Err(Error::invalid_value(
"FREQ",
format!("unknown frequency: {}", other),
)),
}
}
}
impl fmt::Display for Frequency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Frequency::Secondly => "SECONDLY",
Frequency::Minutely => "MINUTELY",
Frequency::Hourly => "HOURLY",
Frequency::Daily => "DAILY",
Frequency::Weekly => "WEEKLY",
Frequency::Monthly => "MONTHLY",
Frequency::Yearly => "YEARLY",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
impl Weekday {
pub fn parse(s: &str) -> Result<Self> {
match s.to_uppercase().as_str() {
"MO" => Ok(Weekday::Monday),
"TU" => Ok(Weekday::Tuesday),
"WE" => Ok(Weekday::Wednesday),
"TH" => Ok(Weekday::Thursday),
"FR" => Ok(Weekday::Friday),
"SA" => Ok(Weekday::Saturday),
"SU" => Ok(Weekday::Sunday),
other => Err(Error::invalid_value(
"BYDAY",
format!("unknown weekday: {}", other),
)),
}
}
}
impl fmt::Display for Weekday {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Weekday::Monday => "MO",
Weekday::Tuesday => "TU",
Weekday::Wednesday => "WE",
Weekday::Thursday => "TH",
Weekday::Friday => "FR",
Weekday::Saturday => "SA",
Weekday::Sunday => "SU",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WeekdayNum {
pub ordinal: Option<i8>,
pub weekday: Weekday,
}
impl WeekdayNum {
pub fn new(weekday: Weekday) -> Self {
Self {
ordinal: None,
weekday,
}
}
pub fn with_ordinal(ordinal: i8, weekday: Weekday) -> Self {
Self {
ordinal: Some(ordinal),
weekday,
}
}
pub fn parse(s: &str) -> Result<Self> {
let s = s.trim();
if s.len() < 2 {
return Err(Error::invalid_value("BYDAY", "too short"));
}
let day_start = s
.find(|c: char| c.is_ascii_alphabetic())
.ok_or_else(|| Error::invalid_value("BYDAY", "no weekday abbreviation found"))?;
let ordinal = if day_start > 0 {
let num: i8 = s[..day_start]
.parse()
.map_err(|_| Error::invalid_value("BYDAY", "invalid ordinal"))?;
Some(num)
} else {
None
};
let weekday = Weekday::parse(&s[day_start..])?;
Ok(WeekdayNum { ordinal, weekday })
}
}
impl fmt::Display for WeekdayNum {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ord) = self.ordinal {
write!(f, "{}{}", ord, self.weekday)
} else {
write!(f, "{}", self.weekday)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecurrenceRule {
pub freq: Frequency,
pub interval: Option<u32>,
pub count: Option<u32>,
pub until: Option<String>,
pub by_second: Vec<u32>,
pub by_minute: Vec<u32>,
pub by_hour: Vec<u32>,
pub by_day: Vec<WeekdayNum>,
pub by_month_day: Vec<i8>,
pub by_year_day: Vec<i16>,
pub by_week_no: Vec<i8>,
pub by_month: Vec<u32>,
pub by_set_pos: Vec<i16>,
pub wkst: Option<Weekday>,
}
impl RecurrenceRule {
pub fn new(freq: Frequency) -> Self {
Self {
freq,
interval: None,
count: None,
until: None,
by_second: Vec::new(),
by_minute: Vec::new(),
by_hour: Vec::new(),
by_day: Vec::new(),
by_month_day: Vec::new(),
by_year_day: Vec::new(),
by_week_no: Vec::new(),
by_month: Vec::new(),
by_set_pos: Vec::new(),
wkst: None,
}
}
pub fn parse(s: &str) -> Result<Self> {
let mut freq: Option<Frequency> = None;
let mut rule = RecurrenceRule::new(Frequency::Daily);
for part in s.split(';') {
let part = part.trim();
if part.is_empty() {
continue;
}
let eq = part.find('=').ok_or_else(|| {
Error::invalid_value("RRULE", format!("missing '=' in part: {}", part))
})?;
let key = &part[..eq];
let val = &part[eq + 1..];
match key.to_uppercase().as_str() {
"FREQ" => freq = Some(Frequency::parse(val)?),
"INTERVAL" => {
rule.interval = Some(
val.parse()
.map_err(|_| Error::invalid_value("INTERVAL", "not a valid number"))?,
);
}
"COUNT" => {
rule.count = Some(
val.parse()
.map_err(|_| Error::invalid_value("COUNT", "not a valid number"))?,
);
}
"UNTIL" => {
rule.until = Some(val.to_string());
}
"BYSECOND" => {
rule.by_second = parse_u32_list(val, "BYSECOND")?;
}
"BYMINUTE" => {
rule.by_minute = parse_u32_list(val, "BYMINUTE")?;
}
"BYHOUR" => {
rule.by_hour = parse_u32_list(val, "BYHOUR")?;
}
"BYDAY" => {
rule.by_day = val
.split(',')
.map(|d| WeekdayNum::parse(d.trim()))
.collect::<Result<Vec<_>>>()?;
}
"BYMONTHDAY" => {
rule.by_month_day = parse_i8_list(val, "BYMONTHDAY")?;
}
"BYYEARDAY" => {
rule.by_year_day = parse_i16_list(val, "BYYEARDAY")?;
}
"BYWEEKNO" => {
rule.by_week_no = parse_i8_list(val, "BYWEEKNO")?;
}
"BYMONTH" => {
rule.by_month = parse_u32_list(val, "BYMONTH")?;
}
"BYSETPOS" => {
rule.by_set_pos = parse_i16_list(val, "BYSETPOS")?;
}
"WKST" => {
rule.wkst = Some(Weekday::parse(val)?);
}
_ => {} }
}
rule.freq = freq.ok_or_else(|| Error::invalid_value("RRULE", "missing FREQ"))?;
Ok(rule)
}
}
impl fmt::Display for RecurrenceRule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "FREQ={}", self.freq)?;
if let Some(interval) = self.interval {
write!(f, ";INTERVAL={}", interval)?;
}
if let Some(count) = self.count {
write!(f, ";COUNT={}", count)?;
}
if let Some(ref until) = self.until {
write!(f, ";UNTIL={}", until)?;
}
if !self.by_day.is_empty() {
write!(f, ";BYDAY=")?;
for (i, d) in self.by_day.iter().enumerate() {
if i > 0 {
write!(f, ",")?;
}
write!(f, "{}", d)?;
}
}
if !self.by_month_day.is_empty() {
write!(f, ";BYMONTHDAY={}", join_list(&self.by_month_day))?;
}
if !self.by_year_day.is_empty() {
write!(f, ";BYYEARDAY={}", join_list(&self.by_year_day))?;
}
if !self.by_week_no.is_empty() {
write!(f, ";BYWEEKNO={}", join_list(&self.by_week_no))?;
}
if !self.by_month.is_empty() {
write!(f, ";BYMONTH={}", join_list(&self.by_month))?;
}
if !self.by_hour.is_empty() {
write!(f, ";BYHOUR={}", join_list(&self.by_hour))?;
}
if !self.by_minute.is_empty() {
write!(f, ";BYMINUTE={}", join_list(&self.by_minute))?;
}
if !self.by_second.is_empty() {
write!(f, ";BYSECOND={}", join_list(&self.by_second))?;
}
if !self.by_set_pos.is_empty() {
write!(f, ";BYSETPOS={}", join_list(&self.by_set_pos))?;
}
if let Some(ref wkst) = self.wkst {
write!(f, ";WKST={}", wkst)?;
}
Ok(())
}
}
fn join_list<T: fmt::Display>(items: &[T]) -> String {
items
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(",")
}
fn parse_u32_list(s: &str, name: &str) -> Result<Vec<u32>> {
s.split(',')
.map(|v| {
v.trim()
.parse()
.map_err(|_| Error::invalid_value(name, format!("invalid number: {}", v)))
})
.collect()
}
fn parse_i8_list(s: &str, name: &str) -> Result<Vec<i8>> {
s.split(',')
.map(|v| {
v.trim()
.parse()
.map_err(|_| Error::invalid_value(name, format!("invalid number: {}", v)))
})
.collect()
}
fn parse_i16_list(s: &str, name: &str) -> Result<Vec<i16>> {
s.split(',')
.map(|v| {
v.trim()
.parse()
.map_err(|_| Error::invalid_value(name, format!("invalid number: {}", v)))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_weekly() {
let rule = RecurrenceRule::parse("FREQ=WEEKLY;COUNT=10").unwrap();
assert_eq!(rule.freq, Frequency::Weekly);
assert_eq!(rule.count, Some(10));
assert_eq!(rule.to_string(), "FREQ=WEEKLY;COUNT=10");
}
#[test]
fn parse_complex_monthly() {
let rule =
RecurrenceRule::parse("FREQ=MONTHLY;BYDAY=2TU,-1FR;BYMONTH=1,6;INTERVAL=2").unwrap();
assert_eq!(rule.freq, Frequency::Monthly);
assert_eq!(rule.interval, Some(2));
assert_eq!(rule.by_day.len(), 2);
assert_eq!(rule.by_day[0].ordinal, Some(2));
assert_eq!(rule.by_day[0].weekday, Weekday::Tuesday);
assert_eq!(rule.by_day[1].ordinal, Some(-1));
assert_eq!(rule.by_day[1].weekday, Weekday::Friday);
assert_eq!(rule.by_month, vec![1, 6]);
}
#[test]
fn roundtrip() {
let input = "FREQ=YEARLY;INTERVAL=2;BYDAY=MO,TU;BYMONTH=1,7;UNTIL=20301231T235959Z";
let rule = RecurrenceRule::parse(input).unwrap();
let output = rule.to_string();
let rule2 = RecurrenceRule::parse(&output).unwrap();
assert_eq!(rule, rule2);
}
#[test]
fn missing_freq() {
let result = RecurrenceRule::parse("INTERVAL=2;COUNT=5");
assert!(result.is_err());
}
}