1use crate::bcd::{from_bcd_byte, to_bcd_byte};
9use core::time::Duration;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct MjdBcdDateTime {
18 pub year: u16,
20 pub month: u8,
22 pub day: u8,
24 pub hour: u8,
26 pub minute: u8,
28 pub second: u8,
30}
31
32#[must_use]
40pub fn decode_mjd_bcd(raw: [u8; 5]) -> Option<MjdBcdDateTime> {
41 let mjd = u16::from_be_bytes([raw[0], raw[1]]);
42 let h = from_bcd_byte(raw[2])?;
43 let mi = from_bcd_byte(raw[3])?;
44 let s = from_bcd_byte(raw[4])?;
45 if mi > 59 || s > 59 || h > 23 {
46 return None;
47 }
48 let (year, month, day) = mjd_to_ymd_nogate(mjd)?;
49 Some(MjdBcdDateTime {
50 year,
51 month,
52 day,
53 hour: h,
54 minute: mi,
55 second: s,
56 })
57}
58
59#[must_use]
63pub fn encode_mjd_bcd(dt: MjdBcdDateTime) -> Option<[u8; 5]> {
64 let mjd = ymd_to_mjd_nogate(i32::from(dt.year), u32::from(dt.month), u32::from(dt.day))?;
65 let [m0, m1] = mjd.to_be_bytes();
66 Some([
67 m0,
68 m1,
69 to_bcd_byte(dt.hour)?,
70 to_bcd_byte(dt.minute)?,
71 to_bcd_byte(dt.second)?,
72 ])
73}
74
75fn mjd_to_ymd_nogate(mjd: u16) -> Option<(u16, u8, u8)> {
80 let mjd = i64::from(mjd);
81 let y_prime = ((mjd as f64 - 15_078.2) / 365.25) as i64;
82 let m_prime = ((mjd as f64 - 14_956.1 - (y_prime as f64 * 365.25).floor()) / 30.6001) as i64;
83 let d = mjd
84 - 14_956
85 - (y_prime as f64 * 365.25).floor() as i64
86 - (m_prime as f64 * 30.6001).floor() as i64;
87 let k = i64::from(m_prime == 14 || m_prime == 15);
88 let y = y_prime + k + 1900;
89 let m = m_prime - 1 - k * 12;
90 let y_u16 = u16::try_from(y).ok()?;
91 let m_u8 = u8::try_from(m).ok()?;
92 let d_u8 = u8::try_from(d).ok()?;
93 if !(1..=12).contains(&m_u8) || !(1..=31).contains(&d_u8) {
94 return None;
95 }
96 Some((y_u16, m_u8, d_u8))
97}
98
99fn ymd_to_mjd_nogate(year: i32, month: u32, day: u32) -> Option<u16> {
104 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
105 return None;
106 }
107 let l = if month <= 2 { 1.0 } else { 0.0 };
108 let y = f64::from(year - 1900);
109 let m = f64::from(month);
110 let mjd = 14_956.0
111 + f64::from(day)
112 + ((y - l) * 365.25).floor()
113 + ((m + 1.0 + l * 12.0) * 30.6001).floor();
114 if (0.0..=f64::from(u16::MAX)).contains(&mjd) {
115 Some(mjd as u16)
116 } else {
117 None
118 }
119}
120
121#[must_use]
126pub fn decode_bcd_duration(raw: [u8; 3]) -> Option<Duration> {
127 let h = u64::from(from_bcd_byte(raw[0])?);
128 let m = u64::from(from_bcd_byte(raw[1])?);
129 let s = u64::from(from_bcd_byte(raw[2])?);
130 if m > 59 || s > 59 {
131 return None;
132 }
133 Some(Duration::from_secs(h * 3600 + m * 60 + s))
134}
135
136#[must_use]
141pub fn encode_bcd_duration(duration: Duration) -> Option<[u8; 3]> {
142 let secs = duration.as_secs();
143 let h = secs / 3600;
144 if h > 99 {
145 return None;
146 }
147 let m = (secs % 3600) / 60;
148 let s = secs % 60;
149 Some([
150 to_bcd_byte(h as u8)?,
151 to_bcd_byte(m as u8)?,
152 to_bcd_byte(s as u8)?,
153 ])
154}
155
156#[cfg(feature = "chrono")]
160#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
161#[must_use]
162pub fn mjd_to_ymd(mjd: u16) -> (i32, u32, u32) {
163 let mjd = i64::from(mjd);
164 let y_prime = ((mjd as f64 - 15_078.2) / 365.25) as i64;
165 let m_prime = ((mjd as f64 - 14_956.1 - (y_prime as f64 * 365.25).floor()) / 30.6001) as i64;
166 let d = mjd
167 - 14_956
168 - (y_prime as f64 * 365.25).floor() as i64
169 - (m_prime as f64 * 30.6001).floor() as i64;
170 let k = i64::from(m_prime == 14 || m_prime == 15);
171 let y = y_prime + k + 1900;
172 let m = m_prime - 1 - k * 12;
173 (y as i32, m as u32, d as u32)
174}
175
176#[cfg(feature = "chrono")]
182#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
183#[must_use]
184pub fn ymd_to_mjd(year: i32, month: u32, day: u32) -> Option<u16> {
185 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
186 return None;
187 }
188 let l = if month <= 2 { 1.0 } else { 0.0 };
189 let y = f64::from(year - 1900);
190 let m = f64::from(month);
191 let mjd = 14_956.0
192 + f64::from(day)
193 + ((y - l) * 365.25).floor()
194 + ((m + 1.0 + l * 12.0) * 30.6001).floor();
195 if (0.0..=f64::from(u16::MAX)).contains(&mjd) {
196 Some(mjd as u16)
197 } else {
198 None
199 }
200}
201
202#[cfg(feature = "chrono")]
208#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
209#[must_use]
210pub fn decode_mjd_bcd_utc(raw: [u8; 5]) -> Option<chrono::DateTime<chrono::Utc>> {
211 use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
212 let mjd = u16::from_be_bytes([raw[0], raw[1]]);
213 let (y, m, d) = mjd_to_ymd(mjd);
214 let h = from_bcd_byte(raw[2])?;
215 let mi = from_bcd_byte(raw[3])?;
216 let s = from_bcd_byte(raw[4])?;
217 let date = NaiveDate::from_ymd_opt(y, m, d)?;
218 let time = NaiveTime::from_hms_opt(u32::from(h), u32::from(mi), u32::from(s))?;
219 chrono::Utc
220 .from_local_datetime(&NaiveDateTime::new(date, time))
221 .single()
222}
223
224#[cfg(feature = "chrono")]
230#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
231#[must_use]
232pub fn encode_mjd_bcd_utc(dt: chrono::DateTime<chrono::Utc>) -> Option<[u8; 5]> {
233 use chrono::{Datelike, Timelike};
234 let naive = dt.naive_utc();
235 let mjd = ymd_to_mjd(naive.year(), naive.month(), naive.day())?;
236 let [m0, m1] = mjd.to_be_bytes();
237 Some([
238 m0,
239 m1,
240 to_bcd_byte(naive.hour() as u8)?,
241 to_bcd_byte(naive.minute() as u8)?,
242 to_bcd_byte(naive.second() as u8)?,
243 ])
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn duration_round_trips() {
252 for &(h, m, s) in &[(0u64, 0u64, 0u64), (1, 30, 45), (99, 59, 59), (2, 0, 0)] {
253 let secs = h * 3600 + m * 60 + s;
254 let raw = encode_bcd_duration(Duration::from_secs(secs)).expect("encodes");
255 assert_eq!(decode_bcd_duration(raw), Some(Duration::from_secs(secs)));
256 }
257 }
258
259 #[test]
260 fn duration_decode_known_vector() {
261 assert_eq!(
263 decode_bcd_duration([0x01, 0x30, 0x45]),
264 Some(Duration::from_secs(5445))
265 );
266 }
267
268 #[test]
269 fn duration_rejects_over_99h_and_bad_fields() {
270 assert_eq!(encode_bcd_duration(Duration::from_secs(100 * 3600)), None);
271 assert_eq!(decode_bcd_duration([0x01, 0x75, 0x00]), None); assert_eq!(decode_bcd_duration([0x01, 0x00, 0x1A]), None); }
274
275 #[cfg(feature = "chrono")]
276 #[test]
277 fn ymd_to_mjd_matches_chrono_epoch_arithmetic() {
278 use chrono::NaiveDate;
279 let epoch = NaiveDate::from_ymd_opt(1858, 11, 17).unwrap();
281 for &(y, m, d) in &[(1993, 10, 13), (2000, 1, 1), (2023, 6, 8), (1900, 3, 1)] {
282 let date = NaiveDate::from_ymd_opt(y, m, d).unwrap();
283 let expected = (date - epoch).num_days() as u16;
284 assert_eq!(ymd_to_mjd(y, m, d), Some(expected), "{y}-{m}-{d}");
285 }
286 }
287
288 #[cfg(feature = "chrono")]
289 #[test]
290 fn mjd_ymd_round_trips() {
291 for mjd in [40_587u16, 49_273, 51_544, 59_945, 60_000] {
292 let (y, m, d) = mjd_to_ymd(mjd);
293 assert_eq!(ymd_to_mjd(y, m, d), Some(mjd), "mjd {mjd}");
294 }
295 }
296
297 #[cfg(feature = "chrono")]
298 #[test]
299 fn utc_round_trips() {
300 let raw = [0xE4, 0x09, 0x12, 0x34, 0x56];
301 let dt = decode_mjd_bcd_utc(raw).expect("decodes");
302 assert_eq!(encode_mjd_bcd_utc(dt), Some(raw));
303 }
304
305 #[cfg(feature = "chrono")]
306 #[test]
307 fn utc_decode_known_vector() {
308 use chrono::{Datelike, Timelike};
309 let dt = decode_mjd_bcd_utc([0xE4, 0x09, 0x12, 0x34, 0x56]).expect("decodes");
311 assert_eq!((dt.hour(), dt.minute(), dt.second()), (12, 34, 56));
312 assert_eq!(dt.year(), 2018);
313 }
314
315 #[test]
316 fn mjd_bcd_round_trips() {
317 for &(y, m, d, h, mi, s) in &[
318 (2023u16, 1u8, 1u8, 12u8, 34u8, 56u8),
319 (2000, 1, 1, 0, 0, 0),
320 (2023, 6, 8, 23, 59, 59),
321 ] {
322 let dt = MjdBcdDateTime {
323 year: y,
324 month: m,
325 day: d,
326 hour: h,
327 minute: mi,
328 second: s,
329 };
330 let raw = encode_mjd_bcd(dt).expect("encodes");
331 let re = decode_mjd_bcd(raw).expect("decodes");
332 assert_eq!(re, dt);
333 }
334 }
335
336 #[test]
337 fn mjd_bcd_rejects_invalid_bcd() {
338 assert_eq!(decode_mjd_bcd([0xE4, 0x09, 0x1A, 0x34, 0x56]), None);
339 assert_eq!(decode_mjd_bcd([0xE4, 0x09, 0x12, 0x75, 0x56]), None);
340 }
341
342 #[test]
343 fn mjd_bcd_matches_chrono_when_available() {
344 let raw = [0xE4, 0x09, 0x12, 0x34, 0x56];
345 let plain = decode_mjd_bcd(raw).expect("decodes");
346 #[cfg(feature = "chrono")]
347 {
348 use chrono::{Datelike, Timelike};
349 let chrono_dt = decode_mjd_bcd_utc(raw).expect("decodes");
350 assert_eq!(plain.year as i32, chrono_dt.year());
351 assert_eq!(plain.month as u32, chrono_dt.month());
352 assert_eq!(plain.day as u32, chrono_dt.day());
353 assert_eq!(plain.hour as u32, chrono_dt.hour());
354 assert_eq!(plain.minute as u32, chrono_dt.minute());
355 assert_eq!(plain.second as u32, chrono_dt.second());
356 }
357 assert_eq!(plain.year, 2018);
359 assert_eq!(plain.month, 9);
360 assert_eq!(plain.day, 16);
361 }
362}