use crate::bcd::{from_bcd_byte, to_bcd_byte};
use core::time::Duration;
#[must_use]
pub fn decode_bcd_duration(raw: [u8; 3]) -> Option<Duration> {
let h = u64::from(from_bcd_byte(raw[0])?);
let m = u64::from(from_bcd_byte(raw[1])?);
let s = u64::from(from_bcd_byte(raw[2])?);
if m > 59 || s > 59 {
return None;
}
Some(Duration::from_secs(h * 3600 + m * 60 + s))
}
#[must_use]
pub fn encode_bcd_duration(duration: Duration) -> Option<[u8; 3]> {
let secs = duration.as_secs();
let h = secs / 3600;
if h > 99 {
return None;
}
let m = (secs % 3600) / 60;
let s = secs % 60;
Some([
to_bcd_byte(h as u8)?,
to_bcd_byte(m as u8)?,
to_bcd_byte(s as u8)?,
])
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
#[must_use]
pub fn mjd_to_ymd(mjd: u16) -> (i32, u32, u32) {
let mjd = i64::from(mjd);
let y_prime = ((mjd as f64 - 15_078.2) / 365.25) as i64;
let m_prime = ((mjd as f64 - 14_956.1 - (y_prime as f64 * 365.25).floor()) / 30.6001) as i64;
let d = mjd
- 14_956
- (y_prime as f64 * 365.25).floor() as i64
- (m_prime as f64 * 30.6001).floor() as i64;
let k = i64::from(m_prime == 14 || m_prime == 15);
let y = y_prime + k + 1900;
let m = m_prime - 1 - k * 12;
(y as i32, m as u32, d as u32)
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
#[must_use]
pub fn ymd_to_mjd(year: i32, month: u32, day: u32) -> Option<u16> {
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let l = if month <= 2 { 1.0 } else { 0.0 };
let y = f64::from(year - 1900);
let m = f64::from(month);
let mjd = 14_956.0
+ f64::from(day)
+ ((y - l) * 365.25).floor()
+ ((m + 1.0 + l * 12.0) * 30.6001).floor();
if (0.0..=f64::from(u16::MAX)).contains(&mjd) {
Some(mjd as u16)
} else {
None
}
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
#[must_use]
pub fn decode_mjd_bcd_utc(raw: [u8; 5]) -> Option<chrono::DateTime<chrono::Utc>> {
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
let mjd = u16::from_be_bytes([raw[0], raw[1]]);
let (y, m, d) = mjd_to_ymd(mjd);
let h = from_bcd_byte(raw[2])?;
let mi = from_bcd_byte(raw[3])?;
let s = from_bcd_byte(raw[4])?;
let date = NaiveDate::from_ymd_opt(y, m, d)?;
let time = NaiveTime::from_hms_opt(u32::from(h), u32::from(mi), u32::from(s))?;
chrono::Utc
.from_local_datetime(&NaiveDateTime::new(date, time))
.single()
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
#[must_use]
pub fn encode_mjd_bcd_utc(dt: chrono::DateTime<chrono::Utc>) -> Option<[u8; 5]> {
use chrono::{Datelike, Timelike};
let naive = dt.naive_utc();
let mjd = ymd_to_mjd(naive.year(), naive.month(), naive.day())?;
let [m0, m1] = mjd.to_be_bytes();
Some([
m0,
m1,
to_bcd_byte(naive.hour() as u8)?,
to_bcd_byte(naive.minute() as u8)?,
to_bcd_byte(naive.second() as u8)?,
])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn duration_round_trips() {
for &(h, m, s) in &[(0u64, 0u64, 0u64), (1, 30, 45), (99, 59, 59), (2, 0, 0)] {
let secs = h * 3600 + m * 60 + s;
let raw = encode_bcd_duration(Duration::from_secs(secs)).expect("encodes");
assert_eq!(decode_bcd_duration(raw), Some(Duration::from_secs(secs)));
}
}
#[test]
fn duration_decode_known_vector() {
assert_eq!(
decode_bcd_duration([0x01, 0x30, 0x45]),
Some(Duration::from_secs(5445))
);
}
#[test]
fn duration_rejects_over_99h_and_bad_fields() {
assert_eq!(encode_bcd_duration(Duration::from_secs(100 * 3600)), None);
assert_eq!(decode_bcd_duration([0x01, 0x75, 0x00]), None); assert_eq!(decode_bcd_duration([0x01, 0x00, 0x1A]), None); }
#[cfg(feature = "chrono")]
#[test]
fn ymd_to_mjd_matches_chrono_epoch_arithmetic() {
use chrono::NaiveDate;
let epoch = NaiveDate::from_ymd_opt(1858, 11, 17).unwrap();
for &(y, m, d) in &[(1993, 10, 13), (2000, 1, 1), (2023, 6, 8), (1900, 3, 1)] {
let date = NaiveDate::from_ymd_opt(y, m, d).unwrap();
let expected = (date - epoch).num_days() as u16;
assert_eq!(ymd_to_mjd(y, m, d), Some(expected), "{y}-{m}-{d}");
}
}
#[cfg(feature = "chrono")]
#[test]
fn mjd_ymd_round_trips() {
for mjd in [40_587u16, 49_273, 51_544, 59_945, 60_000] {
let (y, m, d) = mjd_to_ymd(mjd);
assert_eq!(ymd_to_mjd(y, m, d), Some(mjd), "mjd {mjd}");
}
}
#[cfg(feature = "chrono")]
#[test]
fn utc_round_trips() {
let raw = [0xE4, 0x09, 0x12, 0x34, 0x56];
let dt = decode_mjd_bcd_utc(raw).expect("decodes");
assert_eq!(encode_mjd_bcd_utc(dt), Some(raw));
}
#[cfg(feature = "chrono")]
#[test]
fn utc_decode_known_vector() {
use chrono::{Datelike, Timelike};
let dt = decode_mjd_bcd_utc([0xE4, 0x09, 0x12, 0x34, 0x56]).expect("decodes");
assert_eq!((dt.hour(), dt.minute(), dt.second()), (12, 34, 56));
assert_eq!(dt.year(), 2018);
}
}