use crate::error::NsfError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Timedate {
pub innards0: u32,
pub innards1: u32,
}
impl Timedate {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, NsfError> {
if bytes.len() < 8 {
return Err(NsfError::TooShort {
actual: bytes.len(),
required: 8,
});
}
let innards0 = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let innards1 = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
Ok(Self { innards0, innards1 })
}
pub fn as_clock(&self) -> Option<DecodedTimedate> {
let raw_julian = self.innards1 & 0x00FF_FFFF;
if !(2_400_000..=2_550_000).contains(&raw_julian) {
return None;
}
let centiseconds = self.innards0;
if centiseconds >= 8_640_000 {
return None;
}
let dst = (self.innards1 & 0x8000_0000) != 0;
let east = (self.innards1 & 0x4000_0000) != 0;
let quarter_hours = ((self.innards1 >> 28) & 0x3) as i32;
let hours = ((self.innards1 >> 24) & 0xF) as i32;
let offset_minutes_abs = hours * 60 + quarter_hours * 15;
let tz_offset_minutes = if east { offset_minutes_abs } else { -offset_minutes_abs };
let days_since_unix_epoch = (raw_julian as i64) - 2_440_588;
let seconds_into_day = (centiseconds / 100) as i64;
let unix_seconds_utc = days_since_unix_epoch * 86_400 + seconds_into_day;
let centi_remainder = (centiseconds % 100) as u32;
Some(DecodedTimedate {
unix_seconds_utc,
centiseconds: centi_remainder,
tz_offset_minutes,
dst,
julian_day_number: raw_julian,
})
}
pub fn as_hex_id(&self) -> String {
let b0 = self.innards0.to_le_bytes();
let b1 = self.innards1.to_le_bytes();
format!(
"{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
b0[0], b0[1], b0[2], b0[3], b1[0], b1[1], b1[2], b1[3]
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DecodedTimedate {
pub unix_seconds_utc: i64,
pub centiseconds: u32,
pub tz_offset_minutes: i32,
pub dst: bool,
pub julian_day_number: u32,
}
impl DecodedTimedate {
pub fn to_iso_8601(&self) -> String {
let local_seconds = self.unix_seconds_utc + (self.tz_offset_minutes as i64) * 60;
let (year, month, day) = civil_from_unix_day(local_seconds.div_euclid(86_400));
let day_seconds = local_seconds.rem_euclid(86_400) as u32;
let hour = day_seconds / 3600;
let minute = (day_seconds % 3600) / 60;
let second = day_seconds % 60;
let tz_sign = if self.tz_offset_minutes >= 0 { '+' } else { '-' };
let tz_abs = self.tz_offset_minutes.unsigned_abs();
let tz_h = tz_abs / 60;
let tz_m = tz_abs % 60;
format!(
"{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{:02}{tz_sign}{tz_h:02}:{tz_m:02}",
self.centiseconds
)
}
}
fn civil_from_unix_day(z: i64) -> (i32, u32, u32) {
let z = z + 719_468; let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u32;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = (yoe as i32) + (era as i32) * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_canonical_lotus_example_bytes() {
let bytes = [0xC0, 0xDC, 0x6C, 0x00, 0xFC, 0x63, 0x25, 0x85];
let td = Timedate::from_bytes(&bytes).unwrap();
assert_eq!(td.innards0, 0x006C_DCC0);
assert_eq!(td.innards1, 0x8525_63FC);
let clock = td.as_clock().unwrap();
assert!(clock.dst, "DST flag should be set");
assert!(!matches!(clock.tz_offset_minutes, 0), "offset is non-zero");
assert_eq!(clock.tz_offset_minutes, -300);
assert_eq!(clock.julian_day_number, 2_450_428);
}
#[test]
fn round_trips_unix_epoch_day() {
let mut innards1 = 2_440_588u32;
innards1 |= 0x4000_0000; let td = Timedate {
innards0: 0,
innards1,
};
let clock = td.as_clock().unwrap();
assert_eq!(clock.unix_seconds_utc, 0);
let iso = clock.to_iso_8601();
assert!(iso.starts_with("1970-01-01"), "got {iso}");
}
#[test]
fn renders_iso_8601() {
let bytes = [0xC0, 0xDC, 0x6C, 0x00, 0xFC, 0x63, 0x25, 0x85];
let td = Timedate::from_bytes(&bytes).unwrap();
let clock = td.as_clock().unwrap();
let iso = clock.to_iso_8601();
assert!(iso.starts_with("1996-12-10T14:49:04"), "got {iso}");
assert!(iso.ends_with("-05:00"), "got {iso}");
}
#[test]
fn rejects_short_buffer() {
let bytes = [0x00; 4];
let err = Timedate::from_bytes(&bytes).unwrap_err();
assert!(matches!(err, NsfError::TooShort { .. }));
}
#[test]
fn identifier_uses_render_as_hex() {
let td = Timedate {
innards0: 0xDEAD_BEEF,
innards1: 0xCAFE_BABE,
};
assert_eq!(td.as_hex_id(), "EFBEADDEBEBAFECA");
}
#[test]
fn implausible_jdn_returns_none_for_clock() {
let td = Timedate {
innards0: 0,
innards1: 0, };
assert!(td.as_clock().is_none());
assert_eq!(td.as_hex_id(), "0000000000000000");
}
}