use chrono::{Datelike, Duration, NaiveDate, Weekday}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Repetition { pub frequency: Frequency, pub removed_occurences: HashSet, pub until: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum Frequency { Daily { period: u32 }, Monthly { day: DayOfMonth }, Yearly, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum DayOfMonth { Day { day: u8 }, Weekday { week: u8, day: Weekday }, } pub fn validate_period(str: &str) -> Result { let n = str .parse::() .map_err(|_| format!("{} n’est pas une période valide.", str))?; if n == 0 { Err("La periode doit être positive.".to_string()) } else { Ok(n) } } pub fn validate_day(str: &str) -> Result { let n = str .parse::() .map_err(|_| format!("« {} » n’est pas un jour valide.", str))?; if (1..=31).contains(&n) { Ok(n) } else { Err("Le jour devrait se situer entre le 1er et le 31 du mois.".to_string()) } } impl Repetition { pub fn between(&self, event: NaiveDate, start: NaiveDate, end: NaiveDate) -> Vec { let repeat = |mut date, next: Box NaiveDate>| { let mut repetitions = vec![]; let mut iteration: usize = 0; let end = self.until.map(|u| u.min(end)).unwrap_or(end); while date <= end { if date >= event { if date >= start && !self.removed_occurences.contains(&iteration) { repetitions.push(date) } iteration += 1 } date = next(date); } repetitions }; match self.frequency { Frequency::Daily { period } => { let duration = Duration::days(period as i64); repeat(event, Box::new(|d| d + duration)) } Frequency::Monthly { day: DayOfMonth::Day { day }, } => match event.with_day(day as u32) { Some(first_repetition) => repeat(first_repetition, Box::new(next_month)), None => vec![], }, Frequency::Monthly { day: DayOfMonth::Weekday { week, day }, } => repeat( day_of_month(event, week, day), Box::new(|d| day_of_month(next_month(d), week, day)), ), Frequency::Yearly => repeat( // TODO: error handling NaiveDate::from_ymd_opt(event.year(), event.month(), event.day()).unwrap(), Box::new(|d| NaiveDate::from_ymd_opt(d.year() + 1, d.month(), d.day()).unwrap()), ), } } pub fn occurence_index(&self, event: NaiveDate, date: NaiveDate) -> Option { let mut without_removed_occurences = self.clone(); without_removed_occurences.removed_occurences = HashSet::new(); without_removed_occurences .between(event, event, date) .iter() .position(|d| d == &date) } } fn day_of_month(date: NaiveDate, week: u8, day: Weekday) -> NaiveDate { // TODO: error handling NaiveDate::from_weekday_of_month_opt(date.year(), date.month(), day, week).unwrap() } fn next_month(date: NaiveDate) -> NaiveDate { // TODO: error handling if date.month() == 12 { NaiveDate::from_ymd_opt(date.year() + 1, 1, date.day()).unwrap() } else { NaiveDate::from_ymd_opt(date.year(), date.month() + 1, date.day()).unwrap() } } #[cfg(test)] mod tests { use super::*; #[test] fn every_day_event_before() { let repetition = from_freq(Frequency::Daily { period: 1 }); assert_eq!( repetition.between(d(2022, 6, 1), d(2022, 7, 1), d(2022, 8, 31)), d(2022, 7, 1) .iter_days() .take(62) .collect::>() ) } #[test] fn every_day_event_between() { let repetition = from_freq(Frequency::Daily { period: 1 }); assert_eq!( repetition.between(d(2022, 8, 10), d(2022, 7, 1), d(2022, 8, 31)), d(2022, 8, 10) .iter_days() .take(22) .collect::>() ) } #[test] fn every_day_event_after() { let repetition = from_freq(Frequency::Daily { period: 1 }); assert!(repetition .between(d(2022, 9, 1), d(2022, 7, 1), d(2022, 8, 31)) .is_empty()) } #[test] fn every_three_days() { let repetition = from_freq(Frequency::Daily { period: 3 }); assert_eq!( repetition.between(d(2022, 2, 16), d(2022, 2, 21), d(2022, 3, 6)), vec!( d(2022, 2, 22), d(2022, 2, 25), d(2022, 2, 28), d(2022, 3, 3), d(2022, 3, 6) ) ) } #[test] fn day_of_month() { let repetition = from_freq(Frequency::Monthly { day: DayOfMonth::Day { day: 8 }, }); assert_eq!( repetition.between(d(2022, 2, 7), d(2022, 1, 1), d(2022, 4, 7)), vec!(d(2022, 2, 8), d(2022, 3, 8)) ) } #[test] fn weekday_of_month() { let repetition = from_freq(Frequency::Monthly { day: DayOfMonth::Weekday { week: 1, day: Weekday::Tue, }, }); assert_eq!( repetition.between(d(2022, 1, 5), d(2022, 1, 1), d(2022, 4, 4)), vec!(d(2022, 2, 1), d(2022, 3, 1)) ) } #[test] fn yearly() { let repetition = from_freq(Frequency::Yearly); assert_eq!( repetition.between(d(2020, 5, 5), d(2018, 1, 1), d(2022, 5, 5)), vec!(d(2020, 5, 5), d(2021, 5, 5), d(2022, 5, 5)) ) } #[test] fn every_two_days_removed_occurence() { let repetition = Repetition { frequency: Frequency::Daily { period: 2 }, removed_occurences: HashSet::from([0, 2, 3]), until: None, }; assert_eq!( repetition.between(d(2020, 7, 1), d(2020, 7, 1), d(2020, 7, 9)), vec!(d(2020, 7, 3), d(2020, 7, 9)) ) } #[test] fn day_of_month_removed_occurence() { let repetition = Repetition { frequency: Frequency::Monthly { day: DayOfMonth::Day { day: 8 }, }, removed_occurences: HashSet::from([1, 3]), until: None, }; assert_eq!( repetition.between(d(2020, 1, 8), d(2020, 1, 8), d(2020, 4, 8)), vec!(d(2020, 1, 8), d(2020, 3, 8)) ) } #[test] fn weekday_of_month_removed_occurence() { let repetition = Repetition { frequency: Frequency::Monthly { day: DayOfMonth::Weekday { week: 1, day: Weekday::Fri, }, }, removed_occurences: HashSet::from([1, 2, 3]), until: None, }; assert_eq!( repetition.between(d(2020, 2, 1), d(2020, 2, 1), d(2020, 7, 1)), vec!(d(2020, 2, 7), d(2020, 6, 5)) ) } #[test] fn yearly_removed_occurence() { let repetition = Repetition { frequency: Frequency::Yearly, removed_occurences: HashSet::from([3]), until: None, }; assert_eq!( repetition.between(d(2018, 5, 5), d(2019, 8, 1), d(2022, 5, 5)), vec!(d(2020, 5, 5), d(2022, 5, 5)) ) } #[test] fn occurence_index_after_removed_occurence() { let repetition = Repetition { frequency: Frequency::Yearly, removed_occurences: HashSet::from([1]), until: None, }; assert_eq!( repetition.occurence_index(d(2020, 1, 1), d(2022, 1, 1)), Some(2) ) } #[test] fn repetition_stops_after_until() { let repetition = Repetition { frequency: Frequency::Yearly, removed_occurences: HashSet::new(), until: Some(d(2022, 1, 1)), }; assert_eq!( repetition.between(d(2020, 1, 1), d(2020, 1, 1), d(2024, 1, 1)), vec!(d(2020, 1, 1), d(2021, 1, 1), d(2022, 1, 1)) ) } #[test] fn repetition_daily_after_day() { let repetition = Repetition { frequency: Frequency::Daily { period: 1 }, removed_occurences: HashSet::new(), until: Some(d(2022, 4, 17)), }; assert_eq!( repetition.between(d(2022, 4, 4), d(2022, 3, 20), d(2022, 3, 20)), vec!() ) } fn d(y: i32, m: u32, d: u32) -> NaiveDate { NaiveDate::from_ymd_opt(y, m, d).unwrap() } fn from_freq(frequency: Frequency) -> Repetition { Repetition { frequency, removed_occurences: HashSet::new(), until: None, } } }