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
246pub const SECS_2000_EPOCH: i64 = 946_684_800;
253
254#[cfg(feature = "chrono")]
267#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
268#[must_use]
269pub fn decode_seconds_since_2000_utc(
270 seconds_since_2000: u64,
271 subsec_nanos: u32,
272 utco: u16,
273) -> Option<chrono::DateTime<chrono::Utc>> {
274 use chrono::TimeZone;
275 let unix_secs = SECS_2000_EPOCH
277 .checked_add(i64::try_from(seconds_since_2000).ok()?)?
278 .checked_sub(i64::from(utco))?;
279 chrono::Utc.timestamp_opt(unix_secs, subsec_nanos).single()
280}
281
282#[cfg(feature = "chrono")]
288#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
289#[must_use]
290pub fn encode_seconds_since_2000_utc(
291 dt: chrono::DateTime<chrono::Utc>,
292 utco: u16,
293) -> Option<(u64, u32)> {
294 use chrono::Timelike;
295 let unix_secs = dt.timestamp();
296 let raw = unix_secs
298 .checked_sub(SECS_2000_EPOCH)?
299 .checked_add(i64::from(utco))?;
300 let secs = u64::try_from(raw).ok()?;
301 if secs > 0xFF_FFFF_FFFF {
303 return None;
304 }
305 Some((secs, dt.nanosecond()))
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn duration_round_trips() {
314 for &(h, m, s) in &[(0u64, 0u64, 0u64), (1, 30, 45), (99, 59, 59), (2, 0, 0)] {
315 let secs = h * 3600 + m * 60 + s;
316 let raw = encode_bcd_duration(Duration::from_secs(secs)).expect("encodes");
317 assert_eq!(decode_bcd_duration(raw), Some(Duration::from_secs(secs)));
318 }
319 }
320
321 #[test]
322 fn duration_decode_known_vector() {
323 assert_eq!(
325 decode_bcd_duration([0x01, 0x30, 0x45]),
326 Some(Duration::from_secs(5445))
327 );
328 }
329
330 #[test]
331 fn duration_rejects_over_99h_and_bad_fields() {
332 assert_eq!(encode_bcd_duration(Duration::from_secs(100 * 3600)), None);
333 assert_eq!(decode_bcd_duration([0x01, 0x75, 0x00]), None); assert_eq!(decode_bcd_duration([0x01, 0x00, 0x1A]), None); }
336
337 #[cfg(feature = "chrono")]
338 #[test]
339 fn ymd_to_mjd_matches_chrono_epoch_arithmetic() {
340 use chrono::NaiveDate;
341 let epoch = NaiveDate::from_ymd_opt(1858, 11, 17).unwrap();
343 for &(y, m, d) in &[(1993, 10, 13), (2000, 1, 1), (2023, 6, 8), (1900, 3, 1)] {
344 let date = NaiveDate::from_ymd_opt(y, m, d).unwrap();
345 let expected = (date - epoch).num_days() as u16;
346 assert_eq!(ymd_to_mjd(y, m, d), Some(expected), "{y}-{m}-{d}");
347 }
348 }
349
350 #[cfg(feature = "chrono")]
351 #[test]
352 fn mjd_ymd_round_trips() {
353 for mjd in [40_587u16, 49_273, 51_544, 59_945, 60_000] {
354 let (y, m, d) = mjd_to_ymd(mjd);
355 assert_eq!(ymd_to_mjd(y, m, d), Some(mjd), "mjd {mjd}");
356 }
357 }
358
359 #[cfg(feature = "chrono")]
360 #[test]
361 fn utc_round_trips() {
362 let raw = [0xE4, 0x09, 0x12, 0x34, 0x56];
363 let dt = decode_mjd_bcd_utc(raw).expect("decodes");
364 assert_eq!(encode_mjd_bcd_utc(dt), Some(raw));
365 }
366
367 #[cfg(feature = "chrono")]
368 #[test]
369 fn utc_decode_known_vector() {
370 use chrono::{Datelike, Timelike};
371 let dt = decode_mjd_bcd_utc([0xE4, 0x09, 0x12, 0x34, 0x56]).expect("decodes");
373 assert_eq!((dt.hour(), dt.minute(), dt.second()), (12, 34, 56));
374 assert_eq!(dt.year(), 2018);
375 }
376
377 #[test]
378 fn mjd_bcd_round_trips() {
379 for &(y, m, d, h, mi, s) in &[
380 (2023u16, 1u8, 1u8, 12u8, 34u8, 56u8),
381 (2000, 1, 1, 0, 0, 0),
382 (2023, 6, 8, 23, 59, 59),
383 ] {
384 let dt = MjdBcdDateTime {
385 year: y,
386 month: m,
387 day: d,
388 hour: h,
389 minute: mi,
390 second: s,
391 };
392 let raw = encode_mjd_bcd(dt).expect("encodes");
393 let re = decode_mjd_bcd(raw).expect("decodes");
394 assert_eq!(re, dt);
395 }
396 }
397
398 #[test]
399 fn mjd_bcd_rejects_invalid_bcd() {
400 assert_eq!(decode_mjd_bcd([0xE4, 0x09, 0x1A, 0x34, 0x56]), None);
401 assert_eq!(decode_mjd_bcd([0xE4, 0x09, 0x12, 0x75, 0x56]), None);
402 }
403
404 #[test]
405 fn mjd_bcd_matches_chrono_when_available() {
406 let raw = [0xE4, 0x09, 0x12, 0x34, 0x56];
407 let plain = decode_mjd_bcd(raw).expect("decodes");
408 #[cfg(feature = "chrono")]
409 {
410 use chrono::{Datelike, Timelike};
411 let chrono_dt = decode_mjd_bcd_utc(raw).expect("decodes");
412 assert_eq!(plain.year as i32, chrono_dt.year());
413 assert_eq!(plain.month as u32, chrono_dt.month());
414 assert_eq!(plain.day as u32, chrono_dt.day());
415 assert_eq!(plain.hour as u32, chrono_dt.hour());
416 assert_eq!(plain.minute as u32, chrono_dt.minute());
417 assert_eq!(plain.second as u32, chrono_dt.second());
418 }
419 assert_eq!(plain.year, 2018);
421 assert_eq!(plain.month, 9);
422 assert_eq!(plain.day, 16);
423 }
424
425 #[cfg(feature = "chrono")]
426 #[test]
427 fn secs_2000_epoch_is_correct() {
428 use chrono::{Datelike, TimeZone, Timelike};
429 let dt = chrono::Utc.timestamp_opt(SECS_2000_EPOCH, 0).unwrap();
431 assert_eq!((dt.year(), dt.month(), dt.day()), (2000, 1, 1));
432 assert_eq!((dt.hour(), dt.minute(), dt.second()), (0, 0, 0));
433 }
434
435 #[cfg(feature = "chrono")]
436 #[test]
437 fn decode_seconds_since_2000_utc_known_value() {
438 use chrono::{Datelike, Timelike};
439 let dt = decode_seconds_since_2000_utc(0, 0, 0).expect("decodes");
441 assert_eq!((dt.year(), dt.month(), dt.day()), (2000, 1, 1));
442 assert_eq!((dt.hour(), dt.minute(), dt.second()), (0, 0, 0));
443 }
444
445 #[cfg(feature = "chrono")]
446 #[test]
447 fn encode_decode_seconds_since_2000_utc_round_trips() {
448 use chrono::TimeZone;
449 let dt = chrono::Utc
451 .with_ymd_and_hms(2023, 6, 8, 12, 34, 56)
452 .unwrap();
453 let (secs, nanos) = encode_seconds_since_2000_utc(dt, 37).expect("encodes");
454 let decoded = decode_seconds_since_2000_utc(secs, nanos, 37).expect("decodes");
455 assert_eq!(decoded, dt);
456 }
457}