use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Score {
pub value: f64,
pub threshold: f64,
}
impl Score {
#[must_use]
pub const fn new(value: f64, threshold: f64) -> Self {
Self {
value: value.clamp(0.0, 1.0),
threshold: threshold.clamp(0.0, 1.0),
}
}
#[must_use]
pub const fn pass() -> Self {
Self {
value: 1.0,
threshold: 0.5,
}
}
#[must_use]
pub const fn fail() -> Self {
Self {
value: 0.0,
threshold: 0.5,
}
}
#[must_use]
pub fn verdict(&self) -> Verdict {
if self.value >= self.threshold {
Verdict::Pass
} else {
Verdict::Fail
}
}
}
impl Default for Score {
fn default() -> Self {
Self {
value: 0.0,
threshold: 0.5,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Verdict {
Pass,
Fail,
}
impl Verdict {
#[must_use]
pub const fn is_pass(&self) -> bool {
matches!(self, Self::Pass)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn score_pass_verdict() {
let s = Score::new(0.8, 0.5);
assert_eq!(s.verdict(), Verdict::Pass);
}
#[test]
fn score_fail_verdict() {
let s = Score::new(0.3, 0.5);
assert_eq!(s.verdict(), Verdict::Fail);
}
#[test]
fn score_at_threshold_passes() {
let s = Score::new(0.5, 0.5);
assert_eq!(s.verdict(), Verdict::Pass);
}
#[test]
fn score_clamps_to_bounds() {
let s = Score::new(1.5, -0.1);
assert!((s.value - 1.0).abs() < f64::EPSILON);
assert!((s.threshold - 0.0).abs() < f64::EPSILON);
}
#[test]
fn pass_and_fail_constructors() {
assert_eq!(Score::pass().verdict(), Verdict::Pass);
assert_eq!(Score::fail().verdict(), Verdict::Fail);
}
#[test]
fn verdict_is_pass() {
assert!(Verdict::Pass.is_pass());
assert!(!Verdict::Fail.is_pass());
}
}