Skip to main content

use_simulation_clock/
lib.rs

1#![forbid(unsafe_code)]
2//! Deterministic simulation clock helpers.
3//!
4//! The crate keeps time in integer ticks and derives elapsed time from a
5//! finite `f64` tick duration.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_simulation_clock::SimulationClock;
11//!
12//! let mut clock = SimulationClock::new(0.5).unwrap();
13//! assert_eq!(clock.advance().unwrap(), 0.5);
14//! assert_eq!(clock.advance_by(3).unwrap(), 2.0);
15//! assert_eq!(clock.tick(), 4);
16//! ```
17
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct SimulationClock {
20    tick: usize,
21    tick_duration: f64,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum SimulationClockError {
26    InvalidTickDuration,
27    TickOverflow,
28    NonFiniteElapsed,
29}
30
31impl SimulationClock {
32    pub fn new(tick_duration: f64) -> Result<Self, SimulationClockError> {
33        Self::at_tick(0, tick_duration)
34    }
35
36    pub fn at_tick(tick: usize, tick_duration: f64) -> Result<Self, SimulationClockError> {
37        if !tick_duration.is_finite() || tick_duration <= 0.0 {
38            return Err(SimulationClockError::InvalidTickDuration);
39        }
40
41        let elapsed = tick as f64 * tick_duration;
42        if !elapsed.is_finite() {
43            return Err(SimulationClockError::NonFiniteElapsed);
44        }
45
46        Ok(Self {
47            tick,
48            tick_duration,
49        })
50    }
51
52    pub fn tick(&self) -> usize {
53        self.tick
54    }
55
56    pub fn tick_duration(&self) -> f64 {
57        self.tick_duration
58    }
59
60    pub fn elapsed(&self) -> f64 {
61        self.tick as f64 * self.tick_duration
62    }
63
64    pub fn advance(&mut self) -> Result<f64, SimulationClockError> {
65        self.advance_by(1)
66    }
67
68    pub fn advance_by(&mut self, steps: usize) -> Result<f64, SimulationClockError> {
69        self.tick = self
70            .tick
71            .checked_add(steps)
72            .ok_or(SimulationClockError::TickOverflow)?;
73
74        let elapsed = self.elapsed();
75        if !elapsed.is_finite() {
76            return Err(SimulationClockError::NonFiniteElapsed);
77        }
78
79        Ok(elapsed)
80    }
81}
82
83pub fn elapsed_for(tick_duration: f64, ticks: usize) -> Option<f64> {
84    if !tick_duration.is_finite() || tick_duration <= 0.0 {
85        return None;
86    }
87
88    let elapsed = tick_duration * ticks as f64;
89    elapsed.is_finite().then_some(elapsed)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::{SimulationClock, SimulationClockError, elapsed_for};
95
96    #[test]
97    fn advances_clock_in_ticks() {
98        let mut clock = SimulationClock::new(0.5).unwrap();
99
100        assert_eq!(clock.elapsed(), 0.0);
101        assert_eq!(clock.advance().unwrap(), 0.5);
102        assert_eq!(clock.advance_by(3).unwrap(), 2.0);
103        assert_eq!(clock.tick(), 4);
104        assert_eq!(clock.tick_duration(), 0.5);
105    }
106
107    #[test]
108    fn can_start_at_existing_tick() {
109        let clock = SimulationClock::at_tick(3, 0.25).unwrap();
110
111        assert_eq!(clock.tick(), 3);
112        assert_eq!(clock.elapsed(), 0.75);
113        assert_eq!(elapsed_for(0.25, 3), Some(0.75));
114    }
115
116    #[test]
117    fn rejects_invalid_duration() {
118        assert_eq!(
119            SimulationClock::new(0.0),
120            Err(SimulationClockError::InvalidTickDuration)
121        );
122        assert_eq!(elapsed_for(f64::NAN, 2), None);
123    }
124
125    #[test]
126    fn rejects_non_finite_elapsed_time() {
127        assert_eq!(
128            SimulationClock::at_tick(usize::MAX, f64::MAX),
129            Err(SimulationClockError::NonFiniteElapsed)
130        );
131    }
132}