use anyhow::{Context, Result, bail};
use regex::Regex;
pub struct TimeHandler;
impl TimeHandler {
pub fn validate_and_canonicalize(value: &str, field_name: &str) -> Result<String> {
if let Some(canonical_time) = Self::handle_hour_only_format(value, field_name)? {
tracing::debug!(
field_name = field_name,
input_value = value,
canonical_value = %canonical_time,
format_type = "hour only",
"Time successfully validated and canonicalized"
);
return Ok(canonical_time);
}
if value.contains(':') {
return Self::handle_colon_format(value, field_name);
}
if value.len() == 4 {
return Self::handle_compact_format(value, field_name);
}
bail!(
"Field '{}' has invalid time format '{}'. Expected: H, HH, HH:MM, or HHMM",
field_name,
value
);
}
fn handle_hour_only_format(value: &str, field_name: &str) -> Result<Option<String>> {
if !value.is_empty() && value.len() <= 2 && value.chars().all(|c| c.is_ascii_digit()) {
let hour: u32 = value.parse().context(format!(
"Invalid hour value '{}' in field '{}'",
value, field_name
))?;
if hour > 23 {
bail!(
"Field '{}' has invalid hours: {}. Hours must be 0-23 in 24-hour format",
field_name,
hour
);
}
let canonical_time = format!("{:02}00", hour);
tracing::debug!(
field_name = field_name,
input_value = value,
canonical_value = %canonical_time,
parsed_hour = hour,
"Hour-only time successfully parsed and canonicalized"
);
Ok(Some(canonical_time))
} else {
Ok(None)
}
}
fn handle_colon_format(value: &str, field_name: &str) -> Result<String> {
let time_regex = Regex::new(r"^(\d{1,2}):(\d{2})$").unwrap();
if let Some(captures) = time_regex.captures(value) {
let hours: u32 = captures[1].parse().context(format!(
"Invalid hours '{}' in field '{}'",
&captures[1], field_name
))?;
let minutes: u32 = captures[2].parse().context(format!(
"Invalid minutes '{}' in field '{}'",
&captures[2], field_name
))?;
if hours > 23 {
bail!(
"Field '{}' has invalid hours: {}. Hours must be 0-23 in 24-hour format",
field_name,
hours
);
}
if minutes > 59 {
bail!(
"Field '{}' has invalid minutes: {}. Minutes must be 0-59",
field_name,
minutes
);
}
let canonical_time = format!("{:02}{:02}", hours, minutes);
tracing::debug!(
field_name = field_name,
input_value = value,
canonical_value = %canonical_time,
format_type = "HH:MM",
hours = hours,
minutes = minutes,
"Time successfully validated and canonicalized"
);
Ok(canonical_time)
} else {
bail!(
"Field '{}' has invalid HH:MM format '{}'. Expected format: HH:MM (e.g., 14:30, 9:05)",
field_name,
value
);
}
}
fn handle_compact_format(value: &str, field_name: &str) -> Result<String> {
let time_regex = Regex::new(r"^(\d{2})(\d{2})$").unwrap();
if let Some(captures) = time_regex.captures(value) {
let hours: u32 = captures[1].parse().context(format!(
"Invalid hours '{}' in field '{}'",
&captures[1], field_name
))?;
let minutes: u32 = captures[2].parse().context(format!(
"Invalid minutes '{}' in field '{}'",
&captures[2], field_name
))?;
if hours > 23 {
bail!(
"Field '{}' has invalid hours: {}. Hours must be 0-23 in 24-hour format",
field_name,
hours
);
}
if minutes > 59 {
bail!(
"Field '{}' has invalid minutes: {}. Minutes must be 0-59",
field_name,
minutes
);
}
tracing::debug!(
field_name = field_name,
input_value = value,
canonical_value = value,
format_type = "HHMM",
hours = hours,
minutes = minutes,
"Time successfully validated and canonicalized"
);
Ok(value.to_string())
} else {
bail!(
"Field '{}' has invalid HHMM format '{}'. Expected format: HHMM with exactly 4 digits (e.g., 1430, 0905)",
field_name,
value
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_digit_hours() {
assert_eq!(
TimeHandler::validate_and_canonicalize("0", "time").unwrap(),
"0000"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("1", "time").unwrap(),
"0100"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("2", "time").unwrap(),
"0200"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("3", "time").unwrap(),
"0300"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("4", "time").unwrap(),
"0400"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("5", "time").unwrap(),
"0500"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("6", "time").unwrap(),
"0600"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("7", "time").unwrap(),
"0700"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("8", "time").unwrap(),
"0800"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("9", "time").unwrap(),
"0900"
);
}
#[test]
fn test_double_digit_hours_with_leading_zeros() {
assert_eq!(
TimeHandler::validate_and_canonicalize("00", "time").unwrap(),
"0000"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("01", "time").unwrap(),
"0100"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("02", "time").unwrap(),
"0200"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("03", "time").unwrap(),
"0300"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("04", "time").unwrap(),
"0400"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("05", "time").unwrap(),
"0500"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("06", "time").unwrap(),
"0600"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("07", "time").unwrap(),
"0700"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("08", "time").unwrap(),
"0800"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("09", "time").unwrap(),
"0900"
);
}
#[test]
fn test_double_digit_hours() {
assert_eq!(
TimeHandler::validate_and_canonicalize("10", "time").unwrap(),
"1000"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("11", "time").unwrap(),
"1100"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("12", "time").unwrap(),
"1200"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("13", "time").unwrap(),
"1300"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("14", "time").unwrap(),
"1400"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("15", "time").unwrap(),
"1500"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("16", "time").unwrap(),
"1600"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("17", "time").unwrap(),
"1700"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("18", "time").unwrap(),
"1800"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("19", "time").unwrap(),
"1900"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("20", "time").unwrap(),
"2000"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("21", "time").unwrap(),
"2100"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("22", "time").unwrap(),
"2200"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("23", "time").unwrap(),
"2300"
);
}
#[test]
fn test_invalid_hours() {
assert!(TimeHandler::validate_and_canonicalize("24", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("25", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("99", "time").is_err());
}
#[test]
fn test_hh_mm_format_single_digit_hours() {
assert_eq!(
TimeHandler::validate_and_canonicalize("0:00", "time").unwrap(),
"0000"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("1:15", "time").unwrap(),
"0115"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("9:30", "time").unwrap(),
"0930"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("9:05", "time").unwrap(),
"0905"
);
}
#[test]
fn test_hh_mm_format_double_digit_hours() {
assert_eq!(
TimeHandler::validate_and_canonicalize("10:00", "time").unwrap(),
"1000"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("14:30", "time").unwrap(),
"1430"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("23:59", "time").unwrap(),
"2359"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("00:00", "time").unwrap(),
"0000"
);
}
#[test]
fn test_hh_mm_format_boundaries() {
assert_eq!(
TimeHandler::validate_and_canonicalize("00:00", "time").unwrap(),
"0000"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("23:59", "time").unwrap(),
"2359"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("12:00", "time").unwrap(),
"1200"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("0:59", "time").unwrap(),
"0059"
);
}
#[test]
fn test_invalid_hh_mm_format() {
assert!(TimeHandler::validate_and_canonicalize("24:00", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("25:30", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("12:60", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("12:99", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("12:5", "time").is_err()); assert!(TimeHandler::validate_and_canonicalize("1:2", "time").is_err()); }
#[test]
fn test_hhmm_format() {
assert_eq!(
TimeHandler::validate_and_canonicalize("0000", "time").unwrap(),
"0000"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("0100", "time").unwrap(),
"0100"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("0905", "time").unwrap(),
"0905"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("1430", "time").unwrap(),
"1430"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("2359", "time").unwrap(),
"2359"
);
}
#[test]
fn test_invalid_hhmm_format() {
assert!(TimeHandler::validate_and_canonicalize("2400", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("2500", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("1260", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("1299", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("123", "time").is_err()); assert!(TimeHandler::validate_and_canonicalize("12345", "time").is_err()); }
#[test]
fn test_invalid_formats() {
assert!(TimeHandler::validate_and_canonicalize("", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("invalid", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("abc", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("12:ab", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("ab:30", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("12.30", "time").is_err()); assert!(TimeHandler::validate_and_canonicalize("12-30", "time").is_err()); }
#[test]
fn test_edge_cases() {
assert!(TimeHandler::validate_and_canonicalize(" 12", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("12 ", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize(" 12:30", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("12:30 ", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("1a", "time").is_err());
assert!(TimeHandler::validate_and_canonicalize("a1", "time").is_err());
}
#[test]
fn test_format_consistency() {
assert_eq!(
TimeHandler::validate_and_canonicalize("1", "time").unwrap(),
"0100"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("01", "time").unwrap(),
"0100"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("1:00", "time").unwrap(),
"0100"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("0100", "time").unwrap(),
"0100"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("14:30", "time").unwrap(),
"1430"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("1430", "time").unwrap(),
"1430"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("0", "time").unwrap(),
"0000"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("00", "time").unwrap(),
"0000"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("0:00", "time").unwrap(),
"0000"
);
assert_eq!(
TimeHandler::validate_and_canonicalize("0000", "time").unwrap(),
"0000"
);
}
}