Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change conversion of NTP64 and Timestamp to/from String #16

Merged
merged 8 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ spin = { version = "0.9.8", default-features = false, features = [
[dev-dependencies]
async-std = "1.6"
futures = "0.3"
regex = "1"
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ impl HLC {
let msg_time = timestamp.get_time();
if *msg_time > now && *msg_time - now > self.delta {
let err_msg = format!(
"incoming timestamp from {} exceeding delta {}ms is rejected: {} vs. now: {}",
"incoming timestamp from {} exceeding delta {}ms is rejected: {:#} vs. now: {:#}",
timestamp.get_id(),
self.delta.to_duration().as_millis(),
msg_time,
Expand Down
118 changes: 97 additions & 21 deletions src/ntp64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
use {
core::str::FromStr,
humantime::{format_rfc3339_nanos, parse_rfc3339},
humantime::format_rfc3339_nanos,
std::time::{SystemTime, UNIX_EPOCH},
};

Expand Down Expand Up @@ -48,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 @@ -102,6 +115,29 @@ 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"``
#[cfg(feature = "std")]
pub fn to_string_rfc3339(&self) -> String {
Mallets marked this conversation as resolved.
Show resolved Hide resolved
format_rfc3339_nanos(self.to_system_time()).to_string()
}

/// Parse a RFC3339 time representation into a NTP64.
#[cfg(feature = "std")]
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 @@ -209,17 +245,33 @@ 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 {
#[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()));
JEnoch marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(not(feature = "std"))]
return write!(f, "{}", self.0);
Mallets marked this conversation as resolved.
Show resolved Hide resolved
} else {
write!(f, "{}", self.0)
}
}
}

impl fmt::Debug for NTP64 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:x}", self.0)
write!(f, "{}", self.0)
}
}

Expand All @@ -237,17 +289,9 @@ impl FromStr for NTP64 {
type Err = ParseNTP64Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_rfc3339(s)
.map_err(|e| ParseNTP64Error {
cause: e.to_string(),
})
.and_then(|time| {
time.duration_since(UNIX_EPOCH)
.map_err(|e| ParseNTP64Error {
cause: e.to_string(),
})
})
.map(NTP64::from)
u64::from_str(s).map(NTP64).map_err(|_| ParseNTP64Error {
cause: format!("Invalid NTP64 time : '{s}' (must be a u64)"),
})
}
}

Expand Down Expand Up @@ -278,4 +322,36 @@ 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);
Mallets marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(t, NTP64::from_str(&t.to_string()).unwrap());
}
}

#[test]
fn to_string_rfc3339() {
use crate::*;
use regex::Regex;

let rfc3339_regex = Regex::new(
r"^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]Z$"
).unwrap();

let now = SystemTime::now();
let t = NTP64::from(now.duration_since(UNIX_EPOCH).unwrap());

let rfc3339 = t.to_string_rfc3339();
assert_eq!(rfc3339, humantime::format_rfc3339_nanos(now).to_string());
assert!(rfc3339_regex.is_match(&rfc3339));

// Test that alternate format "{:#}" displays in RFC3339 format
let rfc3339_2 = format!("{t:#}");
assert_eq!(rfc3339_2, humantime::format_rfc3339_nanos(now).to_string());
assert!(rfc3339_regex.is_match(&rfc3339_2));
}
}
72 changes: 71 additions & 1 deletion src/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@
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. **`<ntp64_time>`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 Timestamp converted to RFC3339 String and then converted back to Timestamp might result to a different time.
/// - Timestamp to String: use [`std::fmt::Display::fmt()`] with the alternate flag (`{:#}`) or [`Timestamp::to_string_rfc3339()`].
/// - String to Timestamp: use [`Timestamp::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,56 @@
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"``
#[cfg(feature = "std")]
pub fn to_string_rfc3339(&self) -> String {
Mallets marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(feature = "std")]
return format!("{:#}", self);
#[cfg(not(feature = "std"))]
return self.to_string();
}

/// Parse a RFC3339 time representation into a NTP64.
#[cfg(feature = "std")]
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::*;
/// use std::convert::TryFrom;
///
/// 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 @@ -84,7 +142,7 @@
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(deature = "defmt", derive(defmt::Format))]

Check warning on line 145 in src/timestamp.rs

View workflow job for this annotation

GitHub Actions / Run no_std checks

unexpected `cfg` condition name: `deature`

Check warning on line 145 in src/timestamp.rs

View workflow job for this annotation

GitHub Actions / Run no_std checks

unexpected `cfg` condition name: `deature`
pub struct ParseTimestampError {
pub cause: String,
}
Expand Down Expand Up @@ -143,4 +201,16 @@
let diff = ts1_now.get_diff_duration(&ts2_now);
assert_eq!(diff, Duration::from_secs(0));
}

#[test]
fn bijective_to_string() {
use crate::*;
use std::str::FromStr;

let hlc = HLCBuilder::new().with_id(ID::rand()).build();
for _ in 1..10000 {
let now_ts = hlc.new_timestamp();
assert_eq!(now_ts, Timestamp::from_str(&now_ts.to_string()).unwrap());
}
}
}
Loading