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 {
77 let (d, t) = (dt.date(), dt.time());
78 let base = format!(
79 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
80 d.year(),
81 d.month() as u8,
82 d.day(),
83 t.hour(),
84 t.minute(),
85 t.second()
86 );
87 let nanos = t.nanosecond();
88 if nanos == 0 {
89 base
90 } else {
91 let frac = format!("{nanos:09}");
92 format!("{base}.{}", frac.trim_end_matches('0'))
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn date_only_is_midnight() {
102 let dt = parse_datetime("2026-07-11").unwrap();
103 assert_eq!(dt.time(), Time::MIDNIGHT);
104 assert_eq!(format_datetime(&dt), "2026-07-11T00:00:00");
105 }
106
107 #[test]
108 fn seconds_and_fraction_are_optional() {
109 assert_eq!(
110 format_datetime(&parse_datetime("2026-07-11T22:15").unwrap()),
111 "2026-07-11T22:15:00"
112 );
113 let dt = parse_datetime("2026-07-11T22:15:03.25").unwrap();
114 assert_eq!(dt.time().nanosecond(), 250_000_000);
115 assert_eq!(format_datetime(&dt), "2026-07-11T22:15:03.25");
116 }
117
118 #[test]
119 fn fraction_beyond_nanoseconds_is_truncated() {
120 let dt = parse_datetime("2026-07-11T00:00:00.1234567891234").unwrap();
121 assert_eq!(dt.time().nanosecond(), 123_456_789);
122 }
123
124 #[test]
125 fn quoted_input_is_tolerated() {
126 assert!(parse_datetime("'2026-07-11T01:02:03'").is_some());
127 assert!(parse_datetime(" 2026-07-11 ").is_some());
128 }
129
130 #[test]
131 fn invalid_forms_are_none() {
132 for bad in [
133 "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",
140 ] {
141 assert!(parse_datetime(bad).is_none(), "{bad:?} should not parse");
142 }
143 }
144
145 #[test]
146 fn format_trims_trailing_fraction_zeros() {
147 let dt = parse_datetime("2026-07-11T00:00:00.100").unwrap();
148 assert_eq!(format_datetime(&dt), "2026-07-11T00:00:00.1");
149 let dt = parse_datetime("2026-07-11T00:00:00.000").unwrap();
150 assert_eq!(format_datetime(&dt), "2026-07-11T00:00:00");
151 }
152}