use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy)]
pub struct PeriodicSchedule {
next_due: Instant,
interval: Duration,
}
impl PeriodicSchedule {
pub fn new(interval: Duration, now: Instant) -> Self {
Self {
next_due: now,
interval,
}
}
pub fn is_due(&self, now: Instant) -> bool {
now >= self.next_due
}
pub fn remaining(&self, now: Instant) -> Duration {
self.next_due.saturating_duration_since(now)
}
pub fn resume_after_pause(&mut self, paused_for: Duration, now: Instant) {
self.next_due += paused_for;
self.resync_if_behind(now);
}
pub fn advance_after_tick(&mut self, now: Instant) {
self.next_due += self.interval;
self.resync_if_behind(now);
}
fn resync_if_behind(&mut self, now: Instant) {
if self.next_due < now {
self.next_due = now + self.interval;
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ActiveTimeline {
anchor: Instant,
}
impl ActiveTimeline {
pub fn new(now: Instant) -> Self {
Self { anchor: now }
}
pub fn account_pause(&mut self, paused_for: Duration) {
self.anchor += paused_for;
}
pub fn elapsed(&self, now: Instant) -> Duration {
now.saturating_duration_since(self.anchor)
}
}
#[cfg(test)]
mod active_timeline_tests {
use super::*;
#[test]
fn elapsed_excludes_time_spent_paused() {
let t0 = Instant::now();
let mut timeline = ActiveTimeline::new(t0);
let before_pause = t0 + Duration::from_millis(200);
assert_eq!(timeline.elapsed(before_pause), Duration::from_millis(200));
timeline.account_pause(Duration::from_millis(500));
let after_pause = before_pause + Duration::from_millis(500);
assert_eq!(
timeline.elapsed(after_pause),
Duration::from_millis(200),
"the 500ms spent paused must not count as active time"
);
}
#[test]
fn multiple_pauses_accumulate() {
let t0 = Instant::now();
let mut timeline = ActiveTimeline::new(t0);
timeline.account_pause(Duration::from_millis(100));
timeline.account_pause(Duration::from_millis(250));
assert_eq!(
timeline.elapsed(t0 + Duration::from_secs(1)),
Duration::from_millis(650),
"1s of real time minus 350ms total paused"
);
}
#[test]
fn elapsed_saturates_instead_of_underflowing() {
let t0 = Instant::now();
let mut timeline = ActiveTimeline::new(t0);
timeline.account_pause(Duration::from_secs(10));
assert_eq!(timeline.elapsed(t0), Duration::ZERO);
}
}
#[cfg(test)]
mod tests {
use super::*;
const INTERVAL: Duration = Duration::from_millis(100);
#[test]
fn resume_after_pause_preserves_phase_when_the_shift_is_enough() {
let t0 = Instant::now();
let mut schedule = PeriodicSchedule::new(INTERVAL, t0);
schedule.advance_after_tick(t0); let resumed_at = t0 + Duration::from_millis(590);
schedule.resume_after_pause(Duration::from_millis(500), resumed_at);
assert_eq!(
schedule.remaining(resumed_at),
Duration::from_millis(10),
"expected the pre-pause phase to be preserved, not reset to resume + a full interval"
);
}
#[test]
fn resume_after_pause_resyncs_when_the_shift_still_lands_in_the_past() {
let t0 = Instant::now();
let mut schedule = PeriodicSchedule::new(INTERVAL, t0);
let now = t0 + Duration::from_secs(10);
schedule.resume_after_pause(Duration::from_millis(1), now);
assert_eq!(
schedule.remaining(now),
INTERVAL,
"expected a resync to exactly one interval from now, not a burst \
of back-to-back catch-up ticks"
);
}
#[test]
fn advance_after_tick_holds_steady_cadence_when_ticks_keep_up() {
let t0 = Instant::now();
let mut schedule = PeriodicSchedule::new(INTERVAL, t0);
schedule.advance_after_tick(t0);
assert_eq!(schedule.remaining(t0), INTERVAL);
schedule.advance_after_tick(t0 + INTERVAL);
assert_eq!(schedule.remaining(t0 + INTERVAL), INTERVAL);
}
#[test]
fn advance_after_tick_drops_missed_ticks_instead_of_bursting() {
let t0 = Instant::now();
let mut schedule = PeriodicSchedule::new(INTERVAL, t0);
let slow_tick_done = t0 + INTERVAL * 15;
schedule.advance_after_tick(slow_tick_done);
assert_eq!(
schedule.remaining(slow_tick_done),
INTERVAL,
"expected a resync to one interval from now, not 14 missed \
ticks all firing back-to-back"
);
}
#[test]
fn is_due_and_remaining_agree() {
let t0 = Instant::now();
let mut schedule = PeriodicSchedule::new(INTERVAL, t0);
assert!(schedule.is_due(t0), "the first tick is due immediately");
schedule.advance_after_tick(t0); assert!(!schedule.is_due(t0 + INTERVAL / 2));
assert_eq!(schedule.remaining(t0 + INTERVAL / 2), INTERVAL / 2);
assert!(schedule.is_due(t0 + INTERVAL));
assert_eq!(schedule.remaining(t0 + INTERVAL), Duration::ZERO);
}
}