From ef420f5f4d2e3bd90a2d8d0c4bf62b1ec29a4217 Mon Sep 17 00:00:00 2001 From: Julien Enoch Date: Mon, 1 Jul 2024 18:01:05 +0200 Subject: [PATCH] Add explicit conversion of NTP64 and Timestamp to/from RFC3339 String format --- src/ntp64.rs | 95 +++++++++++++++++++++++++++++++++++++++++++----- src/timestamp.rs | 68 +++++++++++++++++++++++++++++----- 2 files changed, 144 insertions(+), 19 deletions(-) diff --git a/src/ntp64.rs b/src/ntp64.rs index 32ce115..a2c4e4a 100644 --- a/src/ntp64.rs +++ b/src/ntp64.rs @@ -17,6 +17,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "std")] use { core::str::FromStr, + humantime::format_rfc3339_nanos, std::time::{SystemTime, UNIX_EPOCH}, }; @@ -47,11 +48,24 @@ const NANO_PER_SEC: u64 = 1_000_000_000; /// and the 2nd 32-bits part is the fraction of second. /// In case it's part of a [`crate::Timestamp`] generated by an [`crate::HLC`] the last few bits /// of the Fraction part are replaced by the HLC logical counter. -/// The size of this counter currently hard-coded as [`crate::CSIZE`]. +/// The size of this counter is currently hard-coded as [`crate::CSIZE`]. /// -/// Note that this timestamp in actually similar to a [`std::time::Duration`], as it doesn't -/// define an EPOCH. Only the [`NTP64::to_system_time()`] and [`std::fmt::Display::fmt()`] operations assume that -/// it's relative to UNIX_EPOCH (1st Jan 1970) to display the timpestamp in RFC-3339 format. +/// ## Conversion to/from String +/// 2 different String representations are supported: +/// 1. **as an unsigned integer in decimal format** +/// - Such conversion is lossless and thus bijective. +/// - NTP64 to String: use [`std::fmt::Display::fmt()`] or [`std::string::ToString::to_string()`]. +/// - String to NTP64: use [`std::str::FromStr::from_str()`] +/// 2. **as a [RFC3339](https://www.rfc-editor.org/rfc/rfc3339.html#section-5.8) (human readable) format**: +/// - Such conversion loses some precision because of rounding when conferting the fraction part to nanoseconds +/// - As a consequence it's not bijective: a NTP64 converted to RFC3339 String and then converted back to NTP64 might result to a different time. +/// - NTP64 to String: use [`std::fmt::Display::fmt()`] with the alternate flag (`{:#}`) or [`NTP64::to_string_rfc3339()`]. +/// - String to NTP64: use [`NTP64::parse_rfc3339()`] +/// +/// ## On EPOCH +/// This timestamp in actually similar to a [`std::time::Duration`], as it doesn't define an EPOCH. +/// Only [`NTP64::to_system_time()`], [`NTP64::to_string_rfc3339()`] and [`std::fmt::Display::fmt()`] (when using `{:#}` alternate flag) +/// operations assume that it's relative to UNIX_EPOCH (1st Jan 1970) to display the timestamp in RFC-3339 format. #[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default, Deserialize, Serialize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct NTP64(pub u64); @@ -101,6 +115,30 @@ impl NTP64 { pub fn to_system_time(self) -> SystemTime { UNIX_EPOCH + self.to_duration() } + + /// Convert to a RFC3339 time representation with nanoseconds precision. + /// e.g.: `"2024-07-01T13:51:12.129693000Z"`` + pub fn to_string_rfc3339(&self) -> String { + #[cfg(feature = "std")] + return format_rfc3339_nanos(self.to_system_time()).to_string(); + #[cfg(not(feature = "std"))] + return self.0.to_string(); + } + + /// Parse a RFC3339 time representation into a NTP64. + pub fn parse_rfc3339(s: &str) -> Result { + match humantime::parse_rfc3339(s) { + Ok(time) => time + .duration_since(UNIX_EPOCH) + .map(NTP64::from) + .map_err(|e| ParseNTP64Error { + cause: format!("Failed to parse '{s}' : {e}"), + }), + Err(_) => Err(ParseNTP64Error { + cause: format!("Failed to parse '{s}' : invalid RFC3339 format"), + }), + } + } } impl Add for NTP64 { @@ -208,12 +246,27 @@ impl SubAssign for NTP64 { } impl fmt::Display for NTP64 { + /// By default formats the value as an unsigned integer in decimal format. + /// If the alternate flag `{:#}` is used, formats the value with RFC3339 representation with nanoseconds precision. + /// + /// # Examples + /// ``` + /// use uhlc::NTP64; + /// + /// let t = NTP64(7386690599959157260); + /// println!("{t}"); // displays: 7386690599959157260 + /// println!("{t:#}"); // displays: 2024-07-01T15:32:06.860479000Z + /// ``` fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - // #[cfg(feature = "std")] - // return write!(f, "{}", format_rfc3339_nanos(self.to_system_time())); - // #[cfg(not(feature = "std"))] - // return write!(f, "{:x}", self.0); + // if "{:#}" flag is specified, use RFC3339 representation + if f.alternate() { + #[cfg(feature = "std")] + return write!(f, "{}", format_rfc3339_nanos(self.to_system_time())); + #[cfg(not(feature = "std"))] + return write!(f, "{}", self.0); + } else { + write!(f, "{}", self.0) + } } } @@ -270,4 +323,28 @@ mod tests { ); assert!(epoch_plus_counter_max.as_secs_f64() < 0.0000000035f64); } + + #[test] + fn bijective_to_string() { + use crate::*; + use std::str::FromStr; + for n in 0u64..10000 { + let t = NTP64(n); + assert_eq!(t, NTP64::from_str(&t.to_string()).unwrap()); + } + } + + #[test] + fn to_string_rfc3339() { + use crate::*; + let now = SystemTime::now(); + let t = NTP64::from(SystemTime::now().duration_since(UNIX_EPOCH).unwrap()); + + let rfc3339 = t.to_string_rfc3339(); + assert_eq!(rfc3339, humantime::format_rfc3339_nanos(now).to_string()); + + // Test that alternate format "{:#}" displays in RFC3339 format + let rfc3339_2 = format!("{t:#}"); + assert_eq!(rfc3339_2, humantime::format_rfc3339_nanos(now).to_string()); + } } diff --git a/src/timestamp.rs b/src/timestamp.rs index c7fa7aa..7c6eae1 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -17,6 +17,19 @@ use serde::{Deserialize, Serialize}; use core::str::FromStr; /// A timestamp made of a [`NTP64`] and a [`crate::HLC`]'s unique identifier. +/// +/// ## Conversion to/from String +/// A Timestamp is formatted to a String as such: `"/"` +/// 2 different String representations are supported: +/// 1. **`` as an unsigned integer in decimal format** +/// - Such conversion is lossless and thus bijective. +/// - Timestamp to String: use [`std::fmt::Display::fmt()`] or [`std::string::ToString::to_string()`]. +/// - String to Timestamp: use [`std::str::FromStr::from_str()`] +/// 2. **as a [RFC3339](https://www.rfc-editor.org/rfc/rfc3339.html#section-5.8) (human readable) format**: +/// - Such conversion loses some precision because of rounding when conferting the fraction part to nanoseconds +/// - As a consequence it's not bijective: a NTP64 converted to RFC3339 String and then converted back to NTP64 might result to a different time. +/// - Timestamp to String: use [`std::fmt::Display::fmt()`] with the alternate flag (`{:#}`) or [`NTP64::to_string_rfc3339()`]. +/// - String to Timestamp: use [`NTP64::parse_rfc3339()`] #[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Timestamp { @@ -48,11 +61,53 @@ impl Timestamp { pub fn get_diff_duration(&self, other: &Timestamp) -> Duration { (self.time - other.time).to_duration() } + + /// Convert to a RFC3339 time representation with nanoseconds precision. + /// e.g.: `"2024-07-01T13:51:12.129693000Z/33"`` + pub fn to_string_rfc3339(&self) -> String { + #[cfg(feature = "std")] + return format!("{:#}", self); + #[cfg(not(feature = "std"))] + return self.to_string(); + } + + /// Parse a RFC3339 time representation into a NTP64. + pub fn parse_rfc3339(s: &str) -> Result { + match s.find('/') { + Some(i) => { + let (stime, srem) = s.split_at(i); + let time = NTP64::parse_rfc3339(stime) + .map_err(|e| ParseTimestampError { cause: e.cause })?; + let id = + ID::from_str(&srem[1..]).map_err(|e| ParseTimestampError { cause: e.cause })?; + Ok(Timestamp::new(time, id)) + } + None => Err(ParseTimestampError { + cause: "No '/' found in String".into(), + }), + } + } } impl fmt::Display for Timestamp { + /// Formats Timestamp as the time part followed by the ID part, with `/` as separator. + /// By default the time part is formatted as an unsigned integer in decimal format. + /// If the alternate flag `{:#}` is used, the time part is formatted with RFC3339 representation with nanoseconds precision. + /// + /// # Examples + /// ``` + /// use uhlc::*; + /// + /// let t =Timestamp::new(NTP64(7386690599959157260), ID::try_from([0x33]).unwrap()); + /// println!("{t}"); // displays: 7386690599959157260/33 + /// println!("{t:#}"); // displays: 2024-07-01T15:32:06.860479000Z/33 + /// ``` fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}/{}", self.time, self.id) + if f.alternate() { + write!(f, "{:#}/{}", self.time, self.id) + } else { + write!(f, "{}/{}", self.time, self.id) + } } } @@ -145,19 +200,12 @@ mod tests { } #[test] - fn bijective_string_conversion() { + fn bijective_to_string() { use crate::*; - use std::convert::TryFrom; use std::str::FromStr; - let id: ID = ID::try_from([0x01]).unwrap(); - - for n in 0u64..10000 { - let ts = Timestamp::new(NTP64(n), id); - assert_eq!(ts, Timestamp::from_str(&ts.to_string()).unwrap()); - } let hlc = HLCBuilder::new().with_id(ID::rand()).build(); - for _ in 1..1000 { + for _ in 1..10000 { let now_ts = hlc.new_timestamp(); assert_eq!(now_ts, Timestamp::from_str(&now_ts.to_string()).unwrap()); }