1use crate::bcd::{from_bcd_byte, to_bcd_byte};
9use core::time::Duration;
10
11#[must_use]
16pub fn decode_bcd_duration(raw: [u8; 3]) -> Option<Duration> {
17 let h = u64::from(from_bcd_byte(raw[0])?);
18 let m = u64::from(from_bcd_byte(raw[1])?);
19 let s = u64::from(from_bcd_byte(raw[2])?);
20 if m > 59 || s > 59 {
21 return None;
22 }
23 Some(Duration::from_secs(h * 3600 + m * 60 + s))
24}
25
26#[must_use]
31pub fn encode_bcd_duration(duration: Duration) -> Option<[u8; 3]> {
32 let secs = duration.as_secs();
33 let h = secs / 3600;
34 if h > 99 {
35 return None;
36 }
37 let m = (secs % 3600) / 60;
38 let s = secs % 60;
39 Some([
40 to_bcd_byte(h as u8)?,
41 to_bcd_byte(m as u8)?,
42 to_bcd_byte(s as u8)?,
43 ])
44}
45
46#[cfg(feature = "chrono")]
50#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
51#[must_use]
52pub fn mjd_to_ymd(mjd: u16) -> (i32, u32, u32) {
53 let mjd = i64::from(mjd);
54 let y_prime = ((mjd as f64 - 15_078.2) / 365.25) as i64;
55 let m_prime = ((mjd as f64 - 14_956.1 - (y_prime as f64 * 365.25).floor()) / 30.6001) as i64;
56 let d = mjd
57 - 14_956
58 - (y_prime as f64 * 365.25).floor() as i64
59 - (m_prime as f64 * 30.6001).floor() as i64;
60 let k = i64::from(m_prime == 14 || m_prime == 15);
61 let y = y_prime + k + 1900;
62 let m = m_prime - 1 - k * 12;
63 (y as i32, m as u32, d as u32)
64}
65
66#[cfg(feature = "chrono")]
72#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
73#[must_use]
74pub fn ymd_to_mjd(year: i32, month: u32, day: u32) -> Option<u16> {
75 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
76 return None;
77 }
78 let l = if month <= 2 { 1.0 } else { 0.0 };
79 let y = f64::from(year - 1900);
80 let m = f64::from(month);
81 let mjd = 14_956.0
82 + f64::from(day)
83 + ((y - l) * 365.25).floor()
84 + ((m + 1.0 + l * 12.0) * 30.6001).floor();
85 if (0.0..=f64::from(u16::MAX)).contains(&mjd) {
86 Some(mjd as u16)
87 } else {
88 None
89 }
90}
91
92#[cfg(feature = "chrono")]
98#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
99#[must_use]
100pub fn decode_mjd_bcd_utc(raw: [u8; 5]) -> Option<chrono::DateTime<chrono::Utc>> {
101 use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
102 let mjd = u16::from_be_bytes([raw[0], raw[1]]);
103 let (y, m, d) = mjd_to_ymd(mjd);
104 let h = from_bcd_byte(raw[2])?;
105 let mi = from_bcd_byte(raw[3])?;
106 let s = from_bcd_byte(raw[4])?;
107 let date = NaiveDate::from_ymd_opt(y, m, d)?;
108 let time = NaiveTime::from_hms_opt(u32::from(h), u32::from(mi), u32::from(s))?;
109 chrono::Utc
110 .from_local_datetime(&NaiveDateTime::new(date, time))
111 .single()
112}
113
114#[cfg(feature = "chrono")]
120#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
121#[must_use]
122pub fn encode_mjd_bcd_utc(dt: chrono::DateTime<chrono::Utc>) -> Option<[u8; 5]> {
123 use chrono::{Datelike, Timelike};
124 let naive = dt.naive_utc();
125 let mjd = ymd_to_mjd(naive.year(), naive.month(), naive.day())?;
126 let [m0, m1] = mjd.to_be_bytes();
127 Some([
128 m0,
129 m1,
130 to_bcd_byte(naive.hour() as u8)?,
131 to_bcd_byte(naive.minute() as u8)?,
132 to_bcd_byte(naive.second() as u8)?,
133 ])
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn duration_round_trips() {
142 for &(h, m, s) in &[(0u64, 0u64, 0u64), (1, 30, 45), (99, 59, 59), (2, 0, 0)] {
143 let secs = h * 3600 + m * 60 + s;
144 let raw = encode_bcd_duration(Duration::from_secs(secs)).expect("encodes");
145 assert_eq!(decode_bcd_duration(raw), Some(Duration::from_secs(secs)));
146 }
147 }
148
149 #[test]
150 fn duration_decode_known_vector() {
151 assert_eq!(
153 decode_bcd_duration([0x01, 0x30, 0x45]),
154 Some(Duration::from_secs(5445))
155 );
156 }
157
158 #[test]
159 fn duration_rejects_over_99h_and_bad_fields() {
160 assert_eq!(encode_bcd_duration(Duration::from_secs(100 * 3600)), None);
161 assert_eq!(decode_bcd_duration([0x01, 0x75, 0x00]), None); assert_eq!(decode_bcd_duration([0x01, 0x00, 0x1A]), None); }
164
165 #[cfg(feature = "chrono")]
166 #[test]
167 fn ymd_to_mjd_matches_chrono_epoch_arithmetic() {
168 use chrono::NaiveDate;
169 let epoch = NaiveDate::from_ymd_opt(1858, 11, 17).unwrap();
171 for &(y, m, d) in &[(1993, 10, 13), (2000, 1, 1), (2023, 6, 8), (1900, 3, 1)] {
172 let date = NaiveDate::from_ymd_opt(y, m, d).unwrap();
173 let expected = (date - epoch).num_days() as u16;
174 assert_eq!(ymd_to_mjd(y, m, d), Some(expected), "{y}-{m}-{d}");
175 }
176 }
177
178 #[cfg(feature = "chrono")]
179 #[test]
180 fn mjd_ymd_round_trips() {
181 for mjd in [40_587u16, 49_273, 51_544, 59_945, 60_000] {
182 let (y, m, d) = mjd_to_ymd(mjd);
183 assert_eq!(ymd_to_mjd(y, m, d), Some(mjd), "mjd {mjd}");
184 }
185 }
186
187 #[cfg(feature = "chrono")]
188 #[test]
189 fn utc_round_trips() {
190 let raw = [0xE4, 0x09, 0x12, 0x34, 0x56];
191 let dt = decode_mjd_bcd_utc(raw).expect("decodes");
192 assert_eq!(encode_mjd_bcd_utc(dt), Some(raw));
193 }
194
195 #[cfg(feature = "chrono")]
196 #[test]
197 fn utc_decode_known_vector() {
198 use chrono::{Datelike, Timelike};
199 let dt = decode_mjd_bcd_utc([0xE4, 0x09, 0x12, 0x34, 0x56]).expect("decodes");
201 assert_eq!((dt.hour(), dt.minute(), dt.second()), (12, 34, 56));
202 assert_eq!(dt.year(), 2018);
203 }
204}