diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..9e1579c --- /dev/null +++ b/src/event.rs @@ -0,0 +1,132 @@ +//! Typed event description for bulk ingestion. +//! +//! `Event` is the new public event shape (spec Section 4). Replaces +//! the nested `Vec>>`, `Vec>`, `Vec>>` +//! that the old `add_events_with_prior` took. + +use smallvec::SmallVec; + +use crate::{gaussian::Gaussian, outcome::Outcome, time::Time}; + +/// A single match at time `time` involving some number of teams. +#[derive(Clone, Debug)] +pub struct Event { + pub time: T, + pub teams: SmallVec<[Team; 4]>, + pub outcome: Outcome, +} + +/// A team: list of members competing together. +#[derive(Clone, Debug)] +pub struct Team { + pub members: SmallVec<[Member; 4]>, +} + +impl Team { + pub fn new() -> Self { + Self { + members: SmallVec::new(), + } + } + + pub fn with_members>>(members: I) -> Self { + Self { + members: members.into_iter().collect(), + } + } +} + +impl Default for Team { + fn default() -> Self { + Self::new() + } +} + +/// One member of a team, identified by user key `K`. +/// +/// `weight` defaults to 1.0; a per-event `prior` can override the competitor's +/// current skill estimate for this event only. +#[derive(Clone, Debug)] +pub struct Member { + pub key: K, + pub weight: f64, + pub prior: Option, +} + +impl Member { + pub fn new(key: K) -> Self { + Self { + key, + weight: 1.0, + prior: None, + } + } + + pub fn with_weight(mut self, weight: f64) -> Self { + self.weight = weight; + self + } + + pub fn with_prior(mut self, prior: Gaussian) -> Self { + self.prior = Some(prior); + self + } +} + +/// Convenience: a member is a user key with default weight 1.0 and no prior. +impl From for Member { + fn from(key: K) -> Self { + Self::new(key) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Outcome; + + #[test] + fn member_new_has_unit_weight_no_prior() { + let m = Member::new("alice"); + assert_eq!(m.key, "alice"); + assert_eq!(m.weight, 1.0); + assert!(m.prior.is_none()); + } + + #[test] + fn member_builder_methods_chain() { + let m = Member::new("alice") + .with_weight(0.5) + .with_prior(Gaussian::from_ms(20.0, 5.0)); + assert_eq!(m.weight, 0.5); + assert!(m.prior.is_some()); + } + + #[test] + fn member_from_key() { + let m: Member<&str> = "bob".into(); + assert_eq!(m.key, "bob"); + assert_eq!(m.weight, 1.0); + } + + #[test] + fn team_with_members_collects() { + let t: Team<&str> = Team::with_members([Member::new("a"), Member::new("b")]); + assert_eq!(t.members.len(), 2); + } + + #[test] + fn event_construction() { + use smallvec::smallvec; + let e: Event = Event { + time: 1, + teams: smallvec![ + Team::with_members([Member::new("a")]), + Team::with_members([Member::new("b")]), + ], + outcome: Outcome::winner(0, 2), + }; + assert_eq!(e.teams.len(), 2); + assert_eq!(e.time, 1); + } +} diff --git a/src/lib.rs b/src/lib.rs index 695be4a..c007764 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ pub use time_slice::TimeSlice; mod competitor; pub mod drift; mod error; +mod event; pub(crate) mod factor; mod game; pub mod gaussian; @@ -26,6 +27,7 @@ pub mod storage; pub use competitor::Competitor; pub use drift::{ConstantDrift, Drift}; pub use error::InferenceError; +pub use event::{Event, Member, Team}; pub use game::Game; pub use gaussian::Gaussian; pub use history::History;