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_bytes, _) = raw.split_first_chunk::<2>().unwrap();
42 let mjd = u16::from_be_bytes(*mjd_bytes);
43 let h = from_bcd_byte(raw[2])?;
44 let mi = from_bcd_byte(raw[3])?;
45 let s = from_bcd_byte(raw[4])?;
46 if mi > 59 || s > 59 || h > 23 {
47 return None;
48 }
49 let (year, month, day) = mjd_to_ymd_nogate(mjd)?;
50 Some(MjdBcdDateTime {
51 year,
52 month,
53 day,
54 hour: h,
55 minute: mi,
56 second: s,
57 })
58}
59
60#[must_use]
64pub fn encode_mjd_bcd(dt: MjdBcdDateTime) -> Option<[u8; 5]> {
65 let mjd = ymd_to_mjd_nogate(i32::from(dt.year), u32::from(dt.month), u32::from(dt.day))?;
66 let [m0, m1] = mjd.to_be_bytes();
67 Some([
68 m0,
69 m1,
70 to_bcd_byte(dt.hour)?,
71 to_bcd_byte(dt.minute)?,
72 to_bcd_byte(dt.second)?,
73 ])
74}
75
76fn mjd_to_ymd_nogate(mjd: u16) -> Option<(u16, u8, u8)> {
81 let mjd = i64::from(mjd);
82 let y_prime = ((mjd as f64 - 15_078.2) / 365.25) as i64;
83 let m_prime = ((mjd as f64 - 14_956.1 - libm::floor(y_prime as f64 * 365.25)) / 30.6001) as i64;
84 let d = mjd
85 - 14_956
86 - libm::floor(y_prime as f64 * 365.25) as i64
87 - libm::floor(m_prime as f64 * 30.6001) as i64;
88 let k = i64::from(m_prime == 14 || m_prime == 15);
89 let y = y_prime + k + 1900;
90 let m = m_prime - 1 - k * 12;
91 let y_u16 = u16::try_from(y).ok()?;
92 let m_u8 = u8::try_from(m).ok()?;
93 let d_u8 = u8::try_from(d).ok()?;
94 if !(1..=12).contains(&m_u8) || !(1..=31).contains(&d_u8) {
95 return None;
96 }
97 Some((y_u16, m_u8, d_u8))
98}
99
100fn ymd_to_mjd_nogate(year: i32, month: u32, day: u32) -> Option<u16> {
105 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
106 return None;
107 }
108 let l = if month <= 2 { 1.0 } else { 0.0 };
109 let y = f64::from(year - 1900);
110 let m = f64::from(month);
111 let mjd = 14_956.0
112 + f64::from(day)
113 + libm::floor((y - l) * 365.25)
114 + libm::floor((m + 1.0 + l * 12.0) * 30.6001);
115 if (0.0..=f64::from(u16::MAX)).contains(&mjd) {
116 Some(mjd as u16)
117 } else {
118 None
119 }
120}
121
122#[must_use]
127pub fn decode_bcd_duration(raw: [u8; 3]) -> Option<Duration> {
128 let h = u64::from(from_bcd_byte(raw[0])?);
129 let m = u64::from(from_bcd_byte(raw[1])?);
130 let s = u64::from(from_bcd_byte(raw[2])?);
131 if m > 59 || s > 59 {
132 return None;
133 }
134 Some(Duration::from_secs(h * 3600 + m * 60 + s))
135}
136
137#[must_use]
142pub fn encode_bcd_duration(duration: Duration) -> Option<[u8; 3]> {
143 let secs = duration.as_secs();
144 let h = secs / 3600;
145 if h > 99 {
146 return None;
147 }
148 let m = (secs % 3600) / 60;
149 let s = secs % 60;
150 Some([
151 to_bcd_byte(h as u8)?,
152 to_bcd_byte(m as u8)?,
153 to_bcd_byte(s as u8)?,
154 ])
155}
156
157#[cfg(feature = "chrono")]
161#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
162#[must_use]
163pub fn mjd_to_ymd(mjd: u16) -> (i32, u32, u32) {
164 let mjd = i64::from(mjd);
165 let y_prime = ((mjd as f64 - 15_078.2) / 365.25) as i64;
166 let m_prime = ((mjd as f64 - 14_956.1 - libm::floor(y_prime as f64 * 365.25)) / 30.6001) as i64;
167 let d = mjd
168 - 14_956
169 - libm::floor(y_prime as f64 * 365.25) as i64
170 - libm::floor(m_prime as f64 * 30.6001) as i64;
171 let k = i64::from(m_prime == 14 || m_prime == 15);
172 let y = y_prime + k + 1900;
173 let m = m_prime - 1 - k * 12;
174 (y as i32, m as u32, d as u32)
175}
176
177#[cfg(feature = "chrono")]
183#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
184#[must_use]
185pub fn ymd_to_mjd(year: i32, month: u32, day: u32) -> Option<u16> {
186 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
187 return None;
188 }
189 let l = if month <= 2 { 1.0 } else { 0.0 };
190 let y = f64::from(year - 1900);
191 let m = f64::from(month);
192 let mjd = 14_956.0
193 + f64::from(day)
194 + libm::floor((y - l) * 365.25)
195 + libm::floor((m + 1.0 + l * 12.0) * 30.6001);
196 if (0.0..=f64::from(u16::MAX)).contains(&mjd) {
197 Some(mjd as u16)
198 } else {
199 None
200 }
201}
202
203#[cfg(feature = "chrono")]
209#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
210#[must_use]
211pub fn decode_mjd_bcd_utc(raw: [u8; 5]) -> Option<chrono::DateTime<chrono::Utc>> {
212 use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
213 let (mjd_bytes, _) = raw.split_first_chunk::<2>().unwrap();
214 let mjd = u16::from_be_bytes(*mjd_bytes);
215 let (y, m, d) = mjd_to_ymd(mjd);
216 let h = from_bcd_byte(raw[2])?;
217 let mi = from_bcd_byte(raw[3])?;
218 let s = from_bcd_byte(raw[4])?;
219 let date = NaiveDate::from_ymd_opt(y, m, d)?;
220 let time = NaiveTime::from_hms_opt(u32::from(h), u32::from(mi), u32::from(s))?;
221 chrono::Utc
222 .from_local_datetime(&NaiveDateTime::new(date, time))
223 .single()
224}
225
226#[cfg(feature = "chrono")]
232#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
233#[must_use]
234pub fn encode_mjd_bcd_utc(dt: chrono::DateTime<chrono::Utc>) -> Option<[u8; 5]> {
235 use chrono::{Datelike, Timelike};
236 let naive = dt.naive_utc();
237 let mjd = ymd_to_mjd(naive.year(), naive.month(), naive.day())?;
238 let [m0, m1] = mjd.to_be_bytes();
239 Some([
240 m0,
241 m1,
242 to_bcd_byte(naive.hour() as u8)?,
243 to_bcd_byte(naive.minute() as u8)?,
244 to_bcd_byte(naive.second() as u8)?,
245 ])
246}
247
248pub const SECS_2000_EPOCH: i64 = 946_684_800;
255
256#[cfg(feature = "chrono")]
269#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
270#[must_use]
271pub fn decode_seconds_since_2000_utc(
272 seconds_since_2000: u64,
273 subsec_nanos: u32,
274 utco: u16,
275) -> Option<chrono::DateTime<chrono::Utc>> {
276 use chrono::TimeZone;
277 let unix_secs = SECS_2000_EPOCH
279 .checked_add(i64::try_from(seconds_since_2000).ok()?)?
280 .checked_sub(i64::from(utco))?;
281 chrono::Utc.timestamp_opt(unix_secs, subsec_nanos).single()
282}
283
284#[cfg(feature = "chrono")]
290#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
291#[must_use]
292pub fn encode_seconds_since_2000_utc(
293 dt: chrono::DateTime<chrono::Utc>,
294 utco: u16,
295) -> Option<(u64, u32)> {
296 use chrono::Timelike;
297 let unix_secs = dt.timestamp();
298 let raw = unix_secs
300 .checked_sub(SECS_2000_EPOCH)?
301 .checked_add(i64::from(utco))?;
302 let secs = u64::try_from(raw).ok()?;
303 if secs > 0xFF_FFFF_FFFF {
305 return None;
306 }
307 Some((secs, dt.nanosecond()))
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn duration_round_trips() {
316 for &(h, m, s) in &[(0u64, 0u64, 0u64), (1, 30, 45), (99, 59, 59), (2, 0, 0)] {
317 let secs = h * 3600 + m * 60 + s;
318 let raw = encode_bcd_duration(Duration::from_secs(secs)).expect("encodes");
319 assert_eq!(decode_bcd_duration(raw), Some(Duration::from_secs(secs)));
320 }
321 }
322
323 #[test]
324 fn duration_decode_known_vector() {
325 assert_eq!(
327 decode_bcd_duration([0x01, 0x30, 0x45]),
328 Some(Duration::from_secs(5445))
329 );
330 }
331
332 #[test]
333 fn duration_rejects_over_99h_and_bad_fields() {
334 assert_eq!(encode_bcd_duration(Duration::from_secs(100 * 3600)), None);
335 assert_eq!(decode_bcd_duration([0x01, 0x75, 0x00]), None); assert_eq!(decode_bcd_duration([0x01, 0x00, 0x1A]), None); }
338
339 #[cfg(feature = "chrono")]
340 #[test]
341 fn ymd_to_mjd_matches_chrono_epoch_arithmetic() {
342 use chrono::NaiveDate;
343 let epoch = NaiveDate::from_ymd_opt(1858, 11, 17).unwrap();
345 for &(y, m, d) in &[(1993, 10, 13), (2000, 1, 1), (2023, 6, 8), (1900, 3, 1)] {
346 let date = NaiveDate::from_ymd_opt(y, m, d).unwrap();
347 let expected = (date - epoch).num_days() as u16;
348 assert_eq!(ymd_to_mjd(y, m, d), Some(expected), "{y}-{m}-{d}");
349 }
350 }
351
352 #[cfg(feature = "chrono")]
353 #[test]
354 fn mjd_ymd_round_trips() {
355 for mjd in [40_587u16, 49_273, 51_544, 59_945, 60_000] {
356 let (y, m, d) = mjd_to_ymd(mjd);
357 assert_eq!(ymd_to_mjd(y, m, d), Some(mjd), "mjd {mjd}");
358 }
359 }
360
361 #[cfg(feature = "chrono")]
362 #[test]
363 fn utc_round_trips() {
364 let raw = [0xE4, 0x09, 0x12, 0x34, 0x56];
365 let dt = decode_mjd_bcd_utc(raw).expect("decodes");
366 assert_eq!(encode_mjd_bcd_utc(dt), Some(raw));
367 }
368
369 #[cfg(feature = "chrono")]
370 #[test]
371 fn utc_decode_known_vector() {
372 use chrono::{Datelike, Timelike};
373 let dt = decode_mjd_bcd_utc([0xE4, 0x09, 0x12, 0x34, 0x56]).expect("decodes");
375 assert_eq!((dt.hour(), dt.minute(), dt.second()), (12, 34, 56));
376 assert_eq!(dt.year(), 2018);
377 }
378
379 #[test]
380 fn mjd_bcd_round_trips() {
381 for &(y, m, d, h, mi, s) in &[
382 (2023u16, 1u8, 1u8, 12u8, 34u8, 56u8),
383 (2000, 1, 1, 0, 0, 0),
384 (2023, 6, 8, 23, 59, 59),
385 ] {
386 let dt = MjdBcdDateTime {
387 year: y,
388 month: m,
389 day: d,
390 hour: h,
391 minute: mi,
392 second: s,
393 };
394 let raw = encode_mjd_bcd(dt).expect("encodes");
395 let re = decode_mjd_bcd(raw).expect("decodes");
396 assert_eq!(re, dt);
397 }
398 }
399
400 #[test]
401 fn mjd_bcd_rejects_invalid_bcd() {
402 assert_eq!(decode_mjd_bcd([0xE4, 0x09, 0x1A, 0x34, 0x56]), None);
403 assert_eq!(decode_mjd_bcd([0xE4, 0x09, 0x12, 0x75, 0x56]), None);
404 }
405
406 #[test]
407 fn mjd_bcd_matches_chrono_when_available() {
408 let raw = [0xE4, 0x09, 0x12, 0x34, 0x56];
409 let plain = decode_mjd_bcd(raw).expect("decodes");
410 #[cfg(feature = "chrono")]
411 {
412 use chrono::{Datelike, Timelike};
413 let chrono_dt = decode_mjd_bcd_utc(raw).expect("decodes");
414 assert_eq!(plain.year as i32, chrono_dt.year());
415 assert_eq!(plain.month as u32, chrono_dt.month());
416 assert_eq!(plain.day as u32, chrono_dt.day());
417 assert_eq!(plain.hour as u32, chrono_dt.hour());
418 assert_eq!(plain.minute as u32, chrono_dt.minute());
419 assert_eq!(plain.second as u32, chrono_dt.second());
420 }
421 assert_eq!(plain.year, 2018);
423 assert_eq!(plain.month, 9);
424 assert_eq!(plain.day, 16);
425 }
426
427 #[cfg(feature = "chrono")]
428 #[test]
429 fn secs_2000_epoch_is_correct() {
430 use chrono::{Datelike, TimeZone, Timelike};
431 let dt = chrono::Utc.timestamp_opt(SECS_2000_EPOCH, 0).unwrap();
433 assert_eq!((dt.year(), dt.month(), dt.day()), (2000, 1, 1));
434 assert_eq!((dt.hour(), dt.minute(), dt.second()), (0, 0, 0));
435 }
436
437 #[cfg(feature = "chrono")]
438 #[test]
439 fn decode_seconds_since_2000_utc_known_value() {
440 use chrono::{Datelike, Timelike};
441 let dt = decode_seconds_since_2000_utc(0, 0, 0).expect("decodes");
443 assert_eq!((dt.year(), dt.month(), dt.day()), (2000, 1, 1));
444 assert_eq!((dt.hour(), dt.minute(), dt.second()), (0, 0, 0));
445 }
446
447 #[cfg(feature = "chrono")]
448 #[test]
449 fn encode_decode_seconds_since_2000_utc_round_trips() {
450 use chrono::TimeZone;
451 let dt = chrono::Utc
453 .with_ymd_and_hms(2023, 6, 8, 12, 34, 56)
454 .unwrap();
455 let (secs, nanos) = encode_seconds_since_2000_utc(dt, 37).expect("encodes");
456 let decoded = decode_seconds_since_2000_utc(secs, nanos, 37).expect("decodes");
457 assert_eq!(decoded, dt);
458 }
459}