use crate::error::MpsError;
use chrono::NaiveTime;
use regex::Regex;
use std::sync::OnceLock;
fn re_word() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"(?i)^(noon|midnight)$").unwrap())
}
fn re_12h() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"(?i)^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$").unwrap())
}
fn re_24h() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"^(\d{1,2}):(\d{2})$").unwrap())
}
pub fn parse_time(input: &str) -> Result<NaiveTime, MpsError> {
let s = input.trim();
if let Some(cap) = re_word().captures(s) {
return match cap[1].to_lowercase().as_str() {
"noon" => Ok(NaiveTime::from_hms_opt(12, 0, 0).unwrap()),
"midnight" => Ok(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
_ => unreachable!(),
};
}
if let Some(cap) = re_12h().captures(s) {
let hour: u32 = cap[1].parse().unwrap();
let minute: u32 = cap.get(2).map(|m| m.as_str().parse().unwrap()).unwrap_or(0);
let ampm = cap[3].to_lowercase();
let h24 = match (ampm.as_str(), hour) {
("am", 12) => 0,
("am", h) => h,
("pm", 12) => 12,
("pm", h) => h + 12,
_ => unreachable!(),
};
return NaiveTime::from_hms_opt(h24, minute, 0)
.ok_or_else(|| MpsError::TimeParse(input.to_string()));
}
if let Some(cap) = re_24h().captures(s) {
let hour: u32 = cap[1].parse().unwrap();
let minute: u32 = cap[2].parse().unwrap();
return NaiveTime::from_hms_opt(hour, minute, 0)
.ok_or_else(|| MpsError::TimeParse(input.to_string()));
}
Err(MpsError::TimeParse(input.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
fn hm(h: u32, m: u32) -> NaiveTime {
NaiveTime::from_hms_opt(h, m, 0).unwrap()
}
#[test]
fn test_noon() {
assert_eq!(parse_time("noon").unwrap(), hm(12, 0));
}
#[test]
fn test_noon_upper() {
assert_eq!(parse_time("NOON").unwrap(), hm(12, 0));
}
#[test]
fn test_midnight() {
assert_eq!(parse_time("midnight").unwrap(), hm(0, 0));
}
#[test]
fn test_9am() {
assert_eq!(parse_time("9am").unwrap(), hm(9, 0));
}
#[test]
fn test_9_30am() {
assert_eq!(parse_time("9:30am").unwrap(), hm(9, 30));
}
#[test]
fn test_3pm() {
assert_eq!(parse_time("3pm").unwrap(), hm(15, 0));
}
#[test]
fn test_3_45pm() {
assert_eq!(parse_time("3:45pm").unwrap(), hm(15, 45));
}
#[test]
fn test_12am_midnight() {
assert_eq!(parse_time("12am").unwrap(), hm(0, 0));
}
#[test]
fn test_12pm_noon() {
assert_eq!(parse_time("12pm").unwrap(), hm(12, 0));
}
#[test]
fn test_5pm() {
assert_eq!(parse_time("5pm").unwrap(), hm(17, 0));
}
#[test]
fn test_24h_colon() {
assert_eq!(parse_time("17:00").unwrap(), hm(17, 0));
}
#[test]
fn test_24h_930() {
assert_eq!(parse_time("9:30").unwrap(), hm(9, 30));
}
#[test]
fn test_24h_0000() {
assert_eq!(parse_time("00:00").unwrap(), hm(0, 0));
}
#[test]
fn test_with_spaces() {
assert_eq!(parse_time(" 5pm ").unwrap(), hm(17, 0));
}
#[test]
fn test_am_uppercase() {
assert_eq!(parse_time("9AM").unwrap(), hm(9, 0));
}
#[test]
fn test_pm_uppercase() {
assert_eq!(parse_time("3PM").unwrap(), hm(15, 0));
}
#[test]
fn test_reject_empty() {
assert!(parse_time("").is_err());
}
#[test]
fn test_reject_garbage() {
assert!(parse_time("not-a-time").is_err());
}
#[test]
fn test_reject_bare_num() {
assert!(parse_time("930").is_err());
}
#[test]
fn test_reject_bad_hour() {
assert!(parse_time("25:00").is_err());
}
#[test]
fn test_reject_bad_min() {
assert!(parse_time("9:99pm").is_err());
}
#[test]
fn test_reject_13pm() {
assert!(parse_time("13pm").is_err());
}
#[test]
fn test_reject_bad_24h_min() {
assert!(parse_time("9:60").is_err());
}
#[test]
fn test_reject_trailing_chars() {
assert!(parse_time("5pmX").is_err());
}
#[test]
fn test_1159pm() {
assert_eq!(parse_time("11:59pm").unwrap(), hm(23, 59));
}
#[test]
fn test_1am() {
assert_eq!(parse_time("1am").unwrap(), hm(1, 0));
}
}