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) +    } +} | 
