1use time::{Date, Month, PrimitiveDateTime, Time};
4
5pub fn parse_datetime(s: &str) -> Option<PrimitiveDateTime> {
15 let t = s.trim().trim_matches('\'').trim();
16 let (date_part, time_part) = match t.split_once('T') {
17 Some((d, tm)) => (d, Some(tm)),
18 None => (t, None),
19 };
20
21 let mut d = date_part.split('-');
22 let year: i32 = d.next()?.parse().ok()?;
23 let month: u8 = d.next()?.parse().ok()?;
24 let day: u8 = d.next()?.parse().ok()?;
25 if d.next().is_some() {
26 return None;
27 }
28 let date = Date::from_calendar_date(year, Month::try_from(month).ok()?, day).ok()?;
29
30 let time = match time_part {
31 None => Time::MIDNIGHT,
32 Some(tp) => {
33 let mut parts = tp.split(':');
34 let hour: u8 = parts.next()?.parse().ok()?;
35 let minute: u8 = parts.next()?.parse().ok()?;
36 let (sec, nanos) = match parts.next() {
37 None => (0u8, 0u32),
38 Some(sec_field) => {
39 let (whole, frac) = match sec_field.split_once('.') {
40 Some((w, f)) => (w, Some(f)),
41 None => (sec_field, None),
42 };
43 let sec: u8 = whole.parse().ok()?;
44 let nanos = match frac {
45 None => 0,
46 Some(f) => {
47 let mut digits: String = f.chars().take(9).collect();
48 while digits.len() < 9 {
49 digits.push('0');
50 }
51 digits.parse::<u32>().ok()?
52 }
53 };
54 (sec, nanos)
55 }
56 };
57 if parts.next().is_some() {
58 return None;
59 }
60 Time::from_hms_nano(hour, minute, sec, nanos).ok()?
61 }
62 };
63
64 Some(PrimitiveDateTime::new(date, time))
65}
66
67pub fn format_datetime(dt: &PrimitiveDateTime) -> String {
70 let (d, t) = (dt.date(), dt.time());
71 let base = format!(
72 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
73 d.year(),
74 d.month() as u8,
75 d.day(),
76 t.hour(),
77 t.minute(),
78 t.second()
79 );
80 let nanos = t.nanosecond();
81 if nanos == 0 {
82 base
83 } else {
84 let frac = format!("{nanos:09}");
85 format!("{base}.{}", frac.trim_end_matches('0'))
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn date_only_is_midnight() {
95 let dt = parse_datetime("2026-07-11").unwrap();
96 assert_eq!(dt.time(), Time::MIDNIGHT);
97 assert_eq!(format_datetime(&dt), "2026-07-11T00:00:00");
98 }
99
100 #[test]
101 fn seconds_and_fraction_are_optional() {
102 assert_eq!(
103 format_datetime(&parse_datetime("2026-07-11T22:15").unwrap()),
104 "2026-07-11T22:15:00"
105 );
106 let dt = parse_datetime("2026-07-11T22:15:03.25").unwrap();
107 assert_eq!(dt.time().nanosecond(), 250_000_000);
108 assert_eq!(format_datetime(&dt), "2026-07-11T22:15:03.25");
109 }
110
111 #[test]
112 fn fraction_beyond_nanoseconds_is_truncated() {
113 let dt = parse_datetime("2026-07-11T00:00:00.1234567891234").unwrap();
114 assert_eq!(dt.time().nanosecond(), 123_456_789);
115 }
116
117 #[test]
118 fn quoted_input_is_tolerated() {
119 assert!(parse_datetime("'2026-07-11T01:02:03'").is_some());
120 assert!(parse_datetime(" 2026-07-11 ").is_some());
121 }
122
123 #[test]
124 fn invalid_forms_are_none() {
125 for bad in [
126 "2026-13-01", "2026-02-30", "2026-07-11T25:00:00", "2026-07-11-05", "2026-07-11T1:2:3:4", "2026", "not a date",
133 ] {
134 assert!(parse_datetime(bad).is_none(), "{bad:?} should not parse");
135 }
136 }
137
138 #[test]
139 fn format_trims_trailing_fraction_zeros() {
140 let dt = parse_datetime("2026-07-11T00:00:00.100").unwrap();
141 assert_eq!(format_datetime(&dt), "2026-07-11T00:00:00.1");
142 let dt = parse_datetime("2026-07-11T00:00:00.000").unwrap();
143 assert_eq!(format_datetime(&dt), "2026-07-11T00:00:00");
144 }
145}