todo_txt/task/
recurrence.rs1#[derive(Clone, Debug, PartialEq, Eq)]
2#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3pub struct Recurrence {
4 pub num: i64,
5 pub period: super::Period,
6 #[cfg_attr(feature = "serde", serde(default))]
7 pub strict: bool,
8}
9
10impl std::str::FromStr for Recurrence {
11 type Err = crate::Error;
12
13 fn from_str(s: &str) -> Result<Self, Self::Err> {
14 let mut s = String::from(s);
15
16 let strict = if s.get(0..1) == Some("+") {
17 s.remove(0);
18 true
19 } else {
20 false
21 };
22
23 let period = match s.pop() {
24 Some(c) => super::Period::from_str(&c.to_string())?,
25 None => return Err(crate::Error::InvalidRecurrence(s.to_string())),
26 };
27
28 let num = s
29 .parse()
30 .map_err(|_| crate::Error::InvalidRecurrence(s.to_string()))?;
31
32 Ok(Self {
33 num,
34 period,
35 strict,
36 })
37 }
38}
39
40impl std::fmt::Display for Recurrence {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 if self.strict {
43 f.write_str("+")?;
44 }
45
46 f.write_str(&format!("{}{}", self.num, self.period))?;
47
48 Ok(())
49 }
50}
51
52impl std::ops::Add<chrono::NaiveDate> for Recurrence {
53 type Output = chrono::NaiveDate;
54
55 fn add(self, rhs: Self::Output) -> Self::Output {
56 use super::Period::{self, *};
57 use chrono::{Datelike, Duration};
58
59 let delta_months = match self.period {
60 #[allow(clippy::suspicious_arithmetic_impl)]
61 Year => 12 * self.num as u32,
62 Month => self.num as u32,
63 Week => return rhs + Duration::weeks(self.num),
64 Day => return rhs + Duration::days(self.num),
65 };
66
67 let mut y = rhs.year();
68 let mut m = rhs.month();
69 let mut d = rhs.day();
70
71 let was_last_day = d == Period::days_in_month(m, y);
74
75 m += delta_months;
76 y += ((m - 1) / 12) as i32;
77 m = (m - 1) % 12 + 1;
78 if was_last_day || d > Period::days_in_month(m, y) {
79 d = Period::days_in_month(m, y);
80 }
81
82 chrono::NaiveDate::from_ymd_opt(y, m, d).unwrap()
83 }
84}