uhlc/
ntp64.rs

1//
2// Copyright (c) 2017, 2020 ADLINK Technology Inc.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7// which is available at https://www.apache.org/licenses/LICENSE-2.0.
8//
9// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10//
11use core::fmt;
12use core::ops::{Add, AddAssign, Sub, SubAssign};
13use core::str::FromStr;
14use core::time::Duration;
15#[cfg(feature = "std")]
16use std::time::SystemTimeError;
17
18#[cfg(feature = "std")]
19use humantime::TimestampError;
20
21#[cfg(feature = "std")]
22use {
23    humantime::format_rfc3339_nanos,
24    std::time::{SystemTime, UNIX_EPOCH},
25};
26
27// maximal number of seconds that can be represented in the 32-bits part
28const MAX_NB_SEC: u64 = (1u64 << 32) - 1;
29// number of NTP fraction per second (2^32)
30const FRAC_PER_SEC: u64 = 1u64 << 32;
31// Bit-mask for the fraction of a second part within an NTP timestamp
32const FRAC_MASK: u64 = 0xFFFF_FFFFu64;
33
34// number of nanoseconds in 1 second
35const NANO_PER_SEC: u64 = 1_000_000_000;
36
37/// A NTP 64-bits format as specified in
38/// [RFC-5909](https://tools.ietf.org/html/rfc5905#section-6)
39///
40/// ```text
41/// 0                   1                   2                   3
42/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
43/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
44/// |                            Seconds                            |
45/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
46/// |                            Fraction                           |
47/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
48/// ```
49///
50/// The 1st 32-bits part is the number of second since the EPOCH of the physical clock,
51/// and the 2nd 32-bits part is the fraction of second.
52/// In case it's part of a [`crate::Timestamp`] generated by an [`crate::HLC`] the last few bits
53/// of the Fraction part are replaced by the HLC logical counter.
54/// The size of this counter is currently hard-coded as [`crate::CSIZE`].
55///
56/// ## Conversion to/from String
57/// 2 different String representations are supported:
58/// 1. **as an unsigned integer in decimal format**
59///   - Such conversion is lossless and thus bijective.
60///   - NTP64 to String: use [`std::fmt::Display::fmt()`] or [`std::string::ToString::to_string()`].
61///   - String to NTP64: use [`std::str::FromStr::from_str()`]
62/// 2. **as a [RFC3339](https://www.rfc-editor.org/rfc/rfc3339.html#section-5.8) (human readable) format**:
63///   - Such conversion loses some precision because of rounding when conferting the fraction part to nanoseconds
64///   - 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.
65///   - NTP64 to String: use [`std::fmt::Display::fmt()`] with the alternate flag (`{:#}`) or [`NTP64::to_string_rfc3339_lossy()`].
66///   - String to NTP64: use [`NTP64::parse_rfc3339()`]
67///
68/// ## On EPOCH
69/// This timestamp in actually similar to a [`std::time::Duration`], as it doesn't define an EPOCH.
70/// Only [`NTP64::to_system_time()`], [`NTP64::to_string_rfc3339_lossy()`] and [`std::fmt::Display::fmt()`] (when using `{:#}` alternate flag)
71/// operations assume that it's relative to UNIX_EPOCH (1st Jan 1970) to display the timestamp in RFC-3339 format.
72#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
73#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
74#[cfg_attr(feature = "defmt", derive(defmt::Format))]
75pub struct NTP64(pub u64);
76
77impl NTP64 {
78    /// Returns this NTP64 as a u64.
79    #[inline]
80    pub fn as_u64(&self) -> u64 {
81        self.0
82    }
83
84    /// Returns this NTP64 as a f64 in seconds.
85    ///
86    /// The integer part of the f64 is the NTP64's Seconds part.
87    /// The decimal part of the f64 is the result of a division of NTP64's Fraction part divided by 2^32.
88    /// Considering the probable large number of Seconds (for a time relative to UNIX_EPOCH), the precision of the resulting f64 might be in the order of microseconds.
89    /// Therefore, it should not be used for comparison. Directly comparing [NTP64] objects is preferable.
90    #[inline]
91    pub fn as_secs_f64(&self) -> f64 {
92        let secs: f64 = self.as_secs() as f64;
93        let subsec: f64 = ((self.0 & FRAC_MASK) as f64) / FRAC_PER_SEC as f64;
94        secs + subsec
95    }
96
97    /// Returns the 32-bits seconds part.
98    #[inline]
99    pub fn as_secs(&self) -> u32 {
100        (self.0 >> 32) as u32
101    }
102
103    /// Returns the total duration converted to nanoseconds.
104    #[inline]
105    pub fn as_nanos(&self) -> u64 {
106        let secs_as_nanos = (self.as_secs() as u64) * NANO_PER_SEC;
107        let subsec_nanos = self.subsec_nanos() as u64;
108        secs_as_nanos + subsec_nanos
109    }
110
111    /// Returns the 32-bits fraction of second part converted to nanoseconds.
112    #[inline]
113    pub fn subsec_nanos(&self) -> u32 {
114        let frac = self.0 & FRAC_MASK;
115        // Use div_ceil() here to ensure that: assert_eq!(x, NTP64::from(Duration::from_nanos(x)).as_nanos());
116        (frac * NANO_PER_SEC).div_ceil(FRAC_PER_SEC) as u32
117    }
118
119    /// Convert to a [`Duration`].
120    #[inline]
121    pub fn to_duration(self) -> Duration {
122        Duration::new(self.as_secs().into(), self.subsec_nanos())
123    }
124
125    /// Convert to a [`SystemTime`] (making the assumption that this NTP64 is relative to [`UNIX_EPOCH`]).
126    #[inline]
127    #[cfg(feature = "std")]
128    pub fn to_system_time(self) -> SystemTime {
129        UNIX_EPOCH + self.to_duration()
130    }
131
132    /// Convert to a RFC3339 time representation with nanoseconds precision.
133    /// e.g.: `"2024-07-01T13:51:12.129693000Z"``
134    #[cfg(feature = "std")]
135    pub fn to_string_rfc3339_lossy(&self) -> String {
136        format_rfc3339_nanos(self.to_system_time()).to_string()
137    }
138
139    /// Parse a RFC3339 time representation into a NTP64.
140    #[cfg(feature = "std")]
141    pub fn parse_rfc3339(s: &str) -> Result<Self, ParseNTP64Error> {
142        match humantime::parse_rfc3339(s) {
143            Ok(time) => time
144                .duration_since(UNIX_EPOCH)
145                .map(NTP64::from)
146                .map_err(ParseNTP64Error::SystemTimeError),
147            Err(e) => Err(ParseNTP64Error::InvalidRFC3339(e)),
148        }
149    }
150}
151
152impl Add for NTP64 {
153    type Output = Self;
154
155    #[inline]
156    fn add(self, other: Self) -> Self {
157        Self(self.0 + other.0)
158    }
159}
160
161impl Add<NTP64> for &NTP64 {
162    type Output = <NTP64 as Add<NTP64>>::Output;
163
164    #[inline]
165    fn add(self, other: NTP64) -> <NTP64 as Add<NTP64>>::Output {
166        Add::add(*self, other)
167    }
168}
169
170impl Add<&NTP64> for NTP64 {
171    type Output = <NTP64 as Add<NTP64>>::Output;
172
173    #[inline]
174    fn add(self, other: &NTP64) -> <NTP64 as Add<NTP64>>::Output {
175        Add::add(self, *other)
176    }
177}
178
179impl Add<&NTP64> for &NTP64 {
180    type Output = <NTP64 as Add<NTP64>>::Output;
181
182    #[inline]
183    fn add(self, other: &NTP64) -> <NTP64 as Add<NTP64>>::Output {
184        Add::add(*self, *other)
185    }
186}
187
188impl Add<u64> for NTP64 {
189    type Output = Self;
190
191    #[inline]
192    fn add(self, other: u64) -> Self {
193        Self(self.0 + other)
194    }
195}
196
197impl AddAssign<u64> for NTP64 {
198    #[inline]
199    fn add_assign(&mut self, other: u64) {
200        *self = Self(self.0 + other);
201    }
202}
203
204impl Sub for NTP64 {
205    type Output = Self;
206
207    #[inline]
208    fn sub(self, other: Self) -> Self {
209        Self(self.0 - other.0)
210    }
211}
212
213impl Sub<NTP64> for &NTP64 {
214    type Output = <NTP64 as Sub<NTP64>>::Output;
215
216    #[inline]
217    fn sub(self, other: NTP64) -> <NTP64 as Sub<NTP64>>::Output {
218        Sub::sub(*self, other)
219    }
220}
221
222impl Sub<&NTP64> for NTP64 {
223    type Output = <NTP64 as Sub<NTP64>>::Output;
224
225    #[inline]
226    fn sub(self, other: &NTP64) -> <NTP64 as Sub<NTP64>>::Output {
227        Sub::sub(self, *other)
228    }
229}
230
231impl Sub<&NTP64> for &NTP64 {
232    type Output = <NTP64 as Sub<NTP64>>::Output;
233
234    #[inline]
235    fn sub(self, other: &NTP64) -> <NTP64 as Sub<NTP64>>::Output {
236        Sub::sub(*self, *other)
237    }
238}
239
240impl Sub<u64> for NTP64 {
241    type Output = Self;
242
243    #[inline]
244    fn sub(self, other: u64) -> Self {
245        Self(self.0 - other)
246    }
247}
248
249impl SubAssign<u64> for NTP64 {
250    #[inline]
251    fn sub_assign(&mut self, other: u64) {
252        *self = Self(self.0 - other);
253    }
254}
255
256impl fmt::Display for NTP64 {
257    /// By default formats the value as an unsigned integer in decimal format.
258    /// If the alternate flag `{:#}` is used, formats the value with RFC3339 representation with nanoseconds precision.
259    ///
260    /// # Examples
261    /// ```
262    ///   use uhlc::NTP64;
263    ///
264    ///   let t = NTP64(7386690599959157260);
265    ///   println!("{t}");    // displays: 7386690599959157260
266    ///   println!("{t:#}");  // displays: 2024-07-01T15:32:06.860479000Z
267    /// ```
268    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
269        // if "{:#}" flag is specified, use RFC3339 representation
270        if f.alternate() {
271            #[cfg(feature = "std")]
272            return write!(f, "{}", format_rfc3339_nanos(self.to_system_time()));
273            #[cfg(not(feature = "std"))]
274            return write!(f, "{}", self.0);
275        } else {
276            write!(f, "{}", self.0)
277        }
278    }
279}
280
281impl fmt::Debug for NTP64 {
282    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
283        write!(f, "{}", self.0)
284    }
285}
286
287impl From<Duration> for NTP64 {
288    fn from(duration: Duration) -> NTP64 {
289        let secs = duration.as_secs();
290        assert!(secs <= MAX_NB_SEC);
291        let nanos: u64 = duration.subsec_nanos().into();
292        NTP64((secs << 32) + ((nanos * FRAC_PER_SEC) / NANO_PER_SEC))
293    }
294}
295
296#[cfg(all(feature = "nix", target_family = "unix"))]
297impl From<nix::sys::time::TimeSpec> for NTP64 {
298    fn from(ts: nix::sys::time::TimeSpec) -> Self {
299        Self::from(Duration::from(ts))
300    }
301}
302
303impl FromStr for NTP64 {
304    type Err = ParseNTP64Error;
305
306    fn from_str(s: &str) -> Result<Self, Self::Err> {
307        u64::from_str(s)
308            .map(NTP64)
309            .map_err(|_| ParseNTP64Error::ParseIntError)
310    }
311}
312
313#[derive(Debug, Clone)]
314#[cfg_attr(feature = "defmt", derive(defmt::Format))]
315pub enum ParseNTP64Error {
316    ParseIntError,
317    #[cfg(feature = "std")]
318    SystemTimeError(SystemTimeError),
319    #[cfg(feature = "std")]
320    InvalidRFC3339(TimestampError),
321}
322
323impl fmt::Display for ParseNTP64Error {
324    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325        match self {
326            ParseNTP64Error::ParseIntError => {
327                write!(f, "Invalid NTP6 time (not an unsigned 64 bits integer)")
328            }
329            #[cfg(feature = "std")]
330            ParseNTP64Error::SystemTimeError(e) => write!(f, "Invalid NTP6 time ({e})"),
331            #[cfg(feature = "std")]
332            ParseNTP64Error::InvalidRFC3339(e) => write!(f, "Invalid NTP6 time ({e})"),
333        }
334    }
335}
336
337#[cfg(feature = "std")]
338impl std::error::Error for ParseNTP64Error {}
339
340#[cfg(test)]
341mod tests {
342
343    #[test]
344    fn as_secs_f64() {
345        use crate::*;
346
347        let epoch = NTP64::default();
348        assert_eq!(epoch.as_secs_f64(), 0f64);
349
350        let epoch_plus_1 = NTP64(1);
351        assert!(epoch_plus_1 > epoch);
352        assert!(epoch_plus_1.as_secs_f64() > epoch.as_secs_f64());
353
354        // test that Timestamp precision is less than announced (3.5ns) in README.md
355        let epoch_plus_counter_max = NTP64(CMASK);
356        #[cfg(feature = "std")]
357        println!(
358            "Time precision = {} ns",
359            epoch_plus_counter_max.as_secs_f64() * (ntp64::NANO_PER_SEC as f64)
360        );
361        assert!(epoch_plus_counter_max.as_secs_f64() < 0.0000000035f64);
362    }
363
364    #[test]
365    fn as_nanos() {
366        use crate::*;
367        let t = NTP64::from(Duration::new(42, 84));
368        assert_eq!(t.as_nanos(), 42_000_000_084);
369    }
370
371    #[cfg(feature = "std")]
372    #[test]
373    fn bijective_to_string() {
374        use crate::*;
375        use core::str::FromStr;
376        use rand::prelude::*;
377
378        let mut rng = rand::thread_rng();
379        for _ in 0u64..10000 {
380            let t = NTP64(rng.gen());
381            assert_eq!(t, NTP64::from_str(&t.to_string()).unwrap());
382        }
383    }
384
385    #[cfg(feature = "std")]
386    #[test]
387    fn rfc3339_conversion() {
388        use crate::*;
389        use regex::Regex;
390
391        let rfc3339_regex = Regex::new(
392            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$"
393        ).unwrap();
394
395        let now = SystemTime::now();
396        let t = NTP64::from(now.duration_since(UNIX_EPOCH).unwrap());
397
398        let rfc3339 = t.to_string_rfc3339_lossy();
399        assert_eq!(rfc3339, humantime::format_rfc3339_nanos(now).to_string());
400        assert!(rfc3339_regex.is_match(&rfc3339));
401
402        // Test that alternate format "{:#}" displays in RFC3339 format
403        let rfc3339_2 = format!("{t:#}");
404        assert_eq!(rfc3339_2, humantime::format_rfc3339_nanos(now).to_string());
405        assert!(rfc3339_regex.is_match(&rfc3339_2));
406    }
407
408    #[test]
409    fn duration_conversion() {
410        use super::*;
411
412        let zero = NTP64::from(Duration::ZERO);
413        assert_eq!(zero.as_u64(), 0u64);
414        assert_eq!(zero.as_secs_f64(), 0f64);
415
416        let one_sec = NTP64::from(Duration::from_secs(1));
417        assert_eq!(one_sec.as_u64(), 1u64 << 32);
418        assert_eq!(one_sec.as_secs_f64(), 1f64);
419    }
420
421    #[cfg(all(feature = "nix", target_family = "unix"))]
422    #[test]
423    fn from_timespec() {
424        use super::*;
425        let ts = nix::sys::time::TimeSpec::new(42, 84);
426        let t = NTP64::from(ts);
427        assert_eq!(t.as_secs(), 42);
428        assert_eq!(t.subsec_nanos(), 84);
429    }
430}