Add Store/Transit_DateTimeUtc wrapper type for 52-bit timestamp
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::types::{Stored_U26, Transit_U26, U26Error};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
/// Error type for DateTimeUtc wrapper types
|
||||
pub enum DateTimeUtcError {
|
||||
Overflow,
|
||||
InvalidTimestamp,
|
||||
ConversionError,
|
||||
}
|
||||
|
||||
impl From<U26Error> 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<Utc> 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<Utc>
|
||||
pub fn new(dt: DateTime<Utc>) -> Result<Self, DateTimeUtcError> {
|
||||
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<Utc> value
|
||||
pub fn into_datetime(self) -> Result<DateTime<Utc>, 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<Utc> value (borrowed)
|
||||
pub fn to_datetime(&self) -> Result<DateTime<Utc>, 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<Self, DateTimeUtcError> {
|
||||
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<Store_DateTimeUtc> for DateTime<Utc> {
|
||||
fn from(value: Store_DateTimeUtc) -> Self {
|
||||
value.into_datetime().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DateTime<Utc>> for Store_DateTimeUtc {
|
||||
type Error = DateTimeUtcError;
|
||||
|
||||
fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Store_DateTimeUtc> 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<Utc> 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<Utc>
|
||||
pub fn new(dt: DateTime<Utc>) -> Result<Self, DateTimeUtcError> {
|
||||
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<Utc> value
|
||||
pub fn into_datetime(self) -> Result<DateTime<Utc>, 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<Utc> value (borrowed)
|
||||
pub fn to_datetime(&self) -> Result<DateTime<Utc>, 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<Self, DateTimeUtcError> {
|
||||
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<Transit_DateTimeUtc> for DateTime<Utc> {
|
||||
fn from(value: Transit_DateTimeUtc) -> Self {
|
||||
value.into_datetime().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DateTime<Utc>> for Transit_DateTimeUtc {
|
||||
type Error = DateTimeUtcError;
|
||||
|
||||
fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Transit_DateTimeUtc> 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = transit.into();
|
||||
assert_eq!(retrieved, dt);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user