Skip to main content

win_time_sys/
timeline.rs

1// Copyright (c) 2026 Mike Grier
2//! Timelines, the points on them, and the distances between those points ([WT-D-2]).
3//!
4//! [WT-D-2]: https://github.com/MikeGrier/windows-threadpool-sys/blob/main/crates/win-time-sys/DESIGN-NOTES.md#wt-d-2
5
6use core::cmp::Ordering;
7use core::fmt;
8use core::hash::{Hash, Hasher};
9use core::marker::PhantomData;
10use core::num::NonZeroU64;
11use core::ops::{Add, AddAssign, Neg, Sub, SubAssign};
12use core::time::Duration;
13
14#[cfg(test)]
15mod tests;
16
17const NANOS_PER_SECOND: u128 = 1_000_000_000;
18
19/// Windows' 100-nanosecond unit, the tick of interrupt time and system time.
20pub(crate) const HUNDRED_NANOSECOND_TICKS: NonZeroU64 = NonZeroU64::new(10_000_000).unwrap();
21
22/// What a tick count means: how long one tick is, and what zero is.
23///
24/// A timeline is a type that is never made; it exists to keep values on different timelines
25/// apart. Its tick is [`ticks_per_second`](Self::ticks_per_second); its zero is stated in its
26/// documentation, because a value never needs it -- only a reader does.
27pub trait Timeline: 'static {
28    /// How many ticks make one second. It must not change for the life of the process.
29    fn ticks_per_second() -> NonZeroU64;
30}
31
32/// A point on timeline `T`: a tick count since the timeline's zero.
33///
34/// Points on one timeline compare, subtract to [`Ticks`], and can be written down as a tick count
35/// and read back ([`ticks`](Self::ticks), [`from_ticks`](Self::from_ticks)). Points on different
36/// timelines do not compare at all, because their types differ:
37///
38/// ```compile_fail,E0308
39/// use core::num::NonZeroU64;
40/// use win_time_sys::{TimePoint, Timeline};
41///
42/// enum Boot {}
43/// impl Timeline for Boot {
44///     fn ticks_per_second() -> NonZeroU64 { NonZeroU64::new(10_000_000).unwrap() }
45/// }
46/// enum Calendar {}
47/// impl Timeline for Calendar {
48///     fn ticks_per_second() -> NonZeroU64 { NonZeroU64::new(10_000_000).unwrap() }
49/// }
50///
51/// // The same unit, but not the same zero: refused.
52/// let _ = TimePoint::<Boot>::from_ticks(1) < TimePoint::<Calendar>::from_ticks(2);
53/// ```
54pub struct TimePoint<T: Timeline> {
55    ticks: u64,
56    timeline: PhantomData<fn() -> T>,
57}
58
59/// A distance between two points on timeline `T`, in its ticks. Signed, because the later point
60/// may be on either side.
61pub struct Ticks<T: Timeline> {
62    count: i64,
63    timeline: PhantomData<fn() -> T>,
64}
65
66impl<T: Timeline> TimePoint<T> {
67    /// The point `ticks` after the timeline's zero: how a recorded point is read back.
68    #[must_use]
69    pub const fn from_ticks(ticks: u64) -> Self {
70        Self {
71            ticks,
72            timeline: PhantomData,
73        }
74    }
75
76    /// The tick count since the timeline's zero: how a point is recorded.
77    #[must_use]
78    pub const fn ticks(self) -> u64 {
79        self.ticks
80    }
81
82    /// How far this point is after `earlier` -- negative if it is before -- or `None` if the
83    /// distance does not fit in [`Ticks`].
84    #[must_use]
85    pub fn checked_since(self, earlier: Self) -> Option<Ticks<T>> {
86        let distance = i128::from(self.ticks) - i128::from(earlier.ticks);
87        i64::try_from(distance).ok().map(Ticks::new)
88    }
89
90    /// The point `ticks` later, or `None` if it falls outside the timeline.
91    #[must_use]
92    pub fn checked_add(self, ticks: Ticks<T>) -> Option<Self> {
93        let moved = i128::from(self.ticks) + i128::from(ticks.count);
94        u64::try_from(moved).ok().map(Self::from_ticks)
95    }
96
97    /// The point `ticks` earlier, or `None` if it falls outside the timeline.
98    #[must_use]
99    pub fn checked_sub(self, ticks: Ticks<T>) -> Option<Self> {
100        let moved = i128::from(self.ticks) - i128::from(ticks.count);
101        u64::try_from(moved).ok().map(Self::from_ticks)
102    }
103}
104
105impl<T: Timeline> Ticks<T> {
106    /// No distance.
107    pub const ZERO: Self = Self::new(0);
108
109    /// `count` ticks of timeline `T`.
110    #[must_use]
111    pub const fn new(count: i64) -> Self {
112        Self {
113            count,
114            timeline: PhantomData,
115        }
116    }
117
118    /// The count of ticks, negative for a distance backwards.
119    #[must_use]
120    pub const fn count(self) -> i64 {
121        self.count
122    }
123
124    /// Whether the distance is backwards.
125    #[must_use]
126    pub const fn is_negative(self) -> bool {
127        self.count < 0
128    }
129
130    /// The distance as a [`Duration`], or `None` if it is negative, since a `Duration` cannot be.
131    /// A tick shorter than a nanosecond is truncated towards zero.
132    #[must_use]
133    pub fn to_duration(self) -> Option<Duration> {
134        (!self.is_negative()).then(|| self.abs_duration())
135    }
136
137    /// The distance's length as a [`Duration`], whichever way it points. Truncated towards zero
138    /// below a nanosecond.
139    #[must_use]
140    pub fn abs_duration(self) -> Duration {
141        let per_second = T::ticks_per_second().get();
142        let magnitude = self.count.unsigned_abs();
143        let seconds = magnitude / per_second;
144        let remainder = u128::from(magnitude % per_second);
145        // The remainder is below one second's ticks, so this is below a second's nanoseconds.
146        let nanos = remainder * NANOS_PER_SECOND / u128::from(per_second);
147        Duration::new(
148            seconds,
149            u32::try_from(nanos).expect("below one second of nanoseconds"),
150        )
151    }
152
153    /// `duration` in ticks of `T`, truncated towards zero, or `None` if it does not fit.
154    #[must_use]
155    pub fn from_duration(duration: Duration) -> Option<Self> {
156        let per_second = u128::from(T::ticks_per_second().get());
157        let whole = u128::from(duration.as_secs()).checked_mul(per_second)?;
158        let part = u128::from(duration.subsec_nanos()) * per_second / NANOS_PER_SECOND;
159        i64::try_from(whole.checked_add(part)?).ok().map(Self::new)
160    }
161
162    /// The sum, or `None` on overflow.
163    #[must_use]
164    pub fn checked_add(self, other: Self) -> Option<Self> {
165        self.count.checked_add(other.count).map(Self::new)
166    }
167
168    /// The difference, or `None` on overflow.
169    #[must_use]
170    pub fn checked_sub(self, other: Self) -> Option<Self> {
171        self.count.checked_sub(other.count).map(Self::new)
172    }
173
174    /// The distance the other way, or `None` for the one count that has no opposite.
175    #[must_use]
176    pub fn checked_neg(self) -> Option<Self> {
177        self.count.checked_neg().map(Self::new)
178    }
179}
180
181// By hand rather than derived: a derive would require the timeline marker itself to implement
182// each trait, and a timeline is never made.
183
184impl<T: Timeline> Clone for TimePoint<T> {
185    fn clone(&self) -> Self {
186        *self
187    }
188}
189
190impl<T: Timeline> Copy for TimePoint<T> {}
191
192impl<T: Timeline> PartialEq for TimePoint<T> {
193    fn eq(&self, other: &Self) -> bool {
194        self.ticks == other.ticks
195    }
196}
197
198impl<T: Timeline> Eq for TimePoint<T> {}
199
200impl<T: Timeline> PartialOrd for TimePoint<T> {
201    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
202        Some(self.cmp(other))
203    }
204}
205
206impl<T: Timeline> Ord for TimePoint<T> {
207    fn cmp(&self, other: &Self) -> Ordering {
208        self.ticks.cmp(&other.ticks)
209    }
210}
211
212impl<T: Timeline> Hash for TimePoint<T> {
213    fn hash<H: Hasher>(&self, state: &mut H) {
214        self.ticks.hash(state);
215    }
216}
217
218impl<T: Timeline> fmt::Debug for TimePoint<T> {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        f.debug_struct("TimePoint")
221            .field("timeline", &core::any::type_name::<T>())
222            .field("ticks", &self.ticks)
223            .finish()
224    }
225}
226
227impl<T: Timeline> Clone for Ticks<T> {
228    fn clone(&self) -> Self {
229        *self
230    }
231}
232
233impl<T: Timeline> Copy for Ticks<T> {}
234
235impl<T: Timeline> PartialEq for Ticks<T> {
236    fn eq(&self, other: &Self) -> bool {
237        self.count == other.count
238    }
239}
240
241impl<T: Timeline> Eq for Ticks<T> {}
242
243impl<T: Timeline> PartialOrd for Ticks<T> {
244    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
245        Some(self.cmp(other))
246    }
247}
248
249impl<T: Timeline> Ord for Ticks<T> {
250    fn cmp(&self, other: &Self) -> Ordering {
251        self.count.cmp(&other.count)
252    }
253}
254
255impl<T: Timeline> Hash for Ticks<T> {
256    fn hash<H: Hasher>(&self, state: &mut H) {
257        self.count.hash(state);
258    }
259}
260
261impl<T: Timeline> Default for Ticks<T> {
262    fn default() -> Self {
263        Self::ZERO
264    }
265}
266
267impl<T: Timeline> fmt::Debug for Ticks<T> {
268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269        f.debug_struct("Ticks")
270            .field("timeline", &core::any::type_name::<T>())
271            .field("count", &self.count)
272            .finish()
273    }
274}
275
276// The operators panic on overflow, as `Duration`'s do; the `checked_` methods are the way to
277// avoid that.
278
279impl<T: Timeline> Sub for TimePoint<T> {
280    type Output = Ticks<T>;
281
282    fn sub(self, earlier: Self) -> Ticks<T> {
283        self.checked_since(earlier)
284            .expect("the distance between two time points overflowed")
285    }
286}
287
288impl<T: Timeline> Add<Ticks<T>> for TimePoint<T> {
289    type Output = Self;
290
291    fn add(self, ticks: Ticks<T>) -> Self {
292        self.checked_add(ticks)
293            .expect("a time point moved outside its timeline")
294    }
295}
296
297impl<T: Timeline> Sub<Ticks<T>> for TimePoint<T> {
298    type Output = Self;
299
300    fn sub(self, ticks: Ticks<T>) -> Self {
301        self.checked_sub(ticks)
302            .expect("a time point moved outside its timeline")
303    }
304}
305
306impl<T: Timeline> AddAssign<Ticks<T>> for TimePoint<T> {
307    fn add_assign(&mut self, ticks: Ticks<T>) {
308        *self = *self + ticks;
309    }
310}
311
312impl<T: Timeline> SubAssign<Ticks<T>> for TimePoint<T> {
313    fn sub_assign(&mut self, ticks: Ticks<T>) {
314        *self = *self - ticks;
315    }
316}
317
318impl<T: Timeline> Add for Ticks<T> {
319    type Output = Self;
320
321    fn add(self, other: Self) -> Self {
322        self.checked_add(other).expect("a tick count overflowed")
323    }
324}
325
326impl<T: Timeline> Sub for Ticks<T> {
327    type Output = Self;
328
329    fn sub(self, other: Self) -> Self {
330        self.checked_sub(other).expect("a tick count overflowed")
331    }
332}
333
334impl<T: Timeline> Neg for Ticks<T> {
335    type Output = Self;
336
337    fn neg(self) -> Self {
338        self.checked_neg().expect("a tick count overflowed")
339    }
340}
341
342impl<T: Timeline> AddAssign for Ticks<T> {
343    fn add_assign(&mut self, other: Self) {
344        *self = *self + other;
345    }
346}
347
348impl<T: Timeline> SubAssign for Ticks<T> {
349    fn sub_assign(&mut self, other: Self) {
350        *self = *self - other;
351    }
352}