Skip to main content

todo_txt/task/
recurrence.rs

1#[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        // Semantics taken from
72        //  https://github.com/dbeniamine/todo.txt-vim/blob/259125d9efe93f69582f50ef68c17e20fd1e963a/autoload/todo.vim#L531-L538
73        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}