#[derive(Clone, Debug, PartialEq)]
pub struct Timer {
period: f32,
elapsed: f32,
repeating: bool,
}
impl Timer {
pub fn every(seconds: f32) -> Timer {
Timer {
period: sanitize(seconds),
elapsed: 0.0,
repeating: true,
}
}
pub fn once(seconds: f32) -> Timer {
Timer {
period: sanitize(seconds),
elapsed: 0.0,
repeating: false,
}
}
pub fn tick(&mut self, dt: f32) -> bool {
self.ticks(dt) > 0
}
pub fn ticks(&mut self, dt: f32) -> u32 {
if self.finished() || dt.is_nan() || dt <= 0.0 || !dt.is_finite() {
return 0;
}
self.elapsed += dt;
if !self.repeating {
if self.elapsed >= self.period {
self.elapsed = self.period;
return 1;
}
return 0;
}
let whole = (self.elapsed / self.period).floor().min(MAX_TICKS as f32) as u32;
self.elapsed = (self.elapsed - whole as f32 * self.period).clamp(0.0, self.period);
whole
}
pub fn finished(&self) -> bool {
!self.repeating && self.elapsed >= self.period
}
pub fn remaining(&self) -> f32 {
(self.period - self.elapsed).max(0.0)
}
pub fn fraction(&self) -> f32 {
(self.elapsed / self.period).clamp(0.0, 1.0)
}
pub fn set_seconds(&mut self, seconds: f32) {
self.period = sanitize(seconds);
}
pub fn reset(&mut self) {
self.elapsed = 0.0;
}
}
fn sanitize(seconds: f32) -> f32 {
if seconds.is_finite() && seconds > 0.0 {
seconds.max(MIN_PERIOD)
} else {
MIN_PERIOD
}
}
const MIN_PERIOD: f32 = 0.001;
const MAX_TICKS: u32 = 1000;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn once_completes_on_the_tick_that_reaches_its_time() {
let mut t = Timer::once(0.5);
assert!(!t.tick(0.25));
assert!(t.tick(0.25));
}
#[test]
fn every_keeps_the_leftover_time_when_ticked_by_long_steps() {
let mut t = Timer::every(0.1);
assert_eq!(t.ticks(0.25), 2);
assert!(
(t.remaining() - 0.05).abs() < 1e-4,
"remaining {}",
t.remaining()
);
assert_eq!(t.ticks(0.25), 3);
}
#[test]
fn tick_is_true_once_per_call_while_ticks_counts_every_period() {
let mut t = Timer::every(0.1);
assert!(t.tick(0.35), "a long step completes the timer");
assert_eq!(t.ticks(0.0), 0);
let mut u = Timer::every(0.1);
assert_eq!(u.ticks(0.35), 3);
}
#[test]
fn a_repeating_timer_never_reports_finished() {
let mut t = Timer::every(0.1);
t.tick(10.0);
assert!(!t.finished());
}
#[test]
fn long_run_has_no_drift() {
let mut t = Timer::every(0.1);
let mut total = 0;
for _ in 0..1_000 {
total += t.ticks(0.01);
}
assert!((99..=100).contains(&total), "total {total}");
}
#[test]
fn set_seconds_changes_the_period_and_keeps_progress() {
let mut t = Timer::once(1.0);
t.tick(0.25);
t.set_seconds(2.0);
assert!(
(t.fraction() - 0.125).abs() < 1e-6,
"fraction {}",
t.fraction()
);
t.set_seconds(0.25);
assert!(t.finished(), "progress past the new period finishes it");
}
#[test]
fn degenerate_durations_and_steps_never_hang_or_panic() {
for seconds in [0.0, -1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
let mut t = Timer::every(seconds);
assert!(t.ticks(1.0) <= 1000);
assert!(t.ticks(f32::MAX) <= 1000);
assert!(t.ticks(f32::NAN) == 0);
assert!(t.ticks(-5.0) == 0);
assert!(t.remaining() >= 0.0);
assert!(t.fraction() >= 0.0 && t.fraction() <= 1.0);
let mut o = Timer::once(seconds);
assert!(o.tick(f32::MAX));
assert!(o.finished());
}
}
#[test]
fn once_stays_finished_until_reset() {
let mut t = Timer::once(0.2);
t.tick(1.0);
assert!(t.finished());
assert!(
!t.tick(1.0),
"a finished once-timer does not complete again"
);
t.reset();
assert!(!t.finished());
assert!(t.tick(0.5));
}
}