1use std::fmt;
4use std::str::FromStr;
5
6use chrono::Datelike;
7use chrono::NaiveDate;
8
9#[derive(Clone, Debug, Eq, PartialEq)]
11pub enum RepeatRule {
12 Daily,
13 Weekly,
14 Monthly,
15 Yearly,
16 Weekdays,
18 Custom(u32),
20}
21
22impl RepeatRule {
23 #[must_use]
25 pub fn next_due_date(&self, from: &str) -> Option<String> {
26 let base = NaiveDate::parse_from_str(from.trim(), "%Y-%m-%d").ok()?;
27 let next = match self {
28 Self::Daily => base.succ_opt()?,
29 Self::Weekly => base + chrono::Duration::days(7),
30 Self::Monthly => {
31 let (y, m, d) = (base.year(), base.month(), base.day());
32 let (next_y, next_m) = if m == 12 { (y + 1, 1u32) } else { (y, m + 1) };
33 NaiveDate::from_ymd_opt(next_y, next_m, std::cmp::min(d, 28))?
34 }
35 Self::Yearly => NaiveDate::from_ymd_opt(base.year() + 1, base.month(), base.day())?,
36 Self::Weekdays => {
37 let mut d = base.succ_opt()?;
38 for _ in 0..7 {
39 if d.weekday() != chrono::Weekday::Sat && d.weekday() != chrono::Weekday::Sun {
40 break;
41 }
42 d = d.succ_opt()?;
43 }
44 d
45 }
46 Self::Custom(n) => base + chrono::Duration::days(i64::from(*n)),
47 };
48 Some(next.to_string())
49 }
50}
51
52impl fmt::Display for RepeatRule {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 Self::Daily => f.write_str("daily"),
56 Self::Weekly => f.write_str("weekly"),
57 Self::Monthly => f.write_str("monthly"),
58 Self::Yearly => f.write_str("yearly"),
59 Self::Weekdays => f.write_str("weekdays"),
60 Self::Custom(n) => write!(f, "custom:{n}"),
61 }
62 }
63}
64
65impl FromStr for RepeatRule {
66 type Err = ();
67
68 fn from_str(s: &str) -> Result<Self, Self::Err> {
69 let s = s.trim().to_lowercase();
70 if s == "daily" {
71 Ok(Self::Daily)
72 } else if s == "weekly" {
73 Ok(Self::Weekly)
74 } else if s == "monthly" {
75 Ok(Self::Monthly)
76 } else if s == "yearly" {
77 Ok(Self::Yearly)
78 } else if s == "weekdays" {
79 Ok(Self::Weekdays)
80 } else if let Some(n) = s.strip_prefix("custom:") {
81 n.parse::<u32>().map(Self::Custom).map_err(|_| ())
82 } else if let Some(d) = s.strip_suffix('d') {
83 d.parse::<u32>().map(Self::Custom).map_err(|_| ())
85 } else if let Some(w) = s.strip_suffix('w') {
86 w.parse::<u32>()
88 .ok()
89 .and_then(|n| n.checked_mul(7))
90 .map(Self::Custom)
91 .ok_or(())
92 } else {
93 Err(())
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::RepeatRule;
101
102 #[test]
103 fn from_str_2d_and_3w() {
104 assert_eq!("2d".parse::<RepeatRule>().unwrap(), RepeatRule::Custom(2));
105 assert_eq!("3w".parse::<RepeatRule>().unwrap(), RepeatRule::Custom(21));
106 }
107
108 #[test]
109 fn from_str_weekdays() {
110 assert_eq!(
111 "weekdays".parse::<RepeatRule>().unwrap(),
112 RepeatRule::Weekdays
113 );
114 }
115
116 #[test]
117 fn from_str_invalid_returns_err() {
118 assert!("invalid".parse::<RepeatRule>().is_err());
119 assert!("".parse::<RepeatRule>().is_err());
120 assert!("custom:".parse::<RepeatRule>().is_err());
121 }
122}