use chrono::{DateTime, Utc}; use thiserror::Error; use crate::types::{Stored_U26, Transit_U26, U26Error}; #[derive(Debug, PartialEq, Error)] /// Error type for DateTimeUtc wrapper types pub enum DateTimeUtcError { #[error("timestamp overflow")] Overflow, #[error("timestamp invalid")] InvalidTimestamp, #[error("timestamp conversion error")] ConversionError, } impl From for DateTimeUtcError { fn from(error: U26Error) -> Self { match error { U26Error::Overflow => DateTimeUtcError::Overflow, U26Error::InvalidCodeWord => DateTimeUtcError::InvalidTimestamp, U26Error::IncorrectibleError => DateTimeUtcError::InvalidTimestamp, U26Error::ChecksumError => DateTimeUtcError::InvalidTimestamp, } } } #[derive(Debug, PartialEq)] #[allow(non_camel_case_types)] /// Wrapper type for storing DateTime with error correction /// Uses two Stored_U26 values internally to store 52 bits of milliseconds since Unix epoch pub struct Store_DateTimeUtc(Stored_U26, Stored_U26); impl Store_DateTimeUtc { /// Maximum timestamp value that can be stored (in milliseconds) const MAX_TIMESTAMP_MS: u64 = (1u64 << 52) - 1; /// Create a new Store_DateTimeUtc from a DateTime pub fn new(dt: DateTime) -> Result { let timestamp_ms = dt.timestamp_millis(); if timestamp_ms < 0 || timestamp_ms > Self::MAX_TIMESTAMP_MS as i64 { return Err(DateTimeUtcError::Overflow); } let timestamp_u64 = timestamp_ms as u64; let low_26 = (timestamp_u64 & 0x03FFFFFF) as u32; let high_26 = ((timestamp_u64 >> 26) & 0x03FFFFFF) as u32; let stored_low = Stored_U26::try_from(low_26)?; let stored_high = Stored_U26::try_from(high_26)?; Ok(Self(stored_low, stored_high)) } /// Get the DateTime value pub fn into_datetime(self) -> Result, DateTimeUtcError> { let low_26: u32 = self.0.into(); let high_26: u32 = self.1.into(); let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); DateTime::from_timestamp_millis(timestamp_u64 as i64) .ok_or(DateTimeUtcError::ConversionError) } /// Get the DateTime value (borrowed) pub fn to_datetime(&self) -> Result, DateTimeUtcError> { let low_26: u32 = self.0.clone().into(); let high_26: u32 = self.1.clone().into(); let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); DateTime::from_timestamp_millis(timestamp_u64 as i64) .ok_or(DateTimeUtcError::ConversionError) } /// Serialize to big-endian bytes with CRC values /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], high_crc, low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3], low_crc] pub fn to_be_bytes(&self) -> [u8; 10] { let mut bytes = [0u8; 10]; let high_bytes = self.1.to_be_bytes(); let high_crc = self.1.calc_crc6(); let low_bytes = self.0.to_be_bytes(); let low_crc = self.0.calc_crc6(); bytes[0] = high_bytes[0]; bytes[1] = high_bytes[1]; bytes[2] = high_bytes[2]; bytes[3] = high_bytes[3]; bytes[4] = high_crc; bytes[5] = low_bytes[0]; bytes[6] = low_bytes[1]; bytes[7] = low_bytes[2]; bytes[8] = low_bytes[3]; bytes[9] = low_crc; bytes } /// Deserialize from big-endian bytes with CRC check /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], high_crc, low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3], low_crc] pub fn from_be_bytes(bytes: [u8; 10]) -> Result { let high_bytes = [bytes[0], bytes[1], bytes[2], bytes[3]]; let high_crc = bytes[4]; let low_bytes = [bytes[5], bytes[6], bytes[7], bytes[8]]; let low_crc = bytes[9]; let stored_high = Stored_U26::from_be_bytes(high_bytes, high_crc)?; let stored_low = Stored_U26::from_be_bytes(low_bytes, low_crc)?; Ok(Self(stored_low, stored_high)) } } impl From for DateTime { fn from(value: Store_DateTimeUtc) -> Self { value.into_datetime().unwrap() } } impl TryFrom> for Store_DateTimeUtc { type Error = DateTimeUtcError; fn try_from(value: DateTime) -> Result { Self::new(value) } } impl From for u64 { fn from(value: Store_DateTimeUtc) -> Self { let low_26: u32 = value.0.into(); let high_26: u32 = value.1.into(); ((high_26 as u64) << 26) | (low_26 as u64) } } #[derive(Debug, PartialEq)] #[allow(non_camel_case_types)] /// Wrapper type for transmitting DateTime with CRC error detection /// Uses two Transit_U26 values internally to store 52 bits of milliseconds since Unix epoch pub struct Transit_DateTimeUtc(Transit_U26, Transit_U26); impl Transit_DateTimeUtc { /// Maximum timestamp value that can be transmitted (in milliseconds) const MAX_TIMESTAMP_MS: u64 = (1u64 << 52) - 1; /// Create a new Transit_DateTimeUtc from a DateTime pub fn new(dt: DateTime) -> Result { let timestamp_ms = dt.timestamp_millis(); if timestamp_ms < 0 || timestamp_ms > Self::MAX_TIMESTAMP_MS as i64 { return Err(DateTimeUtcError::Overflow); } let timestamp_u64 = timestamp_ms as u64; let low_26 = (timestamp_u64 & 0x03FFFFFF) as u32; let high_26 = ((timestamp_u64 >> 26) & 0x03FFFFFF) as u32; let transit_low = Transit_U26::try_from(low_26)?; let transit_high = Transit_U26::try_from(high_26)?; Ok(Self(transit_low, transit_high)) } /// Get the DateTime value pub fn into_datetime(self) -> Result, DateTimeUtcError> { let low_26: u32 = self.0.into(); let high_26: u32 = self.1.into(); let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); DateTime::from_timestamp_millis(timestamp_u64 as i64) .ok_or(DateTimeUtcError::ConversionError) } /// Get the DateTime value (borrowed) pub fn to_datetime(&self) -> Result, DateTimeUtcError> { let low_26: u32 = self.0.clone().into(); let high_26: u32 = self.1.clone().into(); let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); DateTime::from_timestamp_millis(timestamp_u64 as i64) .ok_or(DateTimeUtcError::ConversionError) } /// Serialize to big-endian bytes /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3]] pub fn to_be_bytes(&self) -> [u8; 8] { let mut bytes = [0u8; 8]; let high_bytes = self.1.to_be_bytes(); let low_bytes = self.0.to_be_bytes(); bytes[0] = high_bytes[0]; bytes[1] = high_bytes[1]; bytes[2] = high_bytes[2]; bytes[3] = high_bytes[3]; bytes[4] = low_bytes[0]; bytes[5] = low_bytes[1]; bytes[6] = low_bytes[2]; bytes[7] = low_bytes[3]; bytes } /// Deserialize from big-endian bytes with CRC check /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3]] pub fn from_be_bytes(bytes: [u8; 8]) -> Result { let high_bytes = [bytes[0], bytes[1], bytes[2], bytes[3]]; let low_bytes = [bytes[4], bytes[5], bytes[6], bytes[7]]; let transit_high = Transit_U26::from_be_bytes(high_bytes)?; let transit_low = Transit_U26::from_be_bytes(low_bytes)?; Ok(Self(transit_low, transit_high)) } } impl From for DateTime { fn from(value: Transit_DateTimeUtc) -> Self { value.into_datetime().unwrap() } } impl TryFrom> for Transit_DateTimeUtc { type Error = DateTimeUtcError; fn try_from(value: DateTime) -> Result { Self::new(value) } } impl From for u64 { fn from(value: Transit_DateTimeUtc) -> Self { let low_26: u32 = value.0.into(); let high_26: u32 = value.1.into(); ((high_26 as u64) << 26) | (low_26 as u64) } } #[cfg(test)] mod timestamp_test { use super::*; use chrono::{DateTime, TimeZone, Utc}; #[test] fn store_datetimeutc_basic_operations() { // Test with Unix epoch let epoch = Utc.timestamp_millis_opt(0).unwrap(); let stored = Store_DateTimeUtc::new(epoch).unwrap(); let retrieved: DateTime = stored.into(); assert_eq!(retrieved, epoch); // Test with a specific date (within 52-bit range) let dt = Utc.with_ymd_and_hms(2020, 1, 15, 12, 30, 45).unwrap(); let stored = Store_DateTimeUtc::new(dt).unwrap(); let retrieved: DateTime = stored.into(); assert_eq!(retrieved, dt); } #[test] fn store_datetimeutc_serialization() { let dt = Utc.with_ymd_and_hms(2020, 5, 20, 8, 15, 30).unwrap(); let stored = Store_DateTimeUtc::new(dt).unwrap(); let bytes = stored.to_be_bytes(); // Deserialize and verify let deserialized = Store_DateTimeUtc::from_be_bytes(bytes).unwrap(); let retrieved: DateTime = deserialized.into(); assert_eq!(retrieved, dt); } #[test] fn store_datetimeutc_error_correction() { let dt = Utc.with_ymd_and_hms(2020, 3, 10, 14, 25, 10).unwrap(); let stored = Store_DateTimeUtc::new(dt).unwrap(); let mut bytes = stored.to_be_bytes(); // Introduce a single bit error in low bytes that can be corrected bytes[7] ^= 0x01; let deserialized = Store_DateTimeUtc::from_be_bytes(bytes).unwrap(); let retrieved: DateTime = deserialized.into(); assert_eq!(retrieved, dt); } #[test] fn store_datetimeutc_overflow() { // Test with timestamp beyond max value let max_timestamp = Store_DateTimeUtc::MAX_TIMESTAMP_MS; let dt = Utc.timestamp_millis_opt(max_timestamp as i64 + 1).unwrap(); let result = Store_DateTimeUtc::new(dt); assert!(matches!(result, Err(DateTimeUtcError::Overflow))); } #[test] fn store_datetimeutc_borrowed_method() { let dt = Utc.with_ymd_and_hms(2020, 7, 22, 18, 45, 20).unwrap(); let stored = Store_DateTimeUtc::new(dt).unwrap(); // Test the borrowed to_datetime method let retrieved = stored.to_datetime().unwrap(); assert_eq!(retrieved, dt); } #[test] fn store_datetimeutc_try_from() { let dt = Utc.with_ymd_and_hms(2020, 9, 15, 10, 10, 10).unwrap(); let stored: Store_DateTimeUtc = dt.try_into().unwrap(); let retrieved: DateTime = stored.into(); assert_eq!(retrieved, dt); } #[test] fn store_datetimeutc_crc_error_detection() { let dt = Utc.with_ymd_and_hms(2020, 12, 25, 0, 0, 0).unwrap(); let stored = Store_DateTimeUtc::new(dt).unwrap(); let mut bytes = stored.to_be_bytes(); // Introduce multiple bit errors in low bytes that can't be corrected bytes[5] ^= 0x01; bytes[6] ^= 0x01; let result = Store_DateTimeUtc::from_be_bytes(bytes); assert!(result.is_err()); } #[test] fn transit_datetimeutc_basic_operations() { // Test with Unix epoch let epoch = Utc.timestamp_millis_opt(0).unwrap(); let transit = Transit_DateTimeUtc::new(epoch).unwrap(); let retrieved: DateTime = transit.into(); assert_eq!(retrieved, epoch); // Test with a specific date (within 52-bit range) let dt = Utc.with_ymd_and_hms(2020, 1, 15, 12, 30, 45).unwrap(); let transit = Transit_DateTimeUtc::new(dt).unwrap(); let retrieved: DateTime = transit.into(); assert_eq!(retrieved, dt); } #[test] fn transit_datetimeutc_serialization() { let dt = Utc.with_ymd_and_hms(2020, 5, 20, 8, 15, 30).unwrap(); let transit = Transit_DateTimeUtc::new(dt).unwrap(); let bytes = transit.to_be_bytes(); // Deserialize and verify let deserialized = Transit_DateTimeUtc::from_be_bytes(bytes).unwrap(); let retrieved: DateTime = deserialized.into(); assert_eq!(retrieved, dt); } #[test] fn transit_datetimeutc_crc_error_detection() { let dt = Utc.with_ymd_and_hms(2020, 12, 25, 0, 0, 0).unwrap(); let transit = Transit_DateTimeUtc::new(dt).unwrap(); let mut bytes = transit.to_be_bytes(); // Introduce bit errors in low bytes that will cause CRC mismatch bytes[4] ^= 0x01; bytes[5] ^= 0x01; let result = Transit_DateTimeUtc::from_be_bytes(bytes); assert!(result.is_err()); } #[test] fn transit_datetimeutc_overflow() { // Test with timestamp beyond max value let max_timestamp = Transit_DateTimeUtc::MAX_TIMESTAMP_MS; let dt = Utc.timestamp_millis_opt(max_timestamp as i64 + 1).unwrap(); let result = Transit_DateTimeUtc::new(dt); assert!(matches!(result, Err(DateTimeUtcError::Overflow))); } #[test] fn transit_datetimeutc_borrowed_method() { let dt = Utc.with_ymd_and_hms(2020, 7, 22, 18, 45, 20).unwrap(); let transit = Transit_DateTimeUtc::new(dt).unwrap(); // Test the borrowed to_datetime method let retrieved = transit.to_datetime().unwrap(); assert_eq!(retrieved, dt); } #[test] fn transit_datetimeutc_try_from() { let dt = Utc.with_ymd_and_hms(2020, 9, 15, 10, 10, 10).unwrap(); let transit: Transit_DateTimeUtc = dt.try_into().unwrap(); let retrieved: DateTime = transit.into(); assert_eq!(retrieved, dt); } }