Skip to main content

minitimer/task/
frequency.rs

1use std::{
2    iter::{Peekable, StepBy},
3    ops::RangeFrom,
4};
5
6use crate::utils::timestamp;
7
8pub(crate) type SecondsState = Peekable<StepBy<RangeFrom<u64>>>;
9const ONE_MINUTE: u64 = 60;
10
11/// Frequency specification for task execution timing.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum FrequencySeconds {
14    /// Execute once after the specified number of seconds.
15    Once(u64),
16    /// Execute repeatedly at the specified interval (in seconds).
17    Repeated(u64),
18    /// Execute a specific number of times at the specified interval.
19    CountDown(u64, u64),
20}
21
22impl FrequencySeconds {
23    /// Returns the interval in seconds for this frequency.
24    ///
25    /// # Returns
26    /// The interval between executions in seconds.
27    pub(crate) fn interval(&self) -> u64 {
28        match self {
29            Self::Once(seconds) => *seconds,
30            Self::Repeated(seconds) => *seconds,
31            Self::CountDown(_, seconds) => *seconds,
32        }
33    }
34}
35
36impl Default for FrequencySeconds {
37    fn default() -> FrequencySeconds {
38        FrequencySeconds::Once(ONE_MINUTE)
39    }
40}
41
42/// Internal state representation of task frequency.
43///
44/// This is used to track the next execution time for tasks.
45#[derive(Clone)]
46#[allow(dead_code)]
47pub(crate) enum FrequencyState {
48    /// Repeated execution at a fixed interval.
49    SecondsRepeated(SecondsState),
50    /// Countdown execution with a limited number of repetitions.
51    SecondsCountDown(u64, SecondsState),
52}
53
54impl From<FrequencySeconds> for FrequencyState {
55    fn from(frequency: FrequencySeconds) -> Self {
56        match frequency {
57            FrequencySeconds::Once(seconds) => {
58                assert!(seconds > 0, "once frequency must be greater than 0");
59                let state: SecondsState = ((timestamp() + seconds)..)
60                    .step_by(seconds as usize)
61                    .peekable();
62                FrequencyState::SecondsRepeated(state)
63            }
64            FrequencySeconds::Repeated(seconds) => {
65                assert!(seconds > 0, "repeated frequency must be greater than 0");
66                let state: SecondsState = ((timestamp() + seconds)..)
67                    .step_by(seconds as usize)
68                    .peekable();
69                FrequencyState::SecondsRepeated(state)
70            }
71            FrequencySeconds::CountDown(count_down, seconds) => {
72                assert!(seconds > 0, "countdown initial must be greater than 0");
73                let state: SecondsState = (timestamp() + seconds..)
74                    .step_by(count_down as usize)
75                    .peekable();
76                FrequencyState::SecondsCountDown(count_down, state)
77            }
78        }
79    }
80}
81
82impl FrequencyState {
83    #[allow(dead_code)]
84    /// Peeks at the next alarm timestamp without advancing the state.
85    ///
86    /// # Returns
87    /// The next timestamp when the task should execute, or None if no more executions.
88    pub(crate) fn peek_alarm_timestamp(&mut self) -> Option<u64> {
89        match self {
90            Self::SecondsRepeated(state) => state.peek().copied(),
91            Self::SecondsCountDown(_, state) => state.peek().copied(),
92        }
93    }
94
95    /// Gets the next alarm timestamp and advances the state.
96    ///
97    /// # Returns
98    /// The next timestamp when the task should execute, or None if no more executions.
99    pub(crate) fn next_alarm_timestamp(&mut self) -> Option<u64> {
100        match self {
101            Self::SecondsRepeated(state) => state.next(),
102            Self::SecondsCountDown(_, state) => state.next(),
103        }
104    }
105
106    #[allow(dead_code)]
107    /// Decrements the countdown for CountDown frequency types.
108    pub(crate) fn down_count(&mut self) {
109        if let Self::SecondsCountDown(count, _) = self {
110            *count = count.saturating_sub(1);
111        }
112    }
113
114    /// Resets the frequency state from a given timestamp.
115    ///
116    /// This is used when accelerating a task to restart the frequency
117    /// sequence from a new base time.
118    ///
119    /// # Arguments
120    /// * `base_timestamp` - The new base timestamp to start the sequence from
121    /// * `interval` - The interval in seconds between executions
122    pub(crate) fn reset_from_timestamp(&mut self, base_timestamp: u64, interval: u64) {
123        let new_state: SecondsState = ((base_timestamp + interval)..)
124            .step_by(interval as usize)
125            .peekable();
126
127        match self {
128            Self::SecondsRepeated(_) => {
129                *self = Self::SecondsRepeated(new_state);
130            }
131            Self::SecondsCountDown(count, _) => {
132                *self = Self::SecondsCountDown(*count, new_state);
133            }
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_frequency_state_from_once() {
144        let freq = FrequencySeconds::Once(10);
145        let mut state = FrequencyState::from(freq);
146
147        // For Once, we should get a timestamp in the future
148        let now = crate::utils::timestamp();
149        let alarm = state.peek_alarm_timestamp().unwrap();
150        assert!(alarm >= now + 10);
151
152        // Next call should give the same timestamp (peek doesn't advance)
153        let alarm2 = state.peek_alarm_timestamp().unwrap();
154        assert_eq!(alarm, alarm2);
155
156        // next_alarm_timestamp should advance the state
157        let alarm3 = state.next_alarm_timestamp().unwrap();
158        assert_eq!(alarm, alarm3);
159    }
160
161    #[test]
162    fn test_frequency_state_from_repeated() {
163        let freq = FrequencySeconds::Repeated(5);
164        let mut state = FrequencyState::from(freq);
165
166        let now = crate::utils::timestamp();
167        // For Repeated, we should get a sequence starting from 0 with step 5
168        let alarm1 = state.next_alarm_timestamp().unwrap();
169        assert_eq!(alarm1, now + 5);
170
171        let alarm2 = state.next_alarm_timestamp().unwrap();
172        assert_eq!(alarm2, now + 10);
173
174        let alarm3 = state.next_alarm_timestamp().unwrap();
175        assert_eq!(alarm3, now + 15);
176    }
177
178    #[test]
179    fn test_frequency_state_from_countdown() {
180        // Note: CountDown implementation creates a sequence starting from 'seconds'
181        // with step 'count_down', and the count is handled separately
182        let freq = FrequencySeconds::CountDown(2, 5); // count_down=2, seconds=5
183        let state = FrequencyState::from(freq);
184
185        // Check that it's the CountDown variant with correct count
186        match state {
187            FrequencyState::SecondsCountDown(count, _) => assert_eq!(count, 2),
188            _ => panic!("Expected SecondsCountDown variant"),
189        }
190    }
191
192    #[test]
193    fn test_peek_alarm_timestamp() {
194        let freq = FrequencySeconds::Repeated(10);
195        let mut state = FrequencyState::from(freq);
196
197        // Peek should not advance the state
198        let peek1 = state.peek_alarm_timestamp().unwrap();
199        let peek2 = state.peek_alarm_timestamp().unwrap();
200        assert_eq!(peek1, peek2);
201
202        // But next should advance
203        let next1 = state.next_alarm_timestamp().unwrap();
204        assert_eq!(peek1, next1);
205
206        let peek3 = state.peek_alarm_timestamp().unwrap();
207        assert_ne!(peek1, peek3);
208    }
209
210    #[test]
211    fn test_reset_from_timestamp_repeated() {
212        let freq = FrequencySeconds::Repeated(10);
213        let mut state = FrequencyState::from(freq);
214
215        // Advance the state a few times
216        let _ = state.next_alarm_timestamp().unwrap();
217        let _ = state.next_alarm_timestamp().unwrap();
218
219        // Reset from a specific timestamp
220        let reset_base = 1000;
221        state.reset_from_timestamp(reset_base, 10);
222
223        // After reset, the next alarm should be at reset_base + interval
224        let next = state.peek_alarm_timestamp().unwrap();
225        assert_eq!(next, reset_base + 10);
226
227        // Subsequent alarms should follow the new interval
228        let next2 = state.next_alarm_timestamp().unwrap();
229        assert_eq!(next2, reset_base + 10);
230
231        let next3 = state.next_alarm_timestamp().unwrap();
232        assert_eq!(next3, reset_base + 20);
233    }
234
235    #[test]
236    fn test_reset_from_timestamp_countdown() {
237        let freq = FrequencySeconds::CountDown(5, 10);
238        let mut state = FrequencyState::from(freq);
239
240        // Reset from a specific timestamp
241        let reset_base = 2000;
242        state.reset_from_timestamp(reset_base, 10);
243
244        // After reset, the next alarm should be at reset_base + interval
245        let next = state.peek_alarm_timestamp().unwrap();
246        assert_eq!(next, reset_base + 10);
247
248        // Count should be preserved
249        match state {
250            FrequencyState::SecondsCountDown(count, _) => assert_eq!(count, 5),
251            _ => panic!("Expected SecondsCountDown variant"),
252        }
253    }
254
255    #[test]
256    fn test_frequency_seconds_interval() {
257        assert_eq!(FrequencySeconds::Once(30).interval(), 30);
258        assert_eq!(FrequencySeconds::Repeated(60).interval(), 60);
259        assert_eq!(FrequencySeconds::CountDown(3, 15).interval(), 15);
260    }
261}