use crate::core::get_day;
use crate::core::get_hour;
use crate::core::get_minute;
use crate::core::get_month;
use crate::core::get_second;
use crate::iter::RRuleIter;
use crate::parser::str_to_weekday;
use crate::parser::ContentLineCaptures;
use crate::parser::ParseError;
use crate::validator::validate_rrule;
use crate::validator::ValidationError;
use crate::Tz;
use crate::{RRuleError, RRuleSet, Unvalidated, Validated};
use chrono::DateTime;
use chrono::{Datelike, Month, Weekday};
#[cfg(feature = "serde")]
use serde_with::{serde_as, DeserializeFromStr, SerializeDisplay};
use std::cmp::Ordering;
use std::fmt::Display;
use std::fmt::Formatter;
use std::marker::PhantomData;
use std::str::FromStr;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(DeserializeFromStr, SerializeDisplay))]
pub enum Frequency {
Yearly = 0,
Monthly = 1,
Weekly = 2,
Daily = 3,
Hourly = 4,
Minutely = 5,
Secondly = 6,
}
impl Display for Frequency {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let name = match self {
Self::Yearly => "YEARLY",
Self::Monthly => "MONTHLY",
Self::Weekly => "WEEKLY",
Self::Daily => "DAILY",
Self::Hourly => "HOURLY",
Self::Minutely => "MINUTELY",
Self::Secondly => "SECONDLY",
};
write!(f, "{}", name)
}
}
impl FromStr for Frequency {
type Err = ParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let freq = match &value.to_uppercase()[..] {
"YEARLY" => Self::Yearly,
"MONTHLY" => Self::Monthly,
"WEEKLY" => Self::Weekly,
"DAILY" => Self::Daily,
"HOURLY" => Self::Hourly,
"MINUTELY" => Self::Minutely,
"SECONDLY" => Self::Secondly,
val => return Err(ParseError::InvalidFrequency(val.to_string())),
};
Ok(freq)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(DeserializeFromStr, SerializeDisplay))]
pub enum NWeekday {
Every(Weekday),
Nth(i16, Weekday),
}
fn n_weekday_cmp(val1: NWeekday, val2: NWeekday) -> Ordering {
match val1 {
NWeekday::Every(wday) => match val2 {
NWeekday::Every(other_wday) => wday
.num_days_from_monday()
.cmp(&other_wday.num_days_from_monday()),
NWeekday::Nth(_n, _other_wday) => Ordering::Less,
},
NWeekday::Nth(n, wday) => match val2 {
NWeekday::Every(_) => Ordering::Greater,
NWeekday::Nth(other_n, other_wday) => match n.cmp(&other_n) {
Ordering::Equal => wday
.num_days_from_monday()
.cmp(&other_wday.num_days_from_monday()),
less_or_greater => less_or_greater,
},
},
}
}
impl PartialOrd for NWeekday {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for NWeekday {
fn cmp(&self, other: &Self) -> Ordering {
n_weekday_cmp(*self, *other)
}
}
impl NWeekday {
#[must_use]
pub fn new(number: Option<i16>, weekday: Weekday) -> Self {
match number {
Some(number) => Self::Nth(number, weekday),
None => Self::Every(weekday),
}
}
}
impl FromStr for NWeekday {
type Err = ParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let length = value.len();
if length < 2 {
return Err(ParseError::InvalidWeekday(value.into()));
}
let wd = str_to_weekday(&value[(length - 2)..])
.map_err(|_| ParseError::InvalidWeekday(value.into()))?;
let nth = value[..(length - 2)].parse::<i16>().unwrap_or_default();
if nth == 0 {
Ok(Self::Every(wd))
} else {
Ok(Self::new(Some(nth), wd))
}
}
}
impl Display for NWeekday {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let weekday = match self {
Self::Every(wd) => weekday_to_str(*wd),
Self::Nth(number, wd) => {
let mut wd_str = weekday_to_str(*wd);
if *number != 1 {
wd_str = format!("{}{}", number, wd_str);
};
wd_str
}
};
write!(f, "{}", weekday)
}
}
fn weekday_to_str(d: Weekday) -> String {
match d {
Weekday::Mon => "MO".to_string(),
Weekday::Tue => "TU".to_string(),
Weekday::Wed => "WE".to_string(),
Weekday::Thu => "TH".to_string(),
Weekday::Fri => "FR".to_string(),
Weekday::Sat => "SA".to_string(),
Weekday::Sun => "SU".to_string(),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", serde_as)]
#[cfg_attr(feature = "serde", derive(DeserializeFromStr, SerializeDisplay))]
pub struct RRule<Stage = Validated> {
pub(crate) freq: Frequency,
pub(crate) interval: u16,
pub(crate) count: Option<u32>,
#[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
pub(crate) until: Option<DateTime<Tz>>,
pub(crate) week_start: Weekday,
pub(crate) by_set_pos: Vec<i32>,
pub(crate) by_month: Vec<u8>,
pub(crate) by_month_day: Vec<i8>,
pub(crate) by_n_month_day: Vec<i8>,
pub(crate) by_year_day: Vec<i16>,
pub(crate) by_week_no: Vec<i8>,
pub(crate) by_weekday: Vec<NWeekday>,
pub(crate) by_hour: Vec<u8>,
pub(crate) by_minute: Vec<u8>,
pub(crate) by_second: Vec<u8>,
pub(crate) by_easter: Option<i16>,
#[cfg_attr(feature = "serde", serde_as(as = "ignore"))]
pub(crate) stage: PhantomData<Stage>,
}
impl Default for RRule<Unvalidated> {
fn default() -> Self {
Self {
freq: Frequency::Yearly,
interval: 1,
count: None,
until: None,
week_start: Weekday::Mon,
by_set_pos: Vec::new(),
by_month: Vec::new(),
by_month_day: Vec::new(),
by_n_month_day: Vec::new(),
by_year_day: Vec::new(),
by_week_no: Vec::new(),
by_weekday: Vec::new(),
by_hour: Vec::new(),
by_minute: Vec::new(),
by_second: Vec::new(),
by_easter: None,
stage: PhantomData,
}
}
}
impl RRule<Unvalidated> {
#[must_use]
pub fn new(freq: Frequency) -> Self {
Self {
freq,
..Self::default()
}
}
#[must_use]
pub fn freq(mut self, freq: Frequency) -> Self {
self.freq = freq;
self
}
#[must_use]
pub fn interval(mut self, interval: u16) -> Self {
self.interval = interval;
self
}
#[must_use]
pub fn count(mut self, count: u32) -> Self {
self.count = Some(count);
self
}
#[must_use]
pub fn until(mut self, until: DateTime<Tz>) -> Self {
self.until = Some(until);
self
}
#[must_use]
pub fn week_start(mut self, week_start: Weekday) -> Self {
self.week_start = week_start;
self
}
#[must_use]
pub fn by_set_pos(mut self, by_set_pos: Vec<i32>) -> Self {
self.by_set_pos = by_set_pos;
self
}
#[must_use]
pub fn by_month(mut self, by_month: &[Month]) -> Self {
self.by_month = by_month
.iter()
.map(|month| {
u8::try_from(month.number_from_month()).expect("1-12 is within range of u8")
})
.collect();
self
}
#[must_use]
pub fn by_month_day(mut self, by_month_day: Vec<i8>) -> Self {
self.by_month_day = by_month_day;
self
}
#[must_use]
pub fn by_year_day(mut self, by_year_day: Vec<i16>) -> Self {
self.by_year_day = by_year_day;
self
}
#[must_use]
pub fn by_week_no(mut self, by_week_no: Vec<i8>) -> Self {
self.by_week_no = by_week_no;
self
}
#[must_use]
pub fn by_weekday(mut self, by_weekday: Vec<NWeekday>) -> Self {
self.by_weekday = by_weekday;
self
}
#[must_use]
pub fn by_hour(mut self, by_hour: Vec<u8>) -> Self {
self.by_hour = by_hour;
self
}
#[must_use]
pub fn by_minute(mut self, by_minute: Vec<u8>) -> Self {
self.by_minute = by_minute;
self
}
#[must_use]
pub fn by_second(mut self, by_second: Vec<u8>) -> Self {
self.by_second = by_second;
self
}
#[cfg(feature = "by-easter")]
#[must_use]
pub fn by_easter(mut self, by_easter: i16) -> Self {
self.by_easter = Some(by_easter);
self
}
pub(crate) fn finalize_parsed_rrule(mut self, dt_start: &DateTime<Tz>) -> Self {
let mut by_month_day = vec![];
let mut by_n_month_day = self.by_n_month_day;
for by_month_day_item in self.by_month_day {
match by_month_day_item.cmp(&0) {
Ordering::Greater => by_month_day.push(by_month_day_item),
Ordering::Less => by_n_month_day.push(by_month_day_item),
Ordering::Equal => {}
}
}
self.by_month_day = by_month_day;
self.by_n_month_day = by_n_month_day;
let by_easter_is_some = if cfg!(feature = "by-easter") {
self.by_easter.is_some()
} else {
false
};
if !(!self.by_week_no.is_empty()
|| !self.by_year_day.is_empty()
|| !self.by_month_day.is_empty()
|| !self.by_n_month_day.is_empty()
|| !self.by_weekday.is_empty()
|| by_easter_is_some)
{
match self.freq {
Frequency::Yearly => {
if self.by_month.is_empty() {
let month = get_month(dt_start);
self.by_month = vec![month];
}
let day = get_day(dt_start);
self.by_month_day = vec![day];
}
Frequency::Monthly => {
let day = get_day(dt_start);
self.by_month_day = vec![day];
}
Frequency::Weekly => {
self.by_weekday = vec![NWeekday::Every(dt_start.weekday())];
}
_ => (),
};
}
if self.by_hour.is_empty() && self.freq < Frequency::Hourly {
let hour = get_hour(dt_start);
self.by_hour = vec![hour];
}
if self.by_minute.is_empty() && self.freq < Frequency::Minutely {
let minute = get_minute(dt_start);
self.by_minute = vec![minute];
}
if self.by_second.is_empty() && self.freq < Frequency::Secondly {
let second = get_second(dt_start);
self.by_second = vec![second];
}
self.by_hour.sort_unstable();
self.by_hour.dedup();
self.by_minute.sort_unstable();
self.by_minute.dedup();
self.by_second.sort_unstable();
self.by_second.dedup();
self.by_month.sort_unstable();
self.by_month.dedup();
self.by_month_day.sort_unstable();
self.by_month_day.dedup();
self.by_n_month_day.sort_unstable();
self.by_n_month_day.dedup();
self.by_year_day.sort_unstable();
self.by_year_day.dedup();
self.by_week_no.sort_unstable();
self.by_week_no.dedup();
self.by_set_pos.sort_unstable();
self.by_set_pos.dedup();
self.by_weekday.sort_unstable();
self.by_weekday.dedup();
self
}
pub fn validate(self, dt_start: DateTime<Tz>) -> Result<RRule<Validated>, RRuleError> {
let rrule = self.finalize_parsed_rrule(&dt_start);
validate_rrule::validate_rrule_forced(&rrule, &dt_start)?;
match rrule.freq {
Frequency::Hourly => {
if rrule.by_minute.is_empty() && rrule.by_second.is_empty() {
return Err(ValidationError::UnableToGenerateTimeset.into());
}
}
Frequency::Minutely => {
if rrule.by_second.is_empty() {
return Err(ValidationError::UnableToGenerateTimeset.into());
}
}
Frequency::Secondly => {}
_ => {
if rrule.by_hour.is_empty()
&& rrule.by_minute.is_empty()
&& rrule.by_second.is_empty()
{
return Err(ValidationError::UnableToGenerateTimeset.into());
}
}
}
Ok(RRule {
freq: rrule.freq,
interval: rrule.interval,
count: rrule.count,
until: rrule.until,
week_start: rrule.week_start,
by_set_pos: rrule.by_set_pos,
by_month: rrule.by_month,
by_month_day: rrule.by_month_day,
by_n_month_day: rrule.by_n_month_day,
by_year_day: rrule.by_year_day,
by_week_no: rrule.by_week_no,
by_weekday: rrule.by_weekday,
by_hour: rrule.by_hour,
by_minute: rrule.by_minute,
by_second: rrule.by_second,
by_easter: rrule.by_easter,
stage: PhantomData,
})
}
pub fn build(self, dt_start: DateTime<Tz>) -> Result<RRuleSet, RRuleError> {
let rrule = self.validate(dt_start)?;
let rrule_set = RRuleSet::new(dt_start).rrule(rrule);
Ok(rrule_set)
}
}
impl RRule {
pub(crate) fn iter_with_ctx(&self, dt_start: DateTime<Tz>, limited: bool) -> RRuleIter {
RRuleIter::new(self, &dt_start, limited)
}
}
impl FromStr for RRule<Unvalidated> {
type Err = RRuleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts = ContentLineCaptures::new(s)?;
Self::try_from(parts).map_err(From::from)
}
}
impl<S> Display for RRule<S> {
#[allow(clippy::too_many_lines)]
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut res = Vec::with_capacity(15);
res.push(format!("FREQ={}", &self.freq));
if let Some(until) = &self.until {
let maybe_zulu = if until.timezone().is_local() { "" } else { "Z" };
res.push(format!(
"UNTIL={}{}",
until.format("%Y%m%dT%H%M%S"),
maybe_zulu
));
}
if let Some(count) = &self.count {
res.push(format!("COUNT={}", count));
}
if self.interval != 1 {
res.push(format!("INTERVAL={}", &self.interval));
}
if self.week_start != Weekday::Mon {
res.push(format!("WKST={}", weekday_to_str(self.week_start)));
}
if !self.by_set_pos.is_empty() {
res.push(format!(
"BYSETPOS={}",
self.by_set_pos
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
));
}
if !self.by_month.is_empty() {
res.push(format!(
"BYMONTH={}",
self.by_month
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
));
}
if !self.by_month_day.is_empty() {
res.push(format!(
"BYMONTHDAY={}",
self.by_month_day
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
));
}
if !self.by_week_no.is_empty() {
res.push(format!(
"BYWEEKNO={}",
self.by_week_no
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
));
}
if !self.by_hour.is_empty() {
res.push(format!(
"BYHOUR={}",
self.by_hour
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
));
}
if !self.by_minute.is_empty() {
res.push(format!(
"BYMINUTE={}",
self.by_minute
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
));
}
if !self.by_second.is_empty() {
res.push(format!(
"BYSECOND={}",
self.by_second
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
));
}
if !self.by_year_day.is_empty() {
res.push(format!(
"BYYEARDAY={}",
self.by_year_day
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
));
}
if !self.by_weekday.is_empty() {
res.push(format!(
"BYDAY={}",
self.by_weekday
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
));
}
#[cfg(feature = "by-easter")]
if let Some(by_easter) = &self.by_easter {
res.push(format!("BYEASTER={}", by_easter));
}
write!(f, "{}", res.join(";"))
}
}
impl<S> RRule<S> {
#[must_use]
pub fn get_freq(&self) -> Frequency {
self.freq
}
#[must_use]
pub fn get_interval(&self) -> u16 {
self.interval
}
#[must_use]
pub fn get_count(&self) -> Option<u32> {
self.count
}
#[must_use]
pub fn get_until(&self) -> Option<&DateTime<Tz>> {
self.until.as_ref()
}
#[must_use]
pub fn get_week_start(&self) -> Weekday {
self.week_start
}
#[must_use]
pub fn get_by_set_pos(&self) -> &[i32] {
&self.by_set_pos
}
#[must_use]
pub fn get_by_month(&self) -> &[u8] {
&self.by_month
}
#[must_use]
pub fn get_by_month_day(&self) -> &[i8] {
&self.by_month_day
}
#[must_use]
pub fn get_by_year_day(&self) -> &[i16] {
&self.by_year_day
}
#[must_use]
pub fn get_by_week_no(&self) -> &[i8] {
&self.by_week_no
}
#[must_use]
pub fn get_by_weekday(&self) -> &[NWeekday] {
&self.by_weekday
}
#[must_use]
pub fn get_by_hour(&self) -> &[u8] {
&self.by_hour
}
#[must_use]
pub fn get_by_minute(&self) -> &[u8] {
&self.by_minute
}
#[must_use]
pub fn get_by_second(&self) -> &[u8] {
&self.by_second
}
#[cfg(feature = "by-easter")]
#[must_use]
pub fn get_by_easter(&self) -> Option<&i16> {
self.by_easter.as_ref()
}
}