1use chrono::{Datelike, Duration, Local, NaiveDate, Weekday};
2use crate::error::MpsError;
3
4pub fn parse_date(input: &str) -> Result<NaiveDate, MpsError> {
8 let s = input.trim().to_lowercase();
9 let today = Local::now().date_naive();
10
11 match s.as_str() {
12 "today" => return Ok(today),
13 "yesterday" => return Ok(today - Duration::days(1)),
14 "last week" => return Ok(today - Duration::days(7)),
15 _ => {}
16 }
17
18 if let Some(rest) = s.strip_suffix(" days ago") {
20 if let Ok(n) = rest.trim().parse::<i64>() {
21 return Ok(today - Duration::days(n));
22 }
23 }
24 if let Some(rest) = s.strip_suffix(" day ago") {
25 if rest.trim() == "1" || rest.trim() == "a" {
26 return Ok(today - Duration::days(1));
27 }
28 }
29
30 if let Some(rest) = s.strip_prefix("last ") {
32 if let Some(wd) = parse_weekday(rest.trim()) {
33 return Ok(last_weekday(today, wd));
34 }
35 }
36
37 if let Some(wd) = parse_weekday(&s) {
39 return Ok(last_weekday(today, wd));
40 }
41
42 if s.len() == 8 && s.chars().all(|c| c.is_ascii_digit()) {
44 return NaiveDate::parse_from_str(&s, "%Y%m%d")
45 .map_err(|_| MpsError::DateParseError(input.to_string()));
46 }
47
48 if s.len() == 10 && s.chars().nth(4) == Some('-') {
50 return NaiveDate::parse_from_str(&s, "%Y-%m-%d")
51 .map_err(|_| MpsError::DateParseError(input.to_string()));
52 }
53
54 Err(MpsError::DateParseError(input.to_string()))
55}
56
57fn parse_weekday(s: &str) -> Option<Weekday> {
58 match s {
59 "monday" | "mon" => Some(Weekday::Mon),
60 "tuesday" | "tue" => Some(Weekday::Tue),
61 "wednesday" | "wed" => Some(Weekday::Wed),
62 "thursday" | "thu" => Some(Weekday::Thu),
63 "friday" | "fri" => Some(Weekday::Fri),
64 "saturday" | "sat" => Some(Weekday::Sat),
65 "sunday" | "sun" => Some(Weekday::Sun),
66 _ => None,
67 }
68}
69
70fn last_weekday(today: NaiveDate, wd: Weekday) -> NaiveDate {
72 let mut d = today - Duration::days(1);
73 while d.weekday() != wd {
74 d -= Duration::days(1);
75 }
76 d
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn test_today() {
85 assert_eq!(parse_date("today").unwrap(), Local::now().date_naive());
86 }
87
88 #[test]
89 fn test_yesterday() {
90 let expected = Local::now().date_naive() - Duration::days(1);
91 assert_eq!(parse_date("yesterday").unwrap(), expected);
92 }
93
94 #[test]
95 fn test_yyyymmdd() {
96 assert_eq!(
97 parse_date("20260428").unwrap(),
98 NaiveDate::from_ymd_opt(2026, 4, 28).unwrap()
99 );
100 }
101
102 #[test]
103 fn test_yyyy_mm_dd() {
104 assert_eq!(
105 parse_date("2026-04-28").unwrap(),
106 NaiveDate::from_ymd_opt(2026, 4, 28).unwrap()
107 );
108 }
109
110 #[test]
111 fn test_n_days_ago() {
112 let expected = Local::now().date_naive() - Duration::days(3);
113 assert_eq!(parse_date("3 days ago").unwrap(), expected);
114 }
115
116 #[test]
117 fn test_last_week() {
118 let expected = Local::now().date_naive() - Duration::days(7);
119 assert_eq!(parse_date("last week").unwrap(), expected);
120 }
121
122 #[test]
123 fn test_invalid() {
124 assert!(parse_date("gibberish date string").is_err());
125 }
126}