use anyhow::{Context, Result, bail};
use chrono::NaiveDate;
pub struct DateHandler;
impl DateHandler {
pub fn validate_and_canonicalize(
value: &str,
canonical_format: &str,
field_name: &str,
) -> Result<String> {
let parsed_date = Self::parse_date(value, field_name).context(format!(
"Failed to parse date value for field '{}'",
field_name
))?;
let canonicalized = match canonical_format {
"%Y%m%d" => parsed_date.format("%Y%m%d").to_string(),
"%Y-%m-%d" => parsed_date.format("%Y-%m-%d").to_string(),
_ => bail!(
"Unsupported date format '{}' for field '{}'",
canonical_format,
field_name
),
};
tracing::debug!(
field_name = field_name,
input_value = value,
canonical_value = %canonicalized,
canonical_format = canonical_format,
"Date successfully validated and canonicalized"
);
Ok(canonicalized)
}
fn parse_date(value: &str, field_name: &str) -> Result<NaiveDate> {
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
tracing::debug!(
field_name = field_name,
input_value = value,
parsed_format = "ISO 8601 (YYYY-MM-DD)",
"Date parsed successfully"
);
return Ok(date);
}
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") {
tracing::debug!(
field_name = field_name,
input_value = value,
parsed_format = "Compact (YYYYMMDD)",
"Date parsed successfully"
);
return Ok(date);
}
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%j") {
tracing::debug!(
field_name = field_name,
input_value = value,
parsed_format = "Day-of-year (YYYY-DDD)",
"Date parsed successfully"
);
return Ok(date);
}
bail!(
"Field '{}' contains invalid date '{}'. Expected: YYYY-MM-DD, YYYYMMDD, or YYYY-DDD",
field_name,
value
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_iso_8601_format() {
let result = DateHandler::validate_and_canonicalize("2025-12-25", "%Y%m%d", "date");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "20251225");
}
#[test]
fn test_compact_format() {
let result = DateHandler::validate_and_canonicalize("20251225", "%Y-%m-%d", "date");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "2025-12-25");
}
#[test]
fn test_day_of_year_format() {
let result = DateHandler::validate_and_canonicalize("2025-359", "%Y%m%d", "date");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "20251225"); }
#[test]
fn test_invalid_date() {
let result = DateHandler::validate_and_canonicalize("2025-02-30", "%Y%m%d", "date");
assert!(result.is_err());
}
#[test]
fn test_unsupported_canonical_format() {
let result = DateHandler::validate_and_canonicalize("2025-12-25", "%d/%m/%Y", "date");
assert!(result.is_err());
}
#[test]
fn test_leap_year_handling() {
let result = DateHandler::validate_and_canonicalize("2024-02-29", "%Y%m%d", "date");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "20240229");
let result = DateHandler::validate_and_canonicalize("2023-02-29", "%Y%m%d", "date");
assert!(result.is_err());
}
#[test]
fn test_date_boundary_conditions() {
let result = DateHandler::validate_and_canonicalize("1999-12-31", "%Y%m%d", "date");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "19991231");
let result = DateHandler::validate_and_canonicalize("2000-01-01", "%Y%m%d", "date");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "20000101");
let result = DateHandler::validate_and_canonicalize("2025-01-31", "%Y%m%d", "date");
assert!(result.is_ok());
let result = DateHandler::validate_and_canonicalize("2025-02-01", "%Y%m%d", "date");
assert!(result.is_ok());
}
#[test]
fn test_invalid_dates_comprehensive() {
let result = DateHandler::validate_and_canonicalize("2025-13-01", "%Y%m%d", "date");
assert!(result.is_err());
let result = DateHandler::validate_and_canonicalize("2025-01-32", "%Y%m%d", "date");
assert!(result.is_err());
let result = DateHandler::validate_and_canonicalize("2025-02-30", "%Y%m%d", "date");
assert!(result.is_err());
let result = DateHandler::validate_and_canonicalize("2025-04-31", "%Y%m%d", "date");
assert!(result.is_err());
}
#[test]
fn test_day_of_year_edge_cases() {
assert_eq!(
DateHandler::validate_and_canonicalize("2025-001", "%Y%m%d", "date").unwrap(),
"20250101"
);
assert_eq!(
DateHandler::validate_and_canonicalize("2025-365", "%Y%m%d", "date").unwrap(),
"20251231"
);
assert_eq!(
DateHandler::validate_and_canonicalize("2024-366", "%Y%m%d", "date").unwrap(),
"20241231"
);
assert!(DateHandler::validate_and_canonicalize("2025-366", "%Y%m%d", "date").is_err());
assert!(DateHandler::validate_and_canonicalize("2025-000", "%Y%m%d", "date").is_err());
}
#[test]
fn test_format_consistency() {
let iso_result =
DateHandler::validate_and_canonicalize("2025-12-25", "%Y%m%d", "date").unwrap();
let compact_result =
DateHandler::validate_and_canonicalize("20251225", "%Y%m%d", "date").unwrap();
let doy_result =
DateHandler::validate_and_canonicalize("2025-359", "%Y%m%d", "date").unwrap();
assert_eq!(iso_result, compact_result);
assert_eq!(compact_result, doy_result);
assert_eq!(iso_result, "20251225");
}
#[test]
fn test_malformed_input_formats() {
let malformed_inputs = [
"2025/12/25", "2025.12.25", "2025", "2025-12-25T00:00:00", "25-12-2025", "2025-13-01", "2025-02-30", "not-a-date", "", "2025-", "abc-def-ghi", ];
for input in malformed_inputs {
let result = DateHandler::validate_and_canonicalize(input, "%Y%m%d", "date");
assert!(
result.is_err(),
"Should fail for input: '{}', but got: {:?}",
input,
result
);
}
}
}