#![forbid(unsafe_code)]
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayFormat {
Compact,
Clock,
}
#[derive(Debug, Clone)]
pub struct Stopwatch {
elapsed: Duration,
interval: Duration,
running: bool,
format: DisplayFormat,
}
impl Default for Stopwatch {
fn default() -> Self {
Self::new()
}
}
impl Stopwatch {
#[must_use]
pub fn new() -> Self {
Self::with_interval(Duration::from_secs(1))
}
#[must_use]
pub fn with_interval(interval: Duration) -> Self {
Self {
elapsed: Duration::ZERO,
interval,
running: false,
format: DisplayFormat::Compact,
}
}
#[must_use]
pub fn format(mut self, format: DisplayFormat) -> Self {
self.format = format;
self
}
#[must_use]
pub fn running(&self) -> bool {
self.running
}
#[must_use]
pub fn elapsed(&self) -> Duration {
self.elapsed
}
#[must_use]
pub fn interval(&self) -> Duration {
self.interval
}
pub fn start(&mut self) {
self.running = true;
}
pub fn stop(&mut self) {
self.running = false;
}
pub fn toggle(&mut self) {
self.running = !self.running;
}
pub fn reset(&mut self) {
self.elapsed = Duration::ZERO;
}
pub fn tick_once(&mut self) -> bool {
if self.running {
self.elapsed += self.interval;
true
} else {
false
}
}
pub fn tick(&mut self, delta: Duration) -> bool {
if self.running {
self.elapsed += delta;
true
} else {
false
}
}
#[must_use]
pub fn view(&self) -> String {
match self.format {
DisplayFormat::Compact => format_compact(self.elapsed),
DisplayFormat::Clock => format_clock(self.elapsed),
}
}
}
fn format_compact(d: Duration) -> String {
let total_nanos = d.as_nanos();
if total_nanos == 0 {
return "0s".to_string();
}
let total_secs = d.as_secs();
let subsec_nanos = d.subsec_nanos();
if total_secs == 0 {
let micros = d.as_micros();
if micros >= 1000 {
let millis = d.as_millis();
let remainder_micros = micros % 1000;
if remainder_micros == 0 {
return format!("{millis}ms");
}
let decimal = format!("{:06}", total_nanos % 1_000_000);
let trimmed = decimal.trim_end_matches('0');
if trimmed.is_empty() {
return format!("{millis}ms");
}
return format!("{millis}.{trimmed}ms");
} else if micros >= 1 {
let nanos = total_nanos % 1000;
if nanos == 0 {
return format!("{micros}\u{00B5}s");
}
let decimal = format!("{nanos:03}");
let trimmed = decimal.trim_end_matches('0');
return format!("{micros}.{trimmed}\u{00B5}s");
} else {
return format!("{total_nanos}ns");
}
}
let hours = total_secs / 3600;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
let subsec_str = if subsec_nanos > 0 {
let decimal = format!("{subsec_nanos:09}");
let trimmed = decimal.trim_end_matches('0');
if trimmed.is_empty() {
String::new()
} else {
format!(".{trimmed}")
}
} else {
String::new()
};
if hours > 0 {
format!("{hours}h{minutes}m{seconds}{subsec_str}s")
} else if minutes > 0 {
format!("{minutes}m{seconds}{subsec_str}s")
} else {
format!("{seconds}{subsec_str}s")
}
}
fn format_clock(d: Duration) -> String {
let total_secs = d.as_secs();
let hours = total_secs / 3600;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
if hours > 0 {
format!("{hours}:{minutes:02}:{seconds:02}")
} else {
format!("{minutes:02}:{seconds:02}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_stopwatch() {
let sw = Stopwatch::new();
assert_eq!(sw.elapsed(), Duration::ZERO);
assert!(!sw.running());
assert_eq!(sw.interval(), Duration::from_secs(1));
}
#[test]
fn with_interval() {
let sw = Stopwatch::with_interval(Duration::from_millis(100));
assert_eq!(sw.interval(), Duration::from_millis(100));
}
#[test]
fn start_stop() {
let mut sw = Stopwatch::new();
assert!(!sw.running());
sw.start();
assert!(sw.running());
sw.stop();
assert!(!sw.running());
}
#[test]
fn toggle() {
let mut sw = Stopwatch::new();
sw.toggle();
assert!(sw.running());
sw.toggle();
assert!(!sw.running());
}
#[test]
fn tick_once_when_running() {
let mut sw = Stopwatch::new();
sw.start();
assert!(sw.tick_once());
assert_eq!(sw.elapsed(), Duration::from_secs(1));
}
#[test]
fn tick_once_when_stopped() {
let mut sw = Stopwatch::new();
assert!(!sw.tick_once());
assert_eq!(sw.elapsed(), Duration::ZERO);
}
#[test]
fn tick_arbitrary_duration() {
let mut sw = Stopwatch::new();
sw.start();
sw.tick(Duration::from_millis(500));
assert_eq!(sw.elapsed(), Duration::from_millis(500));
sw.tick(Duration::from_millis(500));
assert_eq!(sw.elapsed(), Duration::from_secs(1));
}
#[test]
fn tick_when_stopped_is_noop() {
let mut sw = Stopwatch::new();
assert!(!sw.tick(Duration::from_secs(5)));
assert_eq!(sw.elapsed(), Duration::ZERO);
}
#[test]
fn reset() {
let mut sw = Stopwatch::new();
sw.start();
sw.tick(Duration::from_secs(100));
sw.reset();
assert_eq!(sw.elapsed(), Duration::ZERO);
assert!(sw.running()); }
#[test]
fn multiple_ticks() {
let mut sw = Stopwatch::with_interval(Duration::from_secs(1));
sw.start();
for _ in 0..10 {
sw.tick_once();
}
assert_eq!(sw.elapsed(), Duration::from_secs(10));
}
#[test]
fn compact_zero() {
assert_eq!(format_compact(Duration::ZERO), "0s");
}
#[test]
fn compact_seconds() {
assert_eq!(format_compact(Duration::from_secs(45)), "45s");
}
#[test]
fn compact_seconds_with_millis() {
assert_eq!(format_compact(Duration::from_millis(5500)), "5.5s");
assert_eq!(format_compact(Duration::from_millis(5050)), "5.05s");
assert_eq!(format_compact(Duration::from_millis(5001)), "5.001s");
}
#[test]
fn compact_minutes() {
assert_eq!(format_compact(Duration::from_secs(60)), "1m0s");
assert_eq!(format_compact(Duration::from_secs(90)), "1m30s");
assert_eq!(format_compact(Duration::from_secs(125)), "2m5s");
}
#[test]
fn compact_minutes_with_millis() {
assert_eq!(format_compact(Duration::from_millis(90500)), "1m30.5s");
}
#[test]
fn compact_hours() {
assert_eq!(format_compact(Duration::from_secs(3600)), "1h0m0s");
assert_eq!(format_compact(Duration::from_secs(3665)), "1h1m5s");
assert_eq!(
format_compact(Duration::from_secs(100 * 3600 + 30 * 60 + 15)),
"100h30m15s"
);
}
#[test]
fn compact_hours_with_millis() {
assert_eq!(format_compact(Duration::from_millis(3_600_500)), "1h0m0.5s");
}
#[test]
fn compact_milliseconds() {
assert_eq!(format_compact(Duration::from_millis(100)), "100ms");
assert_eq!(format_compact(Duration::from_millis(1)), "1ms");
assert_eq!(format_compact(Duration::from_millis(999)), "999ms");
}
#[test]
fn compact_microseconds() {
assert_eq!(format_compact(Duration::from_micros(500)), "500\u{00B5}s");
}
#[test]
fn compact_nanoseconds() {
assert_eq!(format_compact(Duration::from_nanos(123)), "123ns");
}
#[test]
fn clock_zero() {
assert_eq!(format_clock(Duration::ZERO), "00:00");
}
#[test]
fn clock_seconds() {
assert_eq!(format_clock(Duration::from_secs(45)), "00:45");
}
#[test]
fn clock_minutes() {
assert_eq!(format_clock(Duration::from_secs(90)), "01:30");
assert_eq!(format_clock(Duration::from_secs(125)), "02:05");
}
#[test]
fn clock_hours() {
assert_eq!(format_clock(Duration::from_secs(3600)), "1:00:00");
assert_eq!(format_clock(Duration::from_secs(3665)), "1:01:05");
}
#[test]
fn clock_ignores_subseconds() {
assert_eq!(format_clock(Duration::from_millis(5500)), "00:05");
}
#[test]
fn view_default_format() {
let mut sw = Stopwatch::new();
sw.start();
sw.tick(Duration::from_secs(125));
assert_eq!(sw.view(), "2m5s");
}
#[test]
fn view_clock_format() {
let mut sw = Stopwatch::new().format(DisplayFormat::Clock);
sw.start();
sw.tick(Duration::from_secs(125));
assert_eq!(sw.view(), "02:05");
}
#[test]
fn default_impl() {
let sw = Stopwatch::default();
assert_eq!(sw.elapsed(), Duration::ZERO);
assert!(!sw.running());
}
#[test]
fn format_builder_returns_self() {
let sw = Stopwatch::new().format(DisplayFormat::Clock);
assert_eq!(sw.view(), "00:00");
}
#[test]
fn interval_accessor_after_new() {
let sw = Stopwatch::new();
assert_eq!(sw.interval(), Duration::from_secs(1));
}
#[test]
fn elapsed_accessor_after_tick() {
let mut sw = Stopwatch::new();
sw.start();
sw.tick(Duration::from_secs(42));
assert_eq!(sw.elapsed(), Duration::from_secs(42));
}
#[test]
fn start_idempotent() {
let mut sw = Stopwatch::new();
sw.start();
sw.start(); assert!(sw.running());
sw.tick(Duration::from_secs(1));
assert_eq!(sw.elapsed(), Duration::from_secs(1));
}
#[test]
fn stop_idempotent() {
let mut sw = Stopwatch::new();
sw.stop(); assert!(!sw.running());
}
#[test]
fn toggle_three_times() {
let mut sw = Stopwatch::new();
sw.toggle(); sw.toggle(); sw.toggle(); assert!(sw.running());
}
#[test]
fn reset_while_stopped() {
let mut sw = Stopwatch::new();
sw.start();
sw.tick(Duration::from_secs(10));
sw.stop();
sw.reset();
assert_eq!(sw.elapsed(), Duration::ZERO);
assert!(!sw.running()); }
#[test]
fn multiple_start_stop_cycles_accumulate() {
let mut sw = Stopwatch::new();
sw.start();
sw.tick(Duration::from_secs(5));
sw.stop();
sw.start();
sw.tick(Duration::from_secs(3));
sw.stop();
assert_eq!(sw.elapsed(), Duration::from_secs(8));
}
#[test]
fn tick_once_custom_interval() {
let mut sw = Stopwatch::with_interval(Duration::from_millis(100));
sw.start();
sw.tick_once();
assert_eq!(sw.elapsed(), Duration::from_millis(100));
sw.tick_once();
assert_eq!(sw.elapsed(), Duration::from_millis(200));
}
#[test]
fn tick_once_returns_false_when_stopped() {
let mut sw = Stopwatch::new();
let changed = sw.tick_once();
assert!(!changed);
}
#[test]
fn tick_returns_true_when_running() {
let mut sw = Stopwatch::new();
sw.start();
assert!(sw.tick(Duration::from_secs(1)));
}
#[test]
fn view_after_stop() {
let mut sw = Stopwatch::new();
sw.start();
sw.tick(Duration::from_secs(30));
sw.stop();
assert_eq!(sw.view(), "30s");
}
#[test]
fn view_after_reset() {
let mut sw = Stopwatch::new();
sw.start();
sw.tick(Duration::from_secs(100));
sw.reset();
assert_eq!(sw.view(), "0s");
}
#[test]
fn compact_exactly_one_second() {
assert_eq!(format_compact(Duration::from_secs(1)), "1s");
}
#[test]
fn compact_fractional_micros() {
assert_eq!(format_compact(Duration::from_nanos(1_500)), "1.5\u{00B5}s");
}
#[test]
fn compact_fractional_millis() {
assert_eq!(format_compact(Duration::from_micros(1_500)), "1.5ms");
}
#[test]
fn compact_exact_millis() {
assert_eq!(format_compact(Duration::from_millis(250)), "250ms");
}
#[test]
fn compact_exact_micros() {
assert_eq!(format_compact(Duration::from_micros(42)), "42\u{00B5}s");
}
#[test]
fn compact_one_nanosecond() {
assert_eq!(format_compact(Duration::from_nanos(1)), "1ns");
}
#[test]
fn compact_999_nanoseconds() {
assert_eq!(format_compact(Duration::from_nanos(999)), "999ns");
}
#[test]
fn compact_exactly_one_minute() {
assert_eq!(format_compact(Duration::from_secs(60)), "1m0s");
}
#[test]
fn compact_exactly_one_hour() {
assert_eq!(format_compact(Duration::from_secs(3600)), "1h0m0s");
}
#[test]
fn compact_large_hours() {
assert_eq!(format_compact(Duration::from_secs(360_000)), "100h0m0s");
}
#[test]
fn compact_seconds_with_nanos() {
assert_eq!(format_compact(Duration::new(1, 1)), "1.000000001s");
}
#[test]
fn clock_exactly_one_minute() {
assert_eq!(format_clock(Duration::from_secs(60)), "01:00");
}
#[test]
fn clock_exactly_one_hour() {
assert_eq!(format_clock(Duration::from_secs(3600)), "1:00:00");
}
#[test]
fn clock_large_hours() {
assert_eq!(format_clock(Duration::from_secs(360_000)), "100:00:00");
}
#[test]
fn clock_max_minutes_seconds() {
assert_eq!(format_clock(Duration::from_secs(3599)), "59:59");
}
#[test]
fn clock_one_second() {
assert_eq!(format_clock(Duration::from_secs(1)), "00:01");
}
#[test]
fn with_interval_zero() {
let mut sw = Stopwatch::with_interval(Duration::ZERO);
sw.start();
sw.tick_once();
assert_eq!(sw.elapsed(), Duration::ZERO);
}
#[test]
fn with_interval_sub_millisecond() {
let mut sw = Stopwatch::with_interval(Duration::from_micros(100));
sw.start();
for _ in 0..10 {
sw.tick_once();
}
assert_eq!(sw.elapsed(), Duration::from_micros(1000));
}
#[test]
fn display_format_debug_clone_copy_eq() {
let fmt = DisplayFormat::Compact;
let copied = fmt;
assert_eq!(fmt, copied);
assert_eq!(format!("{fmt:?}"), "Compact");
assert_eq!(format!("{:?}", DisplayFormat::Clock), "Clock");
assert_ne!(DisplayFormat::Compact, DisplayFormat::Clock);
}
#[test]
fn stopwatch_debug_and_clone() {
let mut sw = Stopwatch::new();
sw.start();
sw.tick(Duration::from_secs(5));
let cloned = sw.clone();
assert_eq!(cloned.elapsed(), Duration::from_secs(5));
assert!(cloned.running());
let _ = format!("{sw:?}");
}
#[test]
fn stopwatch_default_equals_new() {
let def = Stopwatch::default();
let new = Stopwatch::new();
assert_eq!(def.elapsed(), new.elapsed());
assert_eq!(def.running(), new.running());
assert_eq!(def.interval(), new.interval());
}
}