Skip to main content

sans_io_time/
lib.rs

1// Copyright (C) 2025 Matthew Waters <matthew@centricular.com>
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9#![no_std]
10#![deny(missing_debug_implementations)]
11#![deny(missing_docs)]
12#![cfg_attr(docsrs, feature(doc_cfg))]
13
14//! # sans-io-time
15//!
16//! Represent time as an abstract absolute value based on an arbitrary start point in a
17//! (user-provided) timeline.
18//!
19//! ## Why?
20//!
21//! 1. `no_std` environments may require an implementation of `Instant` and may require
22//!    leaving the implementation details to the implementor.
23//! 2. Network protocols may require that time continues during suspend of the OS
24//!    which is not something that is guaranteed by `std::time::Instant`.
25//! 3. Other uses of `std::time::Instant` should not count time during suspend e.g.
26//!    CPU process time, or process uptime.
27//!
28//! The `Instant`  type provided by this crate satisfies all of these constraints by
29//! only specifying the carriage of data. It does not specify how the current time
30//! is acquired or whether time continues during suspend. Both of these questions
31//! are required to be answered (or left unspecified) by the caller by either using
32//! `std::time::Instant` or `std::time::SystemTime` (with the "std" feature), or
33//! converting from another source of time like
34//! [boot-time](https://crates.io/crates/boot-time).
35//!
36//! In a sans-IO library, the decision on the exact clock source can be decided by the
37//! user of the library rather than specifying a particular `Instant` implementation.
38//!
39//! ## Features
40//! - "std" (enabled by default) enables conversion from `std::time::Instant` and
41//!   `std::time::SystemTime` into an [`Instant`].
42
43#[cfg(feature = "std")]
44extern crate std;
45
46use core::time::Duration;
47
48/// An absolute point in time.
49///
50/// An [`Instant`] is a wrapper around a signed integer that holds the number of nanoseconds since
51/// an arbitrary point in time, e.g. system startup.
52///
53/// - A value of `0` is arbitrary.
54/// - Values < 0 indicate a time before the start point.
55#[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
56pub struct Instant {
57    nanos: i64,
58}
59
60impl Instant {
61    /// The zero [`Instant`].
62    ///
63    /// An arbitrary point on the absolute timescale.
64    pub const ZERO: Instant = Instant { nanos: 0 };
65
66    /// Construct an [`Instant`] from a number of nanoseconds.
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// # use sans_io_time::Instant;
72    /// let instant = Instant::from_nanos(1_234_567_890);
73    /// assert_eq!(instant.as_nanos(), 1_234_567_890);
74    /// ```
75    pub fn from_nanos(nanos: i64) -> Self {
76        Self { nanos }
77    }
78
79    /// The number of nanoseconds that have passed since the `ZERO` point.
80    pub const fn as_nanos(&self) -> i64 {
81        self.nanos
82    }
83
84    /// The number of whole seconds since the `ZERO` point.
85    pub const fn secs(&self) -> i64 {
86        self.nanos / 1_000_000_000
87    }
88
89    /// The number of fractional nanoseconds since the `ZERO` point.
90    pub const fn subsec_nanos(&self) -> i64 {
91        self.nanos % 1_000_000_000
92    }
93
94    /// The amount of time elapsed since an earlier [`Instant`], or `0`.
95    ///
96    /// # Examples
97    ///
98    /// ```
99    /// # use sans_io_time::Instant;
100    /// # use core::time::Duration;
101    /// let earlier = Instant::from_nanos(1_234_567_890);
102    /// let later = Instant::from_nanos(2_234_567_890);
103    /// assert_eq!(later.duration_since(earlier), Duration::from_nanos(1_000_000_000));
104    /// assert_eq!(earlier.duration_since(later), Duration::ZERO);
105    /// assert_eq!(earlier.duration_since(earlier), Duration::ZERO);
106    /// ```
107    pub fn duration_since(&self, earlier: Instant) -> Duration {
108        self.saturating_duration_since(earlier)
109    }
110
111    /// The amount of time elapsed since an earlier [`Instant`], or `None`.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// # use sans_io_time::Instant;
117    /// # use core::time::Duration;
118    /// let earlier = Instant::from_nanos(1_234_567_890);
119    /// let later = Instant::from_nanos(2_234_567_890);
120    /// assert_eq!(later.checked_duration_since(earlier), Some(Duration::from_nanos(1_000_000_000)));
121    /// assert_eq!(earlier.checked_duration_since(later), None);
122    /// assert_eq!(earlier.checked_duration_since(earlier), Some(Duration::ZERO));
123    /// ```
124    pub fn checked_duration_since(&self, earlier: Instant) -> Option<Duration> {
125        self.nanos.checked_sub(earlier.nanos).and_then(|nanos| {
126            if nanos < 0 {
127                None
128            } else {
129                Some(Duration::from_nanos(nanos as u64))
130            }
131        })
132    }
133
134    /// The amount of time elapsed since an earlier [`Instant`], or `0`.
135    pub fn saturating_duration_since(&self, earlier: Instant) -> Duration {
136        self.checked_duration_since(earlier)
137            .unwrap_or(Duration::ZERO)
138    }
139
140    /// Returns the result of `self + duration` if the result can be represented by the underlying
141    /// data structure, `None` otherwise.
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// # use sans_io_time::Instant;
147    /// # use core::time::Duration;
148    /// let instant = Instant::from_nanos(1_234_567_890);
149    /// assert_eq!(instant.checked_add(Duration::from_secs(1)), Some(Instant::from_nanos(2_234_567_890)));
150    /// assert_eq!(instant.checked_add(Duration::ZERO), Some(instant));
151    /// ```
152    pub fn checked_add(&self, duration: Duration) -> Option<Instant> {
153        let dur_nanos: i64 = duration.as_nanos().try_into().ok()?;
154        let nanos = self.nanos.checked_add(dur_nanos)?;
155        Some(Self { nanos })
156    }
157
158    /// Returns the result of `self - duration` if the result can be represented by the underlying
159    /// data structure, `None` otherwise.
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// # use sans_io_time::Instant;
165    /// # use core::time::Duration;
166    /// let instant = Instant::from_nanos(1_234_567_890);
167    /// assert_eq!(instant.checked_sub(Duration::from_secs(1)), Some(Instant::from_nanos(234_567_890)));
168    /// assert_eq!(instant.checked_sub(Duration::from_secs(2)), Some(Instant::from_nanos(-765_432_110)));
169    /// assert_eq!(instant.checked_sub(Duration::ZERO), Some(instant));
170    /// ```
171    pub fn checked_sub(&self, duration: Duration) -> Option<Instant> {
172        let dur_nanos: i64 = duration.as_nanos().try_into().ok()?;
173        let nanos = self.nanos.checked_sub(dur_nanos)?;
174        Some(Self { nanos })
175    }
176
177    /// Construct an [`Instant`] from a `std::time::Instant` based on its the elapsed time.
178    ///
179    /// Can be used if you have a base `std::time::Instant` where you can create [`Instant`]s as
180    /// needed.
181    ///
182    /// In this usage, the base `std::time::Instant` is mapped to `Instant::ZERO` and [`Instant`]
183    /// values represent the distance from the base `std::time::Instant`.
184    ///
185    /// # Example
186    ///
187    /// ```
188    /// # use sans_io_time::Instant;
189    /// # use core::time::Duration;
190    /// let std_instant = std::time::Instant::now();
191    /// std::thread::sleep(Duration::from_secs(1));
192    /// assert!(Instant::from_std(std_instant) >= Instant::from_nanos(1_000_000_000));
193    /// ```
194    #[cfg(feature = "std")]
195    pub fn from_std(instant: ::std::time::Instant) -> Self {
196        let elapsed = instant.elapsed();
197        Self {
198            nanos: elapsed
199                .as_nanos()
200                .try_into()
201                .expect("Elapsed time too large to fit into Instant"),
202        }
203    }
204
205    /// Construct an [`Instant`] from a `std::time::SystemTime` based on its the elapsed time.
206    ///
207    /// Can be used if you have a base `std::time::SystemTime` where you can create [`Instant`]s as
208    /// needed.
209    ///
210    /// In this usage, the base `std::time::SystemTime` is mapped to `Instant::ZERO` and [`Instant`]
211    /// values represent the distance from the base `std::time::SystemTime`.
212    ///
213    /// # Example
214    ///
215    /// ```
216    /// # use sans_io_time::Instant;
217    /// # use core::time::Duration;
218    /// let sys_time = std::time::SystemTime::now();
219    /// std::thread::sleep(Duration::from_secs(1));
220    /// assert!(Instant::from_system_elapsed(sys_time).unwrap() >= Instant::from_nanos(1_000_000_000));
221    /// ```
222    #[cfg(feature = "std")]
223    pub fn from_system_elapsed(
224        sys_time: ::std::time::SystemTime,
225    ) -> Result<Self, std::time::SystemTimeError> {
226        let dur = sys_time.elapsed()?;
227        Ok(Self {
228            nanos: dur
229                .as_nanos()
230                .try_into()
231                .expect("Elapsed time too large to fit into Instant"),
232        })
233    }
234
235    /// Construct an [`Instant`] from a `std::time::SystemTime` based on the distance from the unix
236    /// epoch.
237    ///
238    /// In this usage, the base `std::time::UNIX_EPOCH` is mapped to `Instant::ZERO` and [`Instant`]
239    /// values represent the distance from the base `std::time::UNIX_EPOCH`.
240    ///
241    /// # Example
242    ///
243    /// ```
244    /// # use sans_io_time::Instant;
245    /// # use core::time::Duration;
246    /// let sys_time = std::time::SystemTime::now();
247    /// assert!(Instant::from_system_unix_epoch(sys_time) >= Instant::from_nanos(0));
248    /// ```
249    #[cfg(feature = "std")]
250    pub fn from_system_unix_epoch(sys_time: ::std::time::SystemTime) -> Self {
251        let dur = sys_time
252            .duration_since(::std::time::UNIX_EPOCH)
253            .expect("start time must not be before the unix epoch");
254        Self {
255            nanos: dur
256                .as_nanos()
257                .try_into()
258                .expect("Elapsed time too large to fit into Instant"),
259        }
260    }
261
262    /// Convert this [`Instant`] to a `std::time::Instant` using a base instant.
263    ///
264    /// This function takes a base `std::time::Instant` and adds the duration represented
265    /// by this [`Instant`] to create a corresponding `std::time::Instant`.
266    ///
267    /// In this usage, the base `std::time::Instant` is mapped to `Instant::ZERO` and [`Instant`]
268    /// values represent the distance from the base `std::time::Instant`.
269    ///
270    /// # Example
271    ///
272    /// ```
273    /// # use sans_io_time::Instant;
274    /// # use core::time::Duration;
275    /// let base = std::time::Instant::now();
276    /// let instant = Instant::from_nanos(1_000_000_000); // 1 second
277    /// let std_instant = instant.to_std(base);
278    ///
279    /// // The resulting std_instant should be 1 second after base
280    /// assert!(std_instant.duration_since(base) == Duration::from_secs(1));
281    /// ```
282    #[cfg(feature = "std")]
283    pub fn to_std(&self, base_instant: ::std::time::Instant) -> ::std::time::Instant {
284        let duration_since = ::core::time::Duration::from_nanos(
285            self.nanos
286                .try_into()
287                .expect("Elapsed time too large to fit into Duration"),
288        );
289        base_instant + duration_since
290    }
291
292    /// Convert this [`Instant`] to a `std::time::SystemTime` using a base system time.
293    ///
294    /// This function takes a base `std::time::SystemTime` and adds the duration represented
295    /// by this [`Instant`] to create a corresponding `std::time::SystemTime`.
296    ///
297    /// If the [`Instant`] was created with [`Instant::from_system_unix_epoch`] then the base
298    /// `std::time::SystemTime` should be `std::time::UNIX_EPOCH`. Otherwise, the base
299    /// `std::time::SystemTime` should be the same value passed in to
300    /// [`Instant::from_system_elapsed`].
301    ///
302    /// # Example
303    ///
304    /// ```
305    /// # use sans_io_time::Instant;
306    /// # use core::time::Duration;
307    /// let base = std::time::SystemTime::now();
308    /// let instant = Instant::from_nanos(1_000_000_000); // 1 second
309    /// let sys_time = instant.to_system(base);
310    ///
311    /// // The resulting sys_time should be 1 second after base
312    /// assert!(sys_time.duration_since(base).unwrap() == Duration::from_secs(1));
313    /// ```
314    #[cfg(feature = "std")]
315    pub fn to_system(&self, base_system_time: ::std::time::SystemTime) -> ::std::time::SystemTime {
316        let duration_since = ::core::time::Duration::from_nanos(
317            self.nanos
318                .try_into()
319                .expect("Elapsed time too large to fit into Duration"),
320        );
321        base_system_time + duration_since
322    }
323}
324
325impl core::fmt::Display for Instant {
326    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
327        let secs = self.secs();
328        let nanos = self.subsec_nanos();
329        if secs != 0 {
330            write!(f, "{}", secs)?;
331            if nanos != 0 {
332                write!(f, ".{:0>9}", nanos.abs())?;
333            }
334            write!(f, "s")
335        } else if nanos != 0 {
336            if nanos < 0 {
337                write!(f, "-")?;
338            }
339            let millis = nanos.abs() / 1_000_000;
340            let millis_rem = nanos.abs() % 1_000_000;
341            write!(f, "{}.{:0>6}ms", millis, millis_rem)
342        } else {
343            write!(f, "0s")
344        }
345    }
346}
347
348impl core::ops::Add<Duration> for Instant {
349    type Output = Instant;
350    fn add(self, rhs: Duration) -> Self::Output {
351        self.checked_add(rhs)
352            .expect("Duration too large to fit into Instant")
353    }
354}
355
356impl core::ops::AddAssign<Duration> for Instant {
357    fn add_assign(&mut self, rhs: Duration) {
358        *self = self
359            .checked_add(rhs)
360            .expect("Duration too large to fit into Instant");
361    }
362}
363
364impl core::ops::Sub<Duration> for Instant {
365    type Output = Instant;
366    fn sub(self, rhs: Duration) -> Self::Output {
367        self.checked_sub(rhs)
368            .expect("Duration too large to fit into Instant")
369    }
370}
371
372impl core::ops::SubAssign<Duration> for Instant {
373    fn sub_assign(&mut self, rhs: Duration) {
374        *self = self
375            .checked_sub(rhs)
376            .expect("Duration too large to fit into Instant");
377    }
378}
379
380impl core::ops::Sub<Instant> for Instant {
381    type Output = Duration;
382    fn sub(self, rhs: Instant) -> Self::Output {
383        self.saturating_duration_since(rhs)
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    extern crate alloc;
392
393    #[test]
394    fn add() {
395        let base = Instant::from_nanos(1_222_333_444);
396        assert_eq!(
397            base + Duration::from_secs(1),
398            Instant::from_nanos(2_222_333_444)
399        );
400        let mut new = base;
401        new += Duration::from_secs(1);
402        assert_eq!(new, Instant::from_nanos(2_222_333_444));
403    }
404
405    #[test]
406    fn sub_duration() {
407        let base = Instant::from_nanos(1_222_333_444);
408        assert_eq!(
409            base - Duration::from_secs(1),
410            Instant::from_nanos(222_333_444)
411        );
412        let mut new = base;
413        new -= Duration::from_secs(1);
414        assert_eq!(new, Instant::from_nanos(222_333_444));
415    }
416
417    #[test]
418    fn sub_instant() {
419        let earlier = Instant::from_nanos(1_222_333_444);
420        let later = Instant::from_nanos(2_333_444_555);
421        assert_eq!(later - earlier, Duration::from_nanos(1_111_111_111));
422        assert_eq!(earlier - later, Duration::ZERO);
423        assert_eq!(earlier - earlier, Duration::ZERO);
424    }
425
426    #[test]
427    fn display() {
428        assert_eq!(&alloc::format!("{}", Instant::ZERO), "0s");
429        assert_eq!(&alloc::format!("{}", Instant::from_nanos(1)), "0.000001ms");
430        assert_eq!(
431            &alloc::format!("{}", Instant::from_nanos(-1)),
432            "-0.000001ms"
433        );
434        assert_eq!(
435            &alloc::format!("{}", Instant::from_nanos(1_000_000_000)),
436            "1s"
437        );
438        assert_eq!(
439            &alloc::format!("{}", Instant::from_nanos(1_000_000_001)),
440            "1.000000001s"
441        );
442        assert_eq!(
443            &alloc::format!("{}", Instant::from_nanos(1_100_000_000)),
444            "1.100000000s"
445        );
446        assert_eq!(
447            &alloc::format!("{}", Instant::from_nanos(-1_000_000_000)),
448            "-1s"
449        );
450        assert_eq!(
451            &alloc::format!("{}", Instant::from_nanos(-1_000_000_001)),
452            "-1.000000001s"
453        );
454        assert_eq!(
455            &alloc::format!("{}", Instant::from_nanos(-1_100_000_000)),
456            "-1.100000000s"
457        );
458    }
459}