1use anyhow::{Context, Result, bail};
16use regex::Regex;
17
18pub struct TimeHandler;
29
30impl TimeHandler {
31 pub fn validate_and_canonicalize(value: &str, field_name: &str) -> Result<String> {
54 if let Some(canonical_time) = Self::handle_hour_only_format(value, field_name)? {
56 tracing::debug!(
57 field_name = field_name,
58 input_value = value,
59 canonical_value = %canonical_time,
60 format_type = "hour only",
61 "Time successfully validated and canonicalized"
62 );
63 return Ok(canonical_time);
64 }
65
66 if value.contains(':') {
68 return Self::handle_colon_format(value, field_name);
69 }
70
71 if value.len() == 4 {
73 return Self::handle_compact_format(value, field_name);
74 }
75
76 bail!(
78 "Field '{}' has invalid time format '{}'. Expected: H, HH, HH:MM, or HHMM",
79 field_name,
80 value
81 );
82 }
83
84 fn handle_hour_only_format(value: &str, field_name: &str) -> Result<Option<String>> {
107 if !value.is_empty() && value.len() <= 2 && value.chars().all(|c| c.is_ascii_digit()) {
109 let hour: u32 = value.parse().context(format!(
110 "Invalid hour value '{}' in field '{}'",
111 value, field_name
112 ))?;
113
114 if hour > 23 {
116 bail!(
117 "Field '{}' has invalid hours: {}. Hours must be 0-23 in 24-hour format",
118 field_name,
119 hour
120 );
121 }
122
123 let canonical_time = format!("{:02}00", hour);
125
126 tracing::debug!(
127 field_name = field_name,
128 input_value = value,
129 canonical_value = %canonical_time,
130 parsed_hour = hour,
131 "Hour-only time successfully parsed and canonicalized"
132 );
133
134 Ok(Some(canonical_time))
135 } else {
136 Ok(None)
138 }
139 }
140
141 fn handle_colon_format(value: &str, field_name: &str) -> Result<String> {
154 let time_regex = Regex::new(r"^(\d{1,2}):(\d{2})$").unwrap();
155
156 if let Some(captures) = time_regex.captures(value) {
157 let hours: u32 = captures[1].parse().context(format!(
158 "Invalid hours '{}' in field '{}'",
159 &captures[1], field_name
160 ))?;
161 let minutes: u32 = captures[2].parse().context(format!(
162 "Invalid minutes '{}' in field '{}'",
163 &captures[2], field_name
164 ))?;
165
166 if hours > 23 {
168 bail!(
169 "Field '{}' has invalid hours: {}. Hours must be 0-23 in 24-hour format",
170 field_name,
171 hours
172 );
173 }
174
175 if minutes > 59 {
177 bail!(
178 "Field '{}' has invalid minutes: {}. Minutes must be 0-59",
179 field_name,
180 minutes
181 );
182 }
183
184 let canonical_time = format!("{:02}{:02}", hours, minutes);
185
186 tracing::debug!(
187 field_name = field_name,
188 input_value = value,
189 canonical_value = %canonical_time,
190 format_type = "HH:MM",
191 hours = hours,
192 minutes = minutes,
193 "Time successfully validated and canonicalized"
194 );
195
196 Ok(canonical_time)
197 } else {
198 bail!(
199 "Field '{}' has invalid HH:MM format '{}'. Expected format: HH:MM (e.g., 14:30, 9:05)",
200 field_name,
201 value
202 );
203 }
204 }
205
206 fn handle_compact_format(value: &str, field_name: &str) -> Result<String> {
219 let time_regex = Regex::new(r"^(\d{2})(\d{2})$").unwrap();
220
221 if let Some(captures) = time_regex.captures(value) {
222 let hours: u32 = captures[1].parse().context(format!(
223 "Invalid hours '{}' in field '{}'",
224 &captures[1], field_name
225 ))?;
226 let minutes: u32 = captures[2].parse().context(format!(
227 "Invalid minutes '{}' in field '{}'",
228 &captures[2], field_name
229 ))?;
230
231 if hours > 23 {
233 bail!(
234 "Field '{}' has invalid hours: {}. Hours must be 0-23 in 24-hour format",
235 field_name,
236 hours
237 );
238 }
239
240 if minutes > 59 {
242 bail!(
243 "Field '{}' has invalid minutes: {}. Minutes must be 0-59",
244 field_name,
245 minutes
246 );
247 }
248
249 tracing::debug!(
250 field_name = field_name,
251 input_value = value,
252 canonical_value = value,
253 format_type = "HHMM",
254 hours = hours,
255 minutes = minutes,
256 "Time successfully validated and canonicalized"
257 );
258
259 Ok(value.to_string())
260 } else {
261 bail!(
262 "Field '{}' has invalid HHMM format '{}'. Expected format: HHMM with exactly 4 digits (e.g., 1430, 0905)",
263 field_name,
264 value
265 );
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
276 fn test_single_digit_hours() {
277 assert_eq!(
278 TimeHandler::validate_and_canonicalize("0", "time").unwrap(),
279 "0000"
280 );
281 assert_eq!(
282 TimeHandler::validate_and_canonicalize("1", "time").unwrap(),
283 "0100"
284 );
285 assert_eq!(
286 TimeHandler::validate_and_canonicalize("2", "time").unwrap(),
287 "0200"
288 );
289 assert_eq!(
290 TimeHandler::validate_and_canonicalize("3", "time").unwrap(),
291 "0300"
292 );
293 assert_eq!(
294 TimeHandler::validate_and_canonicalize("4", "time").unwrap(),
295 "0400"
296 );
297 assert_eq!(
298 TimeHandler::validate_and_canonicalize("5", "time").unwrap(),
299 "0500"
300 );
301 assert_eq!(
302 TimeHandler::validate_and_canonicalize("6", "time").unwrap(),
303 "0600"
304 );
305 assert_eq!(
306 TimeHandler::validate_and_canonicalize("7", "time").unwrap(),
307 "0700"
308 );
309 assert_eq!(
310 TimeHandler::validate_and_canonicalize("8", "time").unwrap(),
311 "0800"
312 );
313 assert_eq!(
314 TimeHandler::validate_and_canonicalize("9", "time").unwrap(),
315 "0900"
316 );
317 }
318
319 #[test]
321 fn test_double_digit_hours_with_leading_zeros() {
322 assert_eq!(
323 TimeHandler::validate_and_canonicalize("00", "time").unwrap(),
324 "0000"
325 );
326 assert_eq!(
327 TimeHandler::validate_and_canonicalize("01", "time").unwrap(),
328 "0100"
329 );
330 assert_eq!(
331 TimeHandler::validate_and_canonicalize("02", "time").unwrap(),
332 "0200"
333 );
334 assert_eq!(
335 TimeHandler::validate_and_canonicalize("03", "time").unwrap(),
336 "0300"
337 );
338 assert_eq!(
339 TimeHandler::validate_and_canonicalize("04", "time").unwrap(),
340 "0400"
341 );
342 assert_eq!(
343 TimeHandler::validate_and_canonicalize("05", "time").unwrap(),
344 "0500"
345 );
346 assert_eq!(
347 TimeHandler::validate_and_canonicalize("06", "time").unwrap(),
348 "0600"
349 );
350 assert_eq!(
351 TimeHandler::validate_and_canonicalize("07", "time").unwrap(),
352 "0700"
353 );
354 assert_eq!(
355 TimeHandler::validate_and_canonicalize("08", "time").unwrap(),
356 "0800"
357 );
358 assert_eq!(
359 TimeHandler::validate_and_canonicalize("09", "time").unwrap(),
360 "0900"
361 );
362 }
363
364 #[test]
366 fn test_double_digit_hours() {
367 assert_eq!(
368 TimeHandler::validate_and_canonicalize("10", "time").unwrap(),
369 "1000"
370 );
371 assert_eq!(
372 TimeHandler::validate_and_canonicalize("11", "time").unwrap(),
373 "1100"
374 );
375 assert_eq!(
376 TimeHandler::validate_and_canonicalize("12", "time").unwrap(),
377 "1200"
378 );
379 assert_eq!(
380 TimeHandler::validate_and_canonicalize("13", "time").unwrap(),
381 "1300"
382 );
383 assert_eq!(
384 TimeHandler::validate_and_canonicalize("14", "time").unwrap(),
385 "1400"
386 );
387 assert_eq!(
388 TimeHandler::validate_and_canonicalize("15", "time").unwrap(),
389 "1500"
390 );
391 assert_eq!(
392 TimeHandler::validate_and_canonicalize("16", "time").unwrap(),
393 "1600"
394 );
395 assert_eq!(
396 TimeHandler::validate_and_canonicalize("17", "time").unwrap(),
397 "1700"
398 );
399 assert_eq!(
400 TimeHandler::validate_and_canonicalize("18", "time").unwrap(),
401 "1800"
402 );
403 assert_eq!(
404 TimeHandler::validate_and_canonicalize("19", "time").unwrap(),
405 "1900"
406 );
407 assert_eq!(
408 TimeHandler::validate_and_canonicalize("20", "time").unwrap(),
409 "2000"
410 );
411 assert_eq!(
412 TimeHandler::validate_and_canonicalize("21", "time").unwrap(),
413 "2100"
414 );
415 assert_eq!(
416 TimeHandler::validate_and_canonicalize("22", "time").unwrap(),
417 "2200"
418 );
419 assert_eq!(
420 TimeHandler::validate_and_canonicalize("23", "time").unwrap(),
421 "2300"
422 );
423 }
424
425 #[test]
427 fn test_invalid_hours() {
428 assert!(TimeHandler::validate_and_canonicalize("24", "time").is_err());
429 assert!(TimeHandler::validate_and_canonicalize("25", "time").is_err());
430 assert!(TimeHandler::validate_and_canonicalize("99", "time").is_err());
431 }
432
433 #[test]
435 fn test_hh_mm_format_single_digit_hours() {
436 assert_eq!(
437 TimeHandler::validate_and_canonicalize("0:00", "time").unwrap(),
438 "0000"
439 );
440 assert_eq!(
441 TimeHandler::validate_and_canonicalize("1:15", "time").unwrap(),
442 "0115"
443 );
444 assert_eq!(
445 TimeHandler::validate_and_canonicalize("9:30", "time").unwrap(),
446 "0930"
447 );
448 assert_eq!(
449 TimeHandler::validate_and_canonicalize("9:05", "time").unwrap(),
450 "0905"
451 );
452 }
453
454 #[test]
456 fn test_hh_mm_format_double_digit_hours() {
457 assert_eq!(
458 TimeHandler::validate_and_canonicalize("10:00", "time").unwrap(),
459 "1000"
460 );
461 assert_eq!(
462 TimeHandler::validate_and_canonicalize("14:30", "time").unwrap(),
463 "1430"
464 );
465 assert_eq!(
466 TimeHandler::validate_and_canonicalize("23:59", "time").unwrap(),
467 "2359"
468 );
469 assert_eq!(
470 TimeHandler::validate_and_canonicalize("00:00", "time").unwrap(),
471 "0000"
472 );
473 }
474
475 #[test]
477 fn test_hh_mm_format_boundaries() {
478 assert_eq!(
479 TimeHandler::validate_and_canonicalize("00:00", "time").unwrap(),
480 "0000"
481 );
482 assert_eq!(
483 TimeHandler::validate_and_canonicalize("23:59", "time").unwrap(),
484 "2359"
485 );
486 assert_eq!(
487 TimeHandler::validate_and_canonicalize("12:00", "time").unwrap(),
488 "1200"
489 );
490 assert_eq!(
491 TimeHandler::validate_and_canonicalize("0:59", "time").unwrap(),
492 "0059"
493 );
494 }
495
496 #[test]
498 fn test_invalid_hh_mm_format() {
499 assert!(TimeHandler::validate_and_canonicalize("24:00", "time").is_err());
501 assert!(TimeHandler::validate_and_canonicalize("25:30", "time").is_err());
502
503 assert!(TimeHandler::validate_and_canonicalize("12:60", "time").is_err());
505 assert!(TimeHandler::validate_and_canonicalize("12:99", "time").is_err());
506
507 assert!(TimeHandler::validate_and_canonicalize("12:5", "time").is_err()); assert!(TimeHandler::validate_and_canonicalize("1:2", "time").is_err()); }
511
512 #[test]
514 fn test_hhmm_format() {
515 assert_eq!(
516 TimeHandler::validate_and_canonicalize("0000", "time").unwrap(),
517 "0000"
518 );
519 assert_eq!(
520 TimeHandler::validate_and_canonicalize("0100", "time").unwrap(),
521 "0100"
522 );
523 assert_eq!(
524 TimeHandler::validate_and_canonicalize("0905", "time").unwrap(),
525 "0905"
526 );
527 assert_eq!(
528 TimeHandler::validate_and_canonicalize("1430", "time").unwrap(),
529 "1430"
530 );
531 assert_eq!(
532 TimeHandler::validate_and_canonicalize("2359", "time").unwrap(),
533 "2359"
534 );
535 }
536
537 #[test]
539 fn test_invalid_hhmm_format() {
540 assert!(TimeHandler::validate_and_canonicalize("2400", "time").is_err());
542 assert!(TimeHandler::validate_and_canonicalize("2500", "time").is_err());
543
544 assert!(TimeHandler::validate_and_canonicalize("1260", "time").is_err());
546 assert!(TimeHandler::validate_and_canonicalize("1299", "time").is_err());
547
548 assert!(TimeHandler::validate_and_canonicalize("123", "time").is_err()); assert!(TimeHandler::validate_and_canonicalize("12345", "time").is_err()); }
552
553 #[test]
555 fn test_invalid_formats() {
556 assert!(TimeHandler::validate_and_canonicalize("", "time").is_err());
557 assert!(TimeHandler::validate_and_canonicalize("invalid", "time").is_err());
558 assert!(TimeHandler::validate_and_canonicalize("abc", "time").is_err());
559 assert!(TimeHandler::validate_and_canonicalize("12:ab", "time").is_err());
560 assert!(TimeHandler::validate_and_canonicalize("ab:30", "time").is_err());
561 assert!(TimeHandler::validate_and_canonicalize("12.30", "time").is_err()); assert!(TimeHandler::validate_and_canonicalize("12-30", "time").is_err()); }
564
565 #[test]
567 fn test_edge_cases() {
568 assert!(TimeHandler::validate_and_canonicalize(" 12", "time").is_err());
570 assert!(TimeHandler::validate_and_canonicalize("12 ", "time").is_err());
571 assert!(TimeHandler::validate_and_canonicalize(" 12:30", "time").is_err());
572 assert!(TimeHandler::validate_and_canonicalize("12:30 ", "time").is_err());
573
574 assert!(TimeHandler::validate_and_canonicalize("1a", "time").is_err());
576 assert!(TimeHandler::validate_and_canonicalize("a1", "time").is_err());
577 }
578
579 #[test]
581 fn test_format_consistency() {
582 assert_eq!(
584 TimeHandler::validate_and_canonicalize("1", "time").unwrap(),
585 "0100"
586 );
587 assert_eq!(
588 TimeHandler::validate_and_canonicalize("01", "time").unwrap(),
589 "0100"
590 );
591 assert_eq!(
592 TimeHandler::validate_and_canonicalize("1:00", "time").unwrap(),
593 "0100"
594 );
595 assert_eq!(
596 TimeHandler::validate_and_canonicalize("0100", "time").unwrap(),
597 "0100"
598 );
599
600 assert_eq!(
602 TimeHandler::validate_and_canonicalize("14:30", "time").unwrap(),
603 "1430"
604 );
605 assert_eq!(
606 TimeHandler::validate_and_canonicalize("1430", "time").unwrap(),
607 "1430"
608 );
609
610 assert_eq!(
612 TimeHandler::validate_and_canonicalize("0", "time").unwrap(),
613 "0000"
614 );
615 assert_eq!(
616 TimeHandler::validate_and_canonicalize("00", "time").unwrap(),
617 "0000"
618 );
619 assert_eq!(
620 TimeHandler::validate_and_canonicalize("0:00", "time").unwrap(),
621 "0000"
622 );
623 assert_eq!(
624 TimeHandler::validate_and_canonicalize("0000", "time").unwrap(),
625 "0000"
626 );
627 }
628}