use chrono::{DateTime, TimeDelta, Utc};
use nom::Parser;
use crate::daemon::time::Instant as ClockBoundInstant;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
#[repr(transparent)]
pub struct Timestamp {
inner: u64,
}
impl Timestamp {
pub const MAX: Self = Self { inner: u64::MAX };
pub const ZERO: Self = Self { inner: 0 };
pub const EPSILON: f64 = 1.0f64 / (1u64 << 32) as f64;
pub const fn max_seconds_since_ntp_epoch() -> f64 {
let td = Self::MAX.to_time_delta_since_ntp_epoch();
let secs = td.num_seconds() as f64;
let subsecs = td.subsec_nanos() as f64 / 1e9f64;
secs + subsecs
}
pub const fn get(self) -> u64 {
self.inner
}
const NTP_UNIX_EPOCH_DIFFERENCE_CHRONO: chrono::TimeDelta =
chrono::TimeDelta::new(2_208_988_800, 0).unwrap();
pub const fn new(val: u64) -> Self {
Self { inner: val }
}
pub const fn to_time_delta_since_ntp_epoch(self) -> TimeDelta {
const FRACTION_TO_NANO: f64 = 1e9f64 / (1u64 << 32) as f64;
let seconds = self.inner >> 32;
let fraction = self.inner & 0xFFFF_FFFF;
let nanoseconds = fraction as f64 * FRACTION_TO_NANO;
let nanoseconds = nanoseconds as u32;
TimeDelta::new(seconds as i64, nanoseconds).unwrap()
}
pub fn to_time_delta_since_unix_epoch(self) -> TimeDelta {
let ntp_time = self.to_time_delta_since_ntp_epoch();
ntp_time - Self::NTP_UNIX_EPOCH_DIFFERENCE_CHRONO
}
pub fn to_utc_datetime(self) -> DateTime<Utc> {
let secs = self.to_time_delta_since_unix_epoch();
DateTime::<Utc>::from_timestamp(secs.num_seconds(), secs.subsec_nanos().unsigned_abs())
.unwrap()
}
pub fn from_utc_system(val: chrono::DateTime<Utc>) -> Option<Self> {
let seconds = val.timestamp() + Self::NTP_UNIX_EPOCH_DIFFERENCE_CHRONO.num_seconds();
let nanoseconds = f64::from(val.timestamp_subsec_nanos()) / 1e9;
let seconds = seconds as f64 + nanoseconds;
Self::from_secs_since_ntp_epoch(seconds)
}
pub fn from_secs_since_ntp_epoch(secs: f64) -> Option<Self> {
(0.0..=Self::max_seconds_since_ntp_epoch())
.contains(&secs)
.then(|| {
let inner = (secs * (1u64 << 32) as f64).ceil() as u64;
Self { inner }
})
}
pub fn into_network_endian(self) -> u64 {
self.inner.to_be()
}
pub fn parse(input: &[u8]) -> nom::IResult<&[u8], Self> {
nom::number::be_u64()
.parse(input)
.map(|(input, val)| (input, Self::new(val)))
}
#[cfg(test)]
pub fn assert_within_error_bound(self, expected: DateTime<Utc>) {
let error = self.to_utc_datetime() - expected;
let error = error.abs().to_std().unwrap().as_secs_f64();
if error > Self::EPSILON {
panic!(
"error when comparing. expected: {expected}, actual: {0}",
self.to_utc_datetime()
);
}
}
}
impl From<Timestamp> for DateTime<Utc> {
fn from(value: Timestamp) -> Self {
value.to_utc_datetime()
}
}
impl From<Timestamp> for ClockBoundInstant {
fn from(value: Timestamp) -> Self {
let dt = value.to_utc_datetime();
Self::from_time(dt.timestamp(), dt.timestamp_subsec_nanos())
}
}
#[cfg(test)]
mod test {
use super::*;
use hex_literal::hex;
#[test]
fn negative_is_none() {
let value = Timestamp::from_secs_since_ntp_epoch(-1.0);
assert_eq!(value, None);
}
#[test]
fn above_max_errors() {
let time_stamp = DateTime::<Utc>::from_timestamp(u32::MAX as i64 + 1, 0).unwrap();
let value = Timestamp::from_utc_system(time_stamp);
assert_eq!(value, None);
}
#[test]
fn seconds() {
let secs = 15;
let value = Timestamp::from_secs_since_ntp_epoch(secs as f64).unwrap();
assert_eq!(value.inner, secs << 32);
}
#[rstest::rstest]
#[case::minimal(1.0 / (1u64 << 33) as f64, 1)]
#[case::maximal(1.0 - 1.0 / ( 1u64 << 33) as f64, 1 << 32)]
fn fraction(#[case] secs: f64, #[case] expected_inner: u64) {
let value = Timestamp::from_secs_since_ntp_epoch(secs).unwrap();
assert_eq!(value.inner, expected_inner);
}
#[test]
fn to_secs() {
let ntp_value = Timestamp { inner: 3 << 32 };
assert_eq!(ntp_value.to_time_delta_since_ntp_epoch().num_seconds(), 3);
}
const TIMESTAMP: &[u8] = &hex!("eb4529fe000046f3");
const SECONDS: u64 = 1738189694;
const NANOSECONDS: i32 = 4228;
#[test]
fn parse_from_packet() {
let (leftover, parsed) = Timestamp::parse(TIMESTAMP).unwrap();
assert!(leftover.is_empty());
let secs = parsed.to_time_delta_since_unix_epoch();
assert_eq!(secs.num_seconds() as u64, SECONDS);
let error = secs.subsec_nanos() - NANOSECONDS;
assert!(error.abs() < 1, "{error} {secs}")
}
#[test]
fn from_clock_bound_instant() {
let (_, timestamp) = Timestamp::parse(TIMESTAMP).unwrap();
let datetime = timestamp.to_utc_datetime();
let instant = ClockBoundInstant::from(timestamp);
assert_eq!(instant.as_seconds() as i64, datetime.timestamp());
}
}