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