diff options
Diffstat (limited to 'src/model/repetition.rs')
-rw-r--r-- | src/model/repetition.rs | 152 |
1 files changed, 145 insertions, 7 deletions
diff --git a/src/model/repetition.rs b/src/model/repetition.rs index 80387d9..ceb903b 100644 --- a/src/model/repetition.rs +++ b/src/model/repetition.rs @@ -1,21 +1,19 @@ -use chrono::Weekday; +use chrono::{Datelike, Duration, NaiveDate, Weekday}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum Repetition { - Daily { frequency: u8 }, - Monthly { frequency: MonthFrequency }, + Daily { period: u8 }, + Monthly { day: DayOfMonth }, Yearly, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum MonthFrequency { +pub enum DayOfMonth { Day { day: u8 }, - FirstDay { day: Weekday }, + Weekday { weekday: Weekday }, } -// Validation - pub fn validate_day(str: &str) -> Option<u8> { let n = str.parse::<u8>().ok()?; if n >= 1 && n <= 31 { @@ -24,3 +22,143 @@ pub fn validate_day(str: &str) -> Option<u8> { None } } + +impl Repetition { + pub fn between(&self, event: NaiveDate, start: NaiveDate, end: NaiveDate) -> Vec<NaiveDate> { + let repeat = |mut date, next: Box<dyn Fn(NaiveDate) -> 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::<Vec<NaiveDate>>() + ) + } + + #[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::<Vec<NaiveDate>>() + ) + } + + #[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) + } +} |