use crate::budget::ErrorBudget;
use crate::error::{Result, SlokitError};
use crate::window::Window;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Objective(f64);
impl Objective {
pub fn percent(p: f64) -> Result<Self> {
if !p.is_finite() || p <= 0.0 || p >= 100.0 {
return Err(SlokitError::InvalidObjective(format!(
"{p} is not a percentage in the open interval (0, 100)"
)));
}
Ok(Self(p / 100.0))
}
pub fn ratio(r: f64) -> Result<Self> {
if !r.is_finite() || r <= 0.0 || r >= 1.0 {
return Err(SlokitError::InvalidObjective(format!(
"{r} is not a ratio in the open interval (0, 1)"
)));
}
Ok(Self(r))
}
pub fn as_ratio(&self) -> f64 {
self.0
}
pub fn as_percent(&self) -> f64 {
self.0 * 100.0
}
pub fn error_budget_ratio(&self) -> f64 {
1.0 - self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Slo {
pub objective: Objective,
pub period: Window,
}
impl Slo {
pub fn new(objective: Objective, period: Window) -> Self {
Self { objective, period }
}
pub fn error_budget_ratio(&self) -> f64 {
self.objective.error_budget_ratio()
}
pub fn error_budget(&self, total_events: f64) -> ErrorBudget {
ErrorBudget::new(total_events, self.error_budget_ratio())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percent_round_trips() {
let o = Objective::percent(99.9).unwrap();
assert!((o.as_ratio() - 0.999).abs() < 1e-12);
assert!((o.as_percent() - 99.9).abs() < 1e-9);
assert!((o.error_budget_ratio() - 0.001).abs() < 1e-12);
}
#[test]
fn rejects_out_of_range_objectives() {
assert!(Objective::percent(0.0).is_err());
assert!(Objective::percent(100.0).is_err());
assert!(Objective::percent(150.0).is_err());
assert!(Objective::ratio(0.0).is_err());
assert!(Objective::ratio(1.0).is_err());
assert!(Objective::ratio(f64::NAN).is_err());
}
#[test]
fn error_budget_ratio_matches_workbook() {
let slo = Slo::new(Objective::percent(99.95).unwrap(), Window::days(30));
assert!((slo.error_budget_ratio() - 0.0005).abs() < 1e-12);
}
}