kanban_core/
datetime_input.rs1use chrono::{DateTime, NaiveDate, Utc};
2
3pub fn parse_datetime_input(input: &str) -> Result<DateTime<Utc>, String> {
4 let trimmed = input.trim();
5 if let Ok(dt) = DateTime::parse_from_rfc3339(trimmed) {
6 return Ok(dt.with_timezone(&Utc));
7 }
8 if let Ok(date) = NaiveDate::parse_from_str(trimmed, "%Y-%m-%d") {
9 if date.format("%Y-%m-%d").to_string() == trimmed {
12 let naive = date
13 .and_hms_opt(0, 0, 0)
14 .expect("midnight is always a valid time");
15 return Ok(naive.and_utc());
16 }
17 }
18 Err(format!(
19 "Invalid date '{input}'. Supported formats: YYYY-MM-DD or RFC 3339 (e.g. 2024-01-15 or 2024-01-15T14:30:00Z)"
20 ))
21}
22
23#[cfg(test)]
24mod tests {
25 use super::*;
26 use chrono::TimeZone;
27
28 #[test]
29 fn test_parse_yyyy_mm_dd_returns_midnight_utc() {
30 let dt = parse_datetime_input("2024-01-15").unwrap();
31 assert_eq!(dt, Utc.with_ymd_and_hms(2024, 1, 15, 0, 0, 0).unwrap());
32 }
33
34 #[test]
35 fn test_parse_full_rfc3339_z_suffix_preserved() {
36 let dt = parse_datetime_input("2024-01-15T14:30:00Z").unwrap();
37 assert_eq!(dt, Utc.with_ymd_and_hms(2024, 1, 15, 14, 30, 0).unwrap());
38 }
39
40 #[test]
41 fn test_parse_full_rfc3339_with_offset_normalized_to_utc() {
42 let dt = parse_datetime_input("2024-01-15T16:30:00+02:00").unwrap();
43 assert_eq!(dt, Utc.with_ymd_and_hms(2024, 1, 15, 14, 30, 0).unwrap());
44 }
45
46 #[test]
47 fn test_parse_leading_and_trailing_whitespace_tolerated() {
48 let dt = parse_datetime_input(" 2024-01-15 ").unwrap();
49 assert_eq!(dt, Utc.with_ymd_and_hms(2024, 1, 15, 0, 0, 0).unwrap());
50 }
51
52 #[test]
53 fn test_parse_iso8601_non_zero_padded_date_rejected() {
54 let err = parse_datetime_input("2024-1-5").unwrap_err();
55 assert!(
56 err.contains("2024-1-5"),
57 "error should quote the offending input, got: {err}"
58 );
59 }
60
61 #[test]
62 fn test_parse_garbage_input_returns_descriptive_error() {
63 let err = parse_datetime_input("yesterday").unwrap_err();
64 assert!(
65 err.contains("yesterday"),
66 "error should quote the offending input, got: {err}"
67 );
68 assert!(
69 err.contains("YYYY-MM-DD"),
70 "error should mention the supported format, got: {err}"
71 );
72 }
73
74 #[test]
75 fn test_parse_empty_string_returns_error() {
76 assert!(parse_datetime_input("").is_err());
77 }
78
79 #[test]
80 fn test_parse_whitespace_only_returns_error() {
81 assert!(parse_datetime_input(" ").is_err());
82 }
83
84 #[test]
85 fn test_parse_date_with_invalid_month_returns_error() {
86 assert!(parse_datetime_input("2024-13-01").is_err());
87 }
88
89 #[test]
90 fn test_parse_date_with_invalid_day_returns_error() {
91 assert!(parse_datetime_input("2024-02-30").is_err());
92 }
93}