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 alloc::string::String;
12use core::fmt;
13use core::ops::{Add, AddAssign, Sub, SubAssign};
14use core::time::Duration;
15use serde::{Deserialize, Serialize};
16
17#[cfg(feature = "std")]
18use {
19    core::str::FromStr,
20    humantime::format_rfc3339_nanos,
21    std::time::{SystemTime, UNIX_EPOCH},
22};
23
24// maximal number of seconds that can be represented in the 32-bits part
25const MAX_NB_SEC: u64 = (1u64 << 32) - 1;
26// number of NTP fraction per second (2^32)
27const FRAC_PER_SEC: u64 = 1u64 << 32;
28// Bit-mask for the fraction of a second part within an NTP timestamp
29const FRAC_MASK: u64 = 0xFFFF_FFFFu64;
30
31// number of nanoseconds in 1 second
32const NANO_PER_SEC: u64 = 1_000_000_000;
33
34/// A NTP 64-bits format as specified in
35/// [RFC-5909](https://tools.ietf.org/html/rfc5905#section-6)
36///
37/// ```text
38/// 0                   1                   2                   3
39/// 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
40/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
41/// |                            Seconds                            |
42/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
43/// |                            Fraction                           |
44/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
45/// ```
46///
47/// The 1st 32-bits part is the number of second since the EPOCH of the physical clock,
48/// and the 2nd 32-bits part is the fraction of second.  
49/// In case it's part of a [`crate::Timestamp`] generated by an [`crate::HLC`] the last few bits
50/// of the Fraction part are replaced by the HLC logical counter.
51/// The size of this counter is currently hard-coded as [`crate::CSIZE`].
52///
53/// ## Conversion to/from String
54/// 2 different String representations are supported:
55/// 1. **as an unsigned integer in decimal format**
56///   - Such conversion is lossless and thus bijective.
57///   - NTP64 to String: use [`std::fmt::Display::fmt()`] or [`std::string::ToString::to_string()`].
58///   - String to NTP64: use [`std::str::FromStr::from_str()`]
59/// 2. **as a [RFC3339](https://www.rfc-editor.org/rfc/rfc3339.html#section-5.8) (human readable) format**:
60///   - Such conversion loses some precision because of rounding when conferting the fraction part to nanoseconds
61///   - 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.
62///   - NTP64 to String: use [`std::fmt::Display::fmt()`] with the alternate flag (`{:#}`) or [`NTP64::to_string_rfc3339_lossy()`].
63///   - String to NTP64: use [`NTP64::parse_rfc3339()`]
64///
65/// ## On EPOCH
66/// This timestamp in actually similar to a [`std::time::Duration`], as it doesn't define an EPOCH.  
67/// Only [`NTP64::to_system_time()`], [`NTP64::to_string_rfc3339_lossy()`] and [`std::fmt::Display::fmt()`] (when using `{:#}` alternate flag)
68/// operations assume that it's relative to UNIX_EPOCH (1st Jan 1970) to display the timestamp in RFC-3339 format.
69#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default, Deserialize, Serialize)]
70#[cfg_attr(feature = "defmt", derive(defmt::Format))]
71pub struct NTP64(pub u64);
72
73impl NTP64 {
74    /// Returns this NTP64 as a u64.
75    #[inline]
76    pub fn as_u64(&self) -> u64 {
77        self.0
78    }
79
80    /// Returns this NTP64 as a f64 in seconds.
81    ///
82    /// The integer part of the f64 is the NTP64's Seconds part.  
83    /// The decimal part of the f64 is the result of a division of NTP64's Fraction part divided by 2^32.  
84    /// 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.
85    /// Therefore, it should not be used for comparison. Directly comparing [NTP64] objects is preferable.
86    #[inline]
87    pub fn as_secs_f64(&self) -> f64 {
88        let secs: f64 = self.as_secs() as f64;
89        let subsec: f64 = ((self.0 & FRAC_MASK) as f64) / FRAC_PER_SEC as f64;
90        secs + subsec
91    }
92
93    /// Returns the 32-bits seconds part.
94    #[inline]
95    pub fn as_secs(&self) -> u32 {
96        (self.0 >> 32) as u32
97    }
98
99    /// Returns the total duration converted to nanoseconds.
100    #[inline]
101    pub fn as_nanos(&self) -> u64 {
102        let secs_as_nanos = (self.as_secs() as u64) * NANO_PER_SEC;
103        let subsec_nanos = self.subsec_nanos() as u64;
104        secs_as_nanos + subsec_nanos
105    }
106
107    /// Returns the 32-bits fraction of second part converted to nanoseconds.
108    #[inline]
109    pub fn subsec_nanos(&self) -> u32 {
110        let frac = self.0 & FRAC_MASK;
111        // Use div_ceil() here to ensure that: assert_eq!(x, NTP64::from(Duration::from_nanos(x)).as_nanos());
112        (frac * NANO_PER_SEC).div_ceil(FRAC_PER_SEC) as u32
113    }
114
115    /// Convert to a [`Duration`].
116    #[inline]
117    pub fn to_duration(self) -> Duration {
118        Duration::new(self.as_secs().into(), self.subsec_nanos())
119    }
120
121    /// Convert to a [`SystemTime`] (making the assumption that this NTP64 is relative to [`UNIX_EPOCH`]).
122    #[inline]
123    #[cfg(feature = "std")]
124    pub fn to_system_time(self) -> SystemTime {
125        UNIX_EPOCH + self.to_duration()
126    }
127
128    /// Convert to a RFC3339 time representation with nanoseconds precision.
129    /// e.g.: `"2024-07-01T13:51:12.129693000Z"``
130    #[cfg(feature = "std")]
131    pub fn to_string_rfc3339_lossy(&self) -> String {
132        format_rfc3339_nanos(self.to_system_time()).to_string()
133    }
134
135    /// Parse a RFC3339 time representation into a NTP64.
136    #[cfg(feature = "std")]
137    pub fn parse_rfc3339(s: &str) -> Result<Self, ParseNTP64Error> {
138        match humantime::parse_rfc3339(s) {
139            Ok(time) => time
140                .duration_since(UNIX_EPOCH)
141                .map(NTP64::from)
142                .map_err(|e| ParseNTP64Error {
143                    cause: format!("Failed to parse '{s}' : {e}"),
144                }),
145            Err(_) => Err(ParseNTP64Error {
146                cause: format!("Failed to parse '{s}' : invalid RFC3339 format"),
147            }),
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
303#[cfg(feature = "std")]
304impl FromStr for NTP64 {
305    type Err = ParseNTP64Error;
306
307    fn from_str(s: &str) -> Result<Self, Self::Err> {
308        u64::from_str(s).map(NTP64).map_err(|_| ParseNTP64Error {
309            cause: format!("Invalid NTP64 time : '{s}' (must be a u64)"),
310        })
311    }
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
315#[cfg_attr(feature = "defmt", derive(defmt::Format))]
316pub struct ParseNTP64Error {
317    pub cause: String,
318}
319
320#[cfg(test)]
321mod tests {
322
323    #[test]
324    fn as_secs_f64() {
325        use crate::*;
326
327        let epoch = NTP64::default();
328        assert_eq!(epoch.as_secs_f64(), 0f64);
329
330        let epoch_plus_1 = NTP64(1);
331        assert!(epoch_plus_1 > epoch);
332        assert!(epoch_plus_1.as_secs_f64() > epoch.as_secs_f64());
333
334        // test that Timestamp precision is less than announced (3.5ns) in README.md
335        let epoch_plus_counter_max = NTP64(CMASK);
336        println!(
337            "Time precision = {} ns",
338            epoch_plus_counter_max.as_secs_f64() * (ntp64::NANO_PER_SEC as f64)
339        );
340        assert!(epoch_plus_counter_max.as_secs_f64() < 0.0000000035f64);
341    }
342
343    #[test]
344    fn as_nanos() {
345        use crate::*;
346        let t = NTP64::from(Duration::new(42, 84));
347        assert_eq!(t.as_nanos(), 42_000_000_084);
348    }
349
350    #[test]
351    fn bijective_to_string() {
352        use crate::*;
353        use rand::prelude::*;
354        use std::str::FromStr;
355
356        let mut rng = rand::thread_rng();
357        for _ in 0u64..10000 {
358            let t = NTP64(rng.gen());
359            assert_eq!(t, NTP64::from_str(&t.to_string()).unwrap());
360        }
361    }
362
363    #[test]
364    fn rfc3339_conversion() {
365        use crate::*;
366        use regex::Regex;
367
368        let rfc3339_regex = Regex::new(
369            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$"
370        ).unwrap();
371
372        let now = SystemTime::now();
373        let t = NTP64::from(now.duration_since(UNIX_EPOCH).unwrap());
374
375        let rfc3339 = t.to_string_rfc3339_lossy();
376        assert_eq!(rfc3339, humantime::format_rfc3339_nanos(now).to_string());
377        assert!(rfc3339_regex.is_match(&rfc3339));
378
379        // Test that alternate format "{:#}" displays in RFC3339 format
380        let rfc3339_2 = format!("{t:#}");
381        assert_eq!(rfc3339_2, humantime::format_rfc3339_nanos(now).to_string());
382        assert!(rfc3339_regex.is_match(&rfc3339_2));
383    }
384
385    #[test]
386    fn duration_conversion() {
387        use super::*;
388
389        let zero = NTP64::from(Duration::ZERO);
390        assert_eq!(zero.as_u64(), 0u64);
391        assert_eq!(zero.as_secs_f64(), 0f64);
392
393        let one_sec = NTP64::from(Duration::from_secs(1));
394        assert_eq!(one_sec.as_u64(), 1u64 << 32);
395        assert_eq!(one_sec.as_secs_f64(), 1f64);
396    }
397
398    #[cfg(all(feature = "nix", target_family = "unix"))]
399    #[test]
400    fn from_timespec() {
401        use super::*;
402        let ts = nix::sys::time::TimeSpec::new(42, 84);
403        let t = NTP64::from(ts);
404        assert_eq!(t.as_secs(), 42);
405        assert_eq!(t.subsec_nanos(), 84);
406    }
407}