diff --git a/Cargo.toml b/Cargo.toml index d6901d2..f0307df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ harness = false [dependencies] approx = { version = "0.5.1", optional = true } +smallvec = "1" [dev-dependencies] criterion = "0.5" diff --git a/src/lib.rs b/src/lib.rs index f9aa348..695be4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ pub mod gaussian; mod history; mod key_table; mod matrix; +mod outcome; mod rating; pub(crate) mod schedule; pub mod storage; @@ -30,6 +31,7 @@ pub use gaussian::Gaussian; pub use history::History; pub use key_table::KeyTable; use matrix::Matrix; +pub use outcome::Outcome; pub use rating::Rating; pub use schedule::ScheduleReport; pub use time::{Time, Untimed}; diff --git a/src/outcome.rs b/src/outcome.rs new file mode 100644 index 0000000..a57c26d --- /dev/null +++ b/src/outcome.rs @@ -0,0 +1,87 @@ +//! Outcome of a match. +//! +//! In T2, only `Ranked` is supported; `Scored` will be added together with +//! `MarginFactor` in T4. The enum is `#[non_exhaustive]` so adding `Scored` +//! is non-breaking for downstream `match` expressions. + +use smallvec::SmallVec; + +/// Final outcome of a match. +/// +/// `Ranked(ranks)`: lower rank = better. Equal ranks mean a tie between those +/// teams. `ranks.len()` must equal the number of teams in the event. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub enum Outcome { + Ranked(SmallVec<[u32; 4]>), +} + +impl Outcome { + /// `N`-team outcome where team `winner` won and everyone else tied for last. + /// + /// Panics if `winner >= n`. + pub fn winner(winner: u32, n: u32) -> Self { + assert!(winner < n, "winner index {winner} out of range 0..{n}"); + let ranks: SmallVec<[u32; 4]> = (0..n).map(|i| if i == winner { 0 } else { 1 }).collect(); + Self::Ranked(ranks) + } + + /// All `n` teams tied. + pub fn draw(n: u32) -> Self { + Self::Ranked(SmallVec::from_vec(vec![0; n as usize])) + } + + /// Explicit per-team ranking. + pub fn ranking>(ranks: I) -> Self { + Self::Ranked(ranks.into_iter().collect()) + } + + pub fn team_count(&self) -> usize { + match self { + Self::Ranked(r) => r.len(), + } + } + + #[allow(dead_code)] + pub(crate) fn as_ranks(&self) -> &[u32] { + match self { + Self::Ranked(r) => r, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn winner_two_teams() { + let o = Outcome::winner(0, 2); + assert_eq!(o.as_ranks(), &[0u32, 1]); + assert_eq!(o.team_count(), 2); + } + + #[test] + fn winner_three_teams_second_wins() { + let o = Outcome::winner(1, 3); + assert_eq!(o.as_ranks(), &[1u32, 0, 1]); + } + + #[test] + fn draw_three_teams() { + let o = Outcome::draw(3); + assert_eq!(o.as_ranks(), &[0u32, 0, 0]); + } + + #[test] + fn ranking_from_iter() { + let o = Outcome::ranking([2, 0, 1]); + assert_eq!(o.as_ranks(), &[2u32, 0, 1]); + } + + #[test] + #[should_panic(expected = "winner index 2 out of range")] + fn winner_out_of_range_panics() { + let _ = Outcome::winner(2, 2); + } +}