use crate::domain::AnomalyScore;
use crate::error::{RcfError, RcfResult};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AnomalyGrade {
score: AnomalyScore,
threshold: f64,
grade: f64,
is_anomaly: bool,
ready: bool,
}
impl AnomalyGrade {
pub fn new(
score: AnomalyScore,
threshold: f64,
grade: f64,
is_anomaly: bool,
ready: bool,
) -> RcfResult<Self> {
if !threshold.is_finite() {
return Err(RcfError::NaNValue);
}
if !grade.is_finite() || !(0.0..=1.0).contains(&grade) {
return Err(RcfError::NaNValue);
}
Ok(Self {
score,
threshold,
grade,
is_anomaly,
ready,
})
}
#[must_use]
pub fn score(&self) -> AnomalyScore {
self.score
}
#[must_use]
pub fn threshold(&self) -> f64 {
self.threshold
}
#[must_use]
pub fn grade(&self) -> f64 {
self.grade
}
#[must_use]
pub fn is_anomaly(&self) -> bool {
self.is_anomaly
}
#[must_use]
pub fn ready(&self) -> bool {
self.ready
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)] mod tests {
use super::*;
fn score(v: f64) -> AnomalyScore {
AnomalyScore::new(v).unwrap()
}
#[test]
fn new_rejects_non_finite_threshold() {
assert!(AnomalyGrade::new(score(1.0), f64::NAN, 0.5, true, true).is_err());
assert!(AnomalyGrade::new(score(1.0), f64::INFINITY, 0.5, true, true).is_err());
}
#[test]
fn new_rejects_grade_outside_unit_interval() {
assert!(AnomalyGrade::new(score(1.0), 1.0, -0.01, false, true).is_err());
assert!(AnomalyGrade::new(score(1.0), 1.0, 1.01, true, true).is_err());
}
#[test]
fn new_rejects_non_finite_grade() {
assert!(AnomalyGrade::new(score(1.0), 1.0, f64::NAN, false, true).is_err());
}
#[test]
fn new_accepts_grade_at_bounds() {
assert!(AnomalyGrade::new(score(1.0), 1.0, 0.0, false, true).is_ok());
assert!(AnomalyGrade::new(score(1.0), 1.0, 1.0, true, true).is_ok());
}
#[test]
fn accessors_expose_fields() {
let g = AnomalyGrade::new(score(2.5), 1.25, 0.5, true, true).unwrap();
assert_eq!(f64::from(g.score()), 2.5);
assert_eq!(g.threshold(), 1.25);
assert_eq!(g.grade(), 0.5);
assert!(g.is_anomaly());
assert!(g.ready());
}
#[test]
fn warming_up_grade_is_not_anomaly() {
let g = AnomalyGrade::new(score(10.0), 0.0, 0.0, false, false).unwrap();
assert!(!g.ready());
assert!(!g.is_anomaly());
assert_eq!(g.grade(), 0.0);
}
}