use chrono::{Datelike, Duration, NaiveDate, Weekday}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum Repetition { Daily { period: u32 }, Monthly { day: DayOfMonth }, Yearly, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum DayOfMonth { Day { day: u8 }, Weekday { weekday: 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![]; while date <= end { if date >= event && date >= start { repetitions.push(date) } date = next(date) } repetitions }; match self { Repetition::Daily { period } => { let n = start.signed_duration_since(event).num_days() % (*period as i64); let duration = Duration::days(*period as i64); repeat(start - Duration::days(n), Box::new(|d| d + duration)) } Repetition::Monthly { day: DayOfMonth::Day { day }, } => match start.with_day(*day as u32) { Some(first_repetition) => repeat(first_repetition, Box::new(next_month)), None => vec![], }, Repetition::Monthly { day: DayOfMonth::Weekday { weekday }, } => repeat( first_weekday_of_month(start, *weekday), Box::new(|d| first_weekday_of_month(next_month(d), *weekday)), ), Repetition::Yearly => repeat( NaiveDate::from_ymd(start.year(), event.month(), event.day()), Box::new(|d| NaiveDate::from_ymd(d.year() + 1, d.month(), d.day())), ), } } } fn first_weekday_of_month(date: NaiveDate, weekday: Weekday) -> NaiveDate { NaiveDate::from_weekday_of_month(date.year(), date.month(), weekday, 1) } fn next_month(date: NaiveDate) -> NaiveDate { if date.month() == 12 { NaiveDate::from_ymd(date.year() + 1, 1, date.day()) } else { NaiveDate::from_ymd(date.year(), date.month() + 1, date.day()) } } #[cfg(test)] mod tests { use super::*; #[test] fn every_day_event_before() { let repetition = Repetition::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 = Repetition::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 = Repetition::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 = Repetition::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 = Repetition::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 = Repetition::Monthly { day: DayOfMonth::Weekday { weekday: 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 = Repetition::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)) ) } fn d(y: i32, m: u32, d: u32) -> NaiveDate { NaiveDate::from_ymd(y, m, d) } }