Skip to content

Commit

Permalink
Add explicit conversion of NTP64 and Timestamp to/from RFC3339 String…
Browse files Browse the repository at this point in the history
… format
  • Loading branch information
JEnoch committed Jul 1, 2024
1 parent 7a78ef0 commit ef420f5
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 19 deletions.
95 changes: 86 additions & 9 deletions src/ntp64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
use {
core::str::FromStr,
humantime::format_rfc3339_nanos,
std::time::{SystemTime, UNIX_EPOCH},
};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<Self, ParseNTP64Error> {
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 {
Expand Down Expand Up @@ -208,12 +246,27 @@ impl SubAssign<u64> 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)
}
}
}

Expand Down Expand Up @@ -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());
}
}
68 changes: 58 additions & 10 deletions src/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: `"<ntp64_time>/<hlc_id_hexadecimal>"`
/// 2 different String representations are supported:
/// 1. **`<ntp64_time>` 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 {
Expand Down Expand Up @@ -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<Self, ParseTimestampError> {
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)
}
}
}

Expand Down Expand Up @@ -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());
}
Expand Down

0 comments on commit ef420f5

Please sign in to comment.