From a285c1a0f22d649113065b391910fd4f0846366b Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Fri, 24 Apr 2026 11:32:38 +0200 Subject: [PATCH] feat(api): add Time trait with Untimed and i64 impls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation for generic History time axis. Untimed is the ZST case (no drift across slices); i64 is the standard timestamp case. Additional impls (time::OffsetDateTime, chrono) can be added behind feature flags in follow-up work. The trait is not yet wired into History — that happens in Task 7 along with generifying Drift over T. Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md. --- src/lib.rs | 2 ++ src/time.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/time.rs diff --git a/src/lib.rs b/src/lib.rs index a9e3938..9147875 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use std::{ #[cfg(feature = "approx")] mod approx; pub(crate) mod arena; +mod time; mod time_slice; pub use time_slice::TimeSlice; mod competitor; @@ -31,6 +32,7 @@ pub use key_table::KeyTable; use matrix::Matrix; pub use rating::Rating; pub use schedule::ScheduleReport; +pub use time::{Time, Untimed}; pub const BETA: f64 = 1.0; pub const MU: f64 = 0.0; diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..813ff39 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,54 @@ +//! Generic time axis for `History`. +//! +//! Users pick the `Time` type based on their domain: `Untimed` when no +//! time axis is meaningful, `i64` for integer day/second timestamps. +//! Additional impls can be added behind feature flags. + +/// A timestamp on the global ordering axis. +/// +/// Must be `Ord + Copy` so slices can sort events, and `'static` so +/// `History` can store it by value without lifetimes. +pub trait Time: Copy + Ord + 'static { + /// How much time elapsed between `self` and `later`. + /// + /// Used by `Drift::variance_delta` to compute skill drift. Returning + /// zero means no drift accumulates between the two points. Return value + /// must be non-negative for `self <= later`. + fn elapsed_to(&self, later: &Self) -> i64; +} + +/// Zero-sized type representing "no time axis." +/// +/// Used as the default `Time` when events are unordered. Elapsed is always 0, +/// so no drift accumulates across slices. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Untimed; + +impl Time for Untimed { + fn elapsed_to(&self, _later: &Self) -> i64 { + 0 + } +} + +impl Time for i64 { + fn elapsed_to(&self, later: &Self) -> i64 { + later - self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn untimed_elapsed_is_zero() { + assert_eq!(Untimed.elapsed_to(&Untimed), 0); + } + + #[test] + fn i64_elapsed_is_difference() { + assert_eq!(5i64.elapsed_to(&10), 5); + assert_eq!(10i64.elapsed_to(&5), -5); + assert_eq!(0i64.elapsed_to(&0), 0); + } +}