Skip to main content

qubit_clock/monotonic/
monotonic_instant.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Defines a monotonic instant scoped to one clock domain.
9
10use crate::{
11    ClockDomain,
12    TimeError,
13};
14use std::cmp::Ordering;
15use std::time::Duration;
16
17/// A fixed point in one monotonic clock domain.
18///
19/// Instants from different domains cannot be ordered or used in arithmetic
20/// together. The value carries the full precision available through
21/// [`Duration`] without claiming any particular hardware timer resolution.
22///
23/// Discarding a sampled instant is rejected when `unused_must_use` is denied:
24///
25/// ```compile_fail
26/// #![deny(unused_must_use)]
27/// use qubit_clock::{ManualMonotonicClock, MonotonicClock};
28///
29/// ManualMonotonicClock::new().now();
30/// ```
31#[must_use = "monotonic instants should be used to measure or compare time"]
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub struct MonotonicInstant {
34    /// The identifier of the originating monotonic clock domain.
35    domain: ClockDomain,
36    /// The elapsed duration from the originating clock domain's origin.
37    elapsed: Duration,
38}
39
40impl MonotonicInstant {
41    /// Creates an instant for a clock implementation.
42    ///
43    /// # Parameters
44    ///
45    /// * `domain` - Identifier of the originating clock.
46    /// * `elapsed` - Duration measured from that clock's private origin.
47    ///
48    /// # Returns
49    ///
50    /// An instant scoped to `domain` at `elapsed`.
51    #[inline(always)]
52    pub const fn new(domain: ClockDomain, elapsed: Duration) -> Self {
53        Self { domain, elapsed }
54    }
55
56    /// Returns the identifier of the originating monotonic clock domain.
57    ///
58    /// # Returns
59    ///
60    /// The domain carried by this instant.
61    #[inline(always)]
62    pub const fn domain(self) -> ClockDomain {
63        self.domain
64    }
65
66    /// Returns the elapsed duration from this clock domain's origin.
67    ///
68    /// The value is meaningful only inside the domain identified by
69    /// [`domain()`](Self::domain).
70    ///
71    /// # Returns
72    ///
73    /// The duration from the originating clock's private origin.
74    #[must_use]
75    #[inline(always)]
76    pub const fn elapsed_since_origin(self) -> Duration {
77        self.elapsed
78    }
79
80    /// Adds a duration while preserving the originating clock domain.
81    ///
82    /// Returns [`TimeError::InstantOverflow`] when the result cannot be
83    /// represented by [`Duration`].
84    ///
85    /// # Parameters
86    ///
87    /// * `duration` - Duration to add to this instant.
88    ///
89    /// # Returns
90    ///
91    /// A same-domain instant advanced by `duration`.
92    ///
93    /// # Errors
94    ///
95    /// Returns [`TimeError::InstantOverflow`] when the elapsed duration
96    /// cannot represent the result.
97    #[inline]
98    pub fn checked_add(self, duration: Duration) -> Result<Self, TimeError> {
99        let elapsed = self
100            .elapsed
101            .checked_add(duration)
102            .ok_or(TimeError::InstantOverflow)?;
103        Ok(Self::new(self.domain, elapsed))
104    }
105
106    /// Calculates the duration elapsed since an earlier instant.
107    ///
108    /// Returns [`TimeError::ClockDomainMismatch`] when `earlier` belongs to a
109    /// different clock, and [`TimeError::InvalidInstantOrder`] when `earlier`
110    /// is later than this instant.
111    ///
112    /// # Parameters
113    ///
114    /// * `earlier` - Earlier instant expected to belong to the same domain.
115    ///
116    /// # Returns
117    ///
118    /// The elapsed duration from `earlier` to this instant.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`TimeError::ClockDomainMismatch`] for a foreign instant.
123    /// Returns [`TimeError::InvalidInstantOrder`] when `earlier` is later,
124    /// retaining both elapsed durations.
125    #[inline]
126    pub fn duration_since(self, earlier: Self) -> Result<Duration, TimeError> {
127        earlier.validate_domain(self.domain)?;
128        self.elapsed.checked_sub(earlier.elapsed).ok_or(
129            TimeError::InvalidInstantOrder {
130                current_elapsed: self.elapsed,
131                earlier_elapsed: earlier.elapsed,
132            },
133        )
134    }
135
136    /// Verifies that this instant belongs to `expected_domain`.
137    ///
138    /// Custom monotonic clocks and timers can use this method to reject
139    /// externally supplied instants before performing domain-specific work.
140    ///
141    /// # Parameters
142    ///
143    /// * `expected_domain` - Domain the instant must belong to.
144    ///
145    /// # Returns
146    ///
147    /// `Ok(())` when the domains match.
148    ///
149    /// # Errors
150    ///
151    /// Returns [`TimeError::ClockDomainMismatch`] when the domains differ.
152    #[inline]
153    pub fn validate_domain(
154        self,
155        expected_domain: ClockDomain,
156    ) -> Result<(), TimeError> {
157        if self.domain == expected_domain {
158            Ok(())
159        } else {
160            Err(TimeError::ClockDomainMismatch {
161                expected: expected_domain,
162                actual: self.domain,
163            })
164        }
165    }
166}
167
168impl PartialOrd for MonotonicInstant {
169    /// Orders two instants only when they belong to the same clock domain.
170    ///
171    /// # Parameters
172    ///
173    /// * `other` - Instant to compare with this one.
174    ///
175    /// # Returns
176    ///
177    /// Their elapsed-time ordering for a shared domain, or `None` for distinct
178    /// domains.
179    #[inline]
180    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
181        (self.domain == other.domain).then(|| self.elapsed.cmp(&other.elapsed))
182    }
183}