aboutsummaryrefslogtreecommitdiff
path: root/src/model/repetition.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/model/repetition.rs')
-rw-r--r--src/model/repetition.rs152
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)
+ }
+}