diff options
Diffstat (limited to 'src/model')
-rw-r--r-- | src/model/event.rs | 20 | ||||
-rw-r--r-- | src/model/repetition.rs | 152 |
2 files changed, 165 insertions, 7 deletions
diff --git a/src/model/event.rs b/src/model/event.rs index 3765fec..b18d811 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -1,5 +1,6 @@ use chrono::Timelike; use chrono::{NaiveDate, NaiveTime}; +use std::collections::HashMap; use uuid::Uuid; use crate::model::repetition::Repetition; @@ -43,6 +44,25 @@ impl Event { } } +/// Repeated events in an included date range +pub fn repetitions_between( + events: &Vec<Event>, + start: NaiveDate, + end: NaiveDate, +) -> HashMap<NaiveDate, Vec<Event>> { + let mut res: HashMap<NaiveDate, Vec<Event>> = HashMap::new(); + + for event in events { + for repetition in event.repetition.as_ref() { + for date in repetition.between(event.date, start, end) { + res.entry(date).or_insert(vec![]).push(event.clone()) + } + } + } + + res +} + pub fn pprint_time(t: NaiveTime) -> String { if t.minute() == 0 { format!("{}h", t.hour()) 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) + } +} |