use crate::error::{Error, Result};
pub const MJD_BCD_LEN: usize = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct MjdBcdTime([u8; MJD_BCD_LEN]);
impl MjdBcdTime {
#[must_use]
pub const fn from_raw(raw: [u8; MJD_BCD_LEN]) -> Self {
Self(raw)
}
#[must_use]
pub const fn raw(&self) -> [u8; MJD_BCD_LEN] {
self.0
}
#[must_use]
pub fn parts(&self) -> (u16, Option<u8>, Option<u8>, Option<u8>) {
(
u16::from_be_bytes([self.0[0], self.0[1]]),
broadcast_common::bcd::from_bcd_byte(self.0[2]),
broadcast_common::bcd::from_bcd_byte(self.0[3]),
broadcast_common::bcd::from_bcd_byte(self.0[4]),
)
}
#[cfg(feature = "chrono")]
#[must_use]
pub fn to_datetime(&self) -> Option<chrono::DateTime<chrono::Utc>> {
broadcast_common::time::decode_mjd_bcd_utc(self.0)
}
#[cfg(feature = "chrono")]
pub fn from_datetime(dt: chrono::DateTime<chrono::Utc>) -> Result<Self> {
broadcast_common::time::encode_mjd_bcd_utc(dt)
.map(Self::from_raw)
.ok_or(Error::ValueOutOfRange {
field: "MjdBcdTime",
reason: "date not representable in 16-bit MJD",
})
}
pub(crate) fn read(bytes: &[u8], off: usize, what: &'static str) -> Result<Self> {
match bytes
.get(off..)
.and_then(|s| s.first_chunk::<MJD_BCD_LEN>())
{
Some(b) => Ok(Self(*b)),
None => Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: off.saturating_add(MJD_BCD_LEN),
have: bytes.len(),
what,
},
)),
}
}
pub(crate) fn write(&self, buf: &mut [u8], off: usize) {
buf[off..off + MJD_BCD_LEN].copy_from_slice(&self.0);
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for MjdBcdTime {
fn serialize<S: serde::Serializer>(&self, s: S) -> core::result::Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let (mjd, hour, minute, second) = self.parts();
let mut st = s.serialize_struct("MjdBcdTime", 4)?;
st.serialize_field("mjd", &mjd)?;
st.serialize_field("hour", &hour)?;
st.serialize_field("minute", &minute)?;
st.serialize_field("second", &second)?;
st.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parts_decode_and_raw_round_trip() {
let t = MjdBcdTime::from_raw([0xC0, 0x79, 0x12, 0x45, 0x00]);
assert_eq!(t.parts(), (0xC079, Some(12), Some(45), Some(0)));
assert_eq!(MjdBcdTime::from_raw(t.raw()), t);
}
#[test]
fn non_decimal_bcd_is_kept_not_rejected() {
let t = MjdBcdTime::from_raw([0xC0, 0x79, 0xFF, 0x45, 0x00]);
assert_eq!(t.parts().1, None);
assert_eq!(t.raw()[2], 0xFF);
}
#[test]
fn read_rejects_short_buffer() {
assert!(matches!(
MjdBcdTime::read(&[0; 4], 0, "t"),
Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: 5,
have: 4,
what: "t"
}
))
));
}
#[cfg(feature = "chrono")]
#[test]
fn from_datetime_encodes_a_known_vector_and_inverts_to_datetime() {
use chrono::TimeZone;
let dt = chrono::Utc
.with_ymd_and_hms(2023, 1, 1, 12, 34, 56)
.unwrap();
let t = MjdBcdTime::from_datetime(dt).unwrap();
assert_eq!(t.raw(), [0xEA, 0x29, 0x12, 0x34, 0x56]);
assert_eq!(t.to_datetime(), Some(dt));
let far_future = chrono::Utc.with_ymd_and_hms(9999, 12, 31, 0, 0, 0).unwrap();
assert!(matches!(
MjdBcdTime::from_datetime(far_future),
Err(Error::ValueOutOfRange { .. })
));
}
}