1use crate::error::MpsError;
2use chrono::NaiveTime;
3use regex::Regex;
4use std::sync::OnceLock;
5
6fn re_word() -> &'static Regex {
7 static RE: OnceLock<Regex> = OnceLock::new();
8 RE.get_or_init(|| Regex::new(r"(?i)^(noon|midnight)$").unwrap())
9}
10
11fn re_12h() -> &'static Regex {
12 static RE: OnceLock<Regex> = OnceLock::new();
13 RE.get_or_init(|| Regex::new(r"(?i)^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$").unwrap())
14}
15
16fn re_24h() -> &'static Regex {
17 static RE: OnceLock<Regex> = OnceLock::new();
18 RE.get_or_init(|| Regex::new(r"^(\d{1,2}):(\d{2})$").unwrap())
19}
20
21pub fn parse_time(input: &str) -> Result<NaiveTime, MpsError> {
32 let s = input.trim();
33
34 if let Some(cap) = re_word().captures(s) {
36 return match cap[1].to_lowercase().as_str() {
37 "noon" => Ok(NaiveTime::from_hms_opt(12, 0, 0).unwrap()),
38 "midnight" => Ok(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
39 _ => unreachable!(),
40 };
41 }
42
43 if let Some(cap) = re_12h().captures(s) {
45 let hour: u32 = cap[1].parse().unwrap();
46 let minute: u32 = cap.get(2).map(|m| m.as_str().parse().unwrap()).unwrap_or(0);
47 let ampm = cap[3].to_lowercase();
48 let h24 = match (ampm.as_str(), hour) {
49 ("am", 12) => 0,
50 ("am", h) => h,
51 ("pm", 12) => 12,
52 ("pm", h) => h + 12,
53 _ => unreachable!(),
54 };
55 return NaiveTime::from_hms_opt(h24, minute, 0)
56 .ok_or_else(|| MpsError::TimeParse(input.to_string()));
57 }
58
59 if let Some(cap) = re_24h().captures(s) {
61 let hour: u32 = cap[1].parse().unwrap();
62 let minute: u32 = cap[2].parse().unwrap();
63 return NaiveTime::from_hms_opt(hour, minute, 0)
64 .ok_or_else(|| MpsError::TimeParse(input.to_string()));
65 }
66
67 Err(MpsError::TimeParse(input.to_string()))
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 fn hm(h: u32, m: u32) -> NaiveTime {
75 NaiveTime::from_hms_opt(h, m, 0).unwrap()
76 }
77
78 #[test]
79 fn test_noon() {
80 assert_eq!(parse_time("noon").unwrap(), hm(12, 0));
81 }
82 #[test]
83 fn test_noon_upper() {
84 assert_eq!(parse_time("NOON").unwrap(), hm(12, 0));
85 }
86 #[test]
87 fn test_midnight() {
88 assert_eq!(parse_time("midnight").unwrap(), hm(0, 0));
89 }
90 #[test]
91 fn test_9am() {
92 assert_eq!(parse_time("9am").unwrap(), hm(9, 0));
93 }
94 #[test]
95 fn test_9_30am() {
96 assert_eq!(parse_time("9:30am").unwrap(), hm(9, 30));
97 }
98 #[test]
99 fn test_3pm() {
100 assert_eq!(parse_time("3pm").unwrap(), hm(15, 0));
101 }
102 #[test]
103 fn test_3_45pm() {
104 assert_eq!(parse_time("3:45pm").unwrap(), hm(15, 45));
105 }
106 #[test]
107 fn test_12am_midnight() {
108 assert_eq!(parse_time("12am").unwrap(), hm(0, 0));
109 }
110 #[test]
111 fn test_12pm_noon() {
112 assert_eq!(parse_time("12pm").unwrap(), hm(12, 0));
113 }
114 #[test]
115 fn test_5pm() {
116 assert_eq!(parse_time("5pm").unwrap(), hm(17, 0));
117 }
118 #[test]
119 fn test_24h_colon() {
120 assert_eq!(parse_time("17:00").unwrap(), hm(17, 0));
121 }
122 #[test]
123 fn test_24h_930() {
124 assert_eq!(parse_time("9:30").unwrap(), hm(9, 30));
125 }
126 #[test]
127 fn test_24h_0000() {
128 assert_eq!(parse_time("00:00").unwrap(), hm(0, 0));
129 }
130 #[test]
131 fn test_with_spaces() {
132 assert_eq!(parse_time(" 5pm ").unwrap(), hm(17, 0));
133 }
134 #[test]
135 fn test_am_uppercase() {
136 assert_eq!(parse_time("9AM").unwrap(), hm(9, 0));
137 }
138 #[test]
139 fn test_pm_uppercase() {
140 assert_eq!(parse_time("3PM").unwrap(), hm(15, 0));
141 }
142
143 #[test]
144 fn test_reject_empty() {
145 assert!(parse_time("").is_err());
146 }
147 #[test]
148 fn test_reject_garbage() {
149 assert!(parse_time("not-a-time").is_err());
150 }
151 #[test]
152 fn test_reject_bare_num() {
153 assert!(parse_time("930").is_err());
154 }
155 #[test]
156 fn test_reject_bad_hour() {
157 assert!(parse_time("25:00").is_err());
158 }
159 #[test]
160 fn test_reject_bad_min() {
161 assert!(parse_time("9:99pm").is_err());
162 }
163 #[test]
165 fn test_reject_13pm() {
166 assert!(parse_time("13pm").is_err());
167 }
168 #[test]
170 fn test_reject_bad_24h_min() {
171 assert!(parse_time("9:60").is_err());
172 }
173 #[test]
175 fn test_reject_trailing_chars() {
176 assert!(parse_time("5pmX").is_err());
177 }
178 #[test]
180 fn test_1159pm() {
181 assert_eq!(parse_time("11:59pm").unwrap(), hm(23, 59));
182 }
183 #[test]
185 fn test_1am() {
186 assert_eq!(parse_time("1am").unwrap(), hm(1, 0));
187 }
188}