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(feature = "coords")]
91fn mjd_epoch() -> PrimitiveDateTime {
92 PrimitiveDateTime::new(
93 Date::from_calendar_date(1858, Month::November, 17).expect("valid MJD epoch"),
94 Time::MIDNIGHT,
95 )
96}
97
98#[cfg(feature = "coords")]
100pub fn mjd_to_datetime(mjd: f64) -> PrimitiveDateTime {
101 mjd_epoch() + time::Duration::seconds_f64(mjd * 86_400.0)
102}
103
104#[cfg(feature = "coords")]
106pub fn datetime_to_mjd(dt: &PrimitiveDateTime) -> f64 {
107 (*dt - mjd_epoch()).as_seconds_f64() / 86_400.0
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn date_only_is_midnight() {
116 let dt = parse_datetime("2026-07-11").unwrap();
117 assert_eq!(dt.time(), Time::MIDNIGHT);
118 assert_eq!(format_datetime(&dt), "2026-07-11T00:00:00");
119 }
120
121 #[test]
122 fn seconds_and_fraction_are_optional() {
123 assert_eq!(
124 format_datetime(&parse_datetime("2026-07-11T22:15").unwrap()),
125 "2026-07-11T22:15:00"
126 );
127 let dt = parse_datetime("2026-07-11T22:15:03.25").unwrap();
128 assert_eq!(dt.time().nanosecond(), 250_000_000);
129 assert_eq!(format_datetime(&dt), "2026-07-11T22:15:03.25");
130 }
131
132 #[test]
133 fn fraction_beyond_nanoseconds_is_truncated() {
134 let dt = parse_datetime("2026-07-11T00:00:00.1234567891234").unwrap();
135 assert_eq!(dt.time().nanosecond(), 123_456_789);
136 }
137
138 #[test]
139 fn quoted_input_is_tolerated() {
140 assert!(parse_datetime("'2026-07-11T01:02:03'").is_some());
141 assert!(parse_datetime(" 2026-07-11 ").is_some());
142 }
143
144 #[test]
145 fn invalid_forms_are_none() {
146 for bad in [
147 "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",
154 ] {
155 assert!(parse_datetime(bad).is_none(), "{bad:?} should not parse");
156 }
157 }
158
159 #[test]
160 fn format_trims_trailing_fraction_zeros() {
161 let dt = parse_datetime("2026-07-11T00:00:00.100").unwrap();
162 assert_eq!(format_datetime(&dt), "2026-07-11T00:00:00.1");
163 let dt = parse_datetime("2026-07-11T00:00:00.000").unwrap();
164 assert_eq!(format_datetime(&dt), "2026-07-11T00:00:00");
165 }
166
167 #[cfg(feature = "coords")]
168 #[test]
169 fn mjd_epoch_is_zero() {
170 assert_eq!(datetime_to_mjd(&mjd_epoch()), 0.0);
171 assert_eq!(mjd_to_datetime(0.0), mjd_epoch());
172 }
173}