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}