groundhog/
lib.rs

1//! `groundhog` - A rolling timer
2//!
3//! Sometimes you just want a simple rolling timer.
4//!
5//! Make sure you poll it often enough.
6
7#![cfg_attr(
8    not(any(test, feature = "std")),
9    no_std
10)]
11
12use core::ops::Div;
13use sealed::{Promote, RollingSince};
14
15#[cfg(any(test, doctest, feature = "std"))]
16pub mod std_timer;
17
18pub trait RollingTimer {
19    type Tick: RollingSince + Promote + Div<Output = Self::Tick>;
20
21    const TICKS_PER_SECOND: Self::Tick;
22
23    /// Get the current tick
24    fn get_ticks(&self) -> Self::Tick;
25
26    /// Determine if the timer is initialized
27    fn is_initialized(&self) -> bool;
28
29    /// Get the number of ticks since the other measurement
30    ///
31    /// Make sure the old value isn't too stale.
32    ///
33    /// NOTE: if the timer is not initialized, the timer may
34    /// return a tick value of `0` until it has been initialized.
35    fn ticks_since(&self, rhs: Self::Tick) -> Self::Tick {
36        self.get_ticks().since(rhs)
37    }
38
39    /// Get the number of whole seconds since the other measurement
40    ///
41    /// Make sure the old value isn't too stale
42    fn seconds_since(&self, rhs: Self::Tick) -> Self::Tick {
43        self.ticks_since(rhs) / Self::TICKS_PER_SECOND
44    }
45
46    /// Get the number of whole milliseconds since the other measurement
47    ///
48    /// Make sure the old value isn't too stale
49    ///
50    /// If the number of milliseconds is larger than Self::Tick::max(),
51    /// then it will saturate at the max value
52    fn millis_since(&self, rhs: Self::Tick) -> Self::Tick {
53        let delta_tick = self.ticks_since(rhs);
54        delta_tick.mul_then_div(
55            <Self::Tick as RollingSince>::MILLIS_PER_SECOND,
56            Self::TICKS_PER_SECOND,
57        )
58    }
59
60    /// Get the number of whole microseconds since the other measurement
61    ///
62    /// Make sure the old value isn't too stale
63    ///
64    /// If the number of microseconds is larger than Self::Tick::max(),
65    /// then it will saturate at the max value
66    fn micros_since(&self, rhs: Self::Tick) -> Self::Tick {
67        let delta_tick = self.ticks_since(rhs);
68        delta_tick.mul_then_div(
69            <Self::Tick as RollingSince>::MICROS_PER_SECOND,
70            Self::TICKS_PER_SECOND,
71        )
72    }
73}
74
75mod sealed {
76    use core::convert::TryInto;
77    use core::ops::{Div, Mul};
78
79    pub trait Promote: Sized + Copy {
80        type NextSize: From<Self>
81            + Ord
82            + TryInto<Self>
83            + Mul<Output = Self::NextSize>
84            + Div<Output = Self::NextSize>;
85        const MAX_VAL: Self::NextSize;
86
87        fn promote(&self) -> Self::NextSize {
88            (*self).into()
89        }
90        fn saturate_demote(other: Self::NextSize) -> Self {
91            match Self::MAX_VAL.min(other).try_into() {
92                Ok(t) => t,
93                Err(_) => unsafe { core::hint::unreachable_unchecked() },
94            }
95        }
96
97        fn mul_then_div(&self, mul: Self, div: Self) -> Self {
98            Self::saturate_demote((self.promote() * mul.promote()) / div.promote())
99        }
100    }
101
102    pub trait RollingSince {
103        const MILLIS_PER_SECOND: Self;
104        const MICROS_PER_SECOND: Self;
105        fn since(&self, other: Self) -> Self;
106    }
107
108    impl Promote for u32 {
109        type NextSize = u64;
110        const MAX_VAL: u64 = 0xFFFF_FFFF;
111    }
112
113    #[cfg(feature = "u128")]
114    impl Promote for u64 {
115        type NextSize = u128;
116        const MAX_VAL: u128 = 0xFFFF_FFFF_FFFF_FFFF;
117    }
118
119    impl RollingSince for u32 {
120        const MILLIS_PER_SECOND: u32 = 1_000;
121        const MICROS_PER_SECOND: u32 = 1_000_000;
122        fn since(&self, other: u32) -> u32 {
123            self.wrapping_sub(other)
124        }
125    }
126
127    #[cfg(feature = "u128")]
128    impl RollingSince for u64 {
129        const MILLIS_PER_SECOND: u64 = 1_000;
130        const MICROS_PER_SECOND: u64 = 1_000_000;
131        fn since(&self, other: u64) -> u64 {
132            self.wrapping_sub(other)
133        }
134    }
135}
136
137#[cfg(test)]
138mod test {
139    use super::*;
140    use core::sync::atomic::{AtomicU32, Ordering};
141
142    struct TestTimer(&'static AtomicU32);
143    impl RollingTimer for TestTimer {
144        type Tick = u32;
145        const TICKS_PER_SECOND: Self::Tick = 10;
146
147        fn is_initialized(&self) -> bool {
148            true
149        }
150
151        fn get_ticks(&self) -> u32 {
152            self.0.load(Ordering::SeqCst)
153        }
154    }
155
156    #[test]
157    fn simple_wrap() {
158        assert_eq!(0x10u32.since(0xFFFFFFF0), 0x20);
159        assert_eq!(0xFFFFFFF0u32.since(0x10), 0xFFFFFFE0);
160    }
161
162    #[test]
163    fn timer_test() {
164        static TIMER: AtomicU32 = AtomicU32::new(20);
165        let timer = TestTimer(&TIMER);
166
167        assert_eq!(timer.ticks_since(0), 20);
168        assert_eq!(timer.seconds_since(0), 2);
169        assert_eq!(timer.millis_since(0), 2000);
170        assert_eq!(timer.micros_since(0), 2000000);
171
172        TIMER.store(0xFFFF_FFFF, Ordering::SeqCst);
173
174        assert_eq!(timer.ticks_since(0), 0xFFFF_FFFF);
175        assert_eq!(timer.seconds_since(0), 0xFFFF_FFFF / 10);
176
177        // Out of range
178        assert_eq!(timer.millis_since(0), 0xFFFF_FFFF);
179        assert_eq!(timer.micros_since(0), 0xFFFF_FFFF);
180
181        TIMER.store(0x4000_0000, Ordering::SeqCst);
182
183        assert_eq!(timer.ticks_since(0xC000_0000), 0x8000_0000);
184        assert_eq!(timer.seconds_since(0xC000_0000), 0x8000_0000 / 10);
185
186        // Out of range
187        assert_eq!(timer.millis_since(0xC000_0000), 0xFFFF_FFFF);
188        assert_eq!(timer.micros_since(0xC000_0000), 0xFFFF_FFFF);
189    }
190}