1use crate::budget::ErrorBudget;
4use crate::error::{Result, SlokitError};
5use crate::window::Window;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct Objective(f64);
13
14impl Objective {
15 pub fn percent(p: f64) -> Result<Self> {
17 if !p.is_finite() || p <= 0.0 || p >= 100.0 {
18 return Err(SlokitError::InvalidObjective(format!(
19 "{p} is not a percentage in the open interval (0, 100)"
20 )));
21 }
22 Ok(Self(p / 100.0))
23 }
24
25 pub fn ratio(r: f64) -> Result<Self> {
27 if !r.is_finite() || r <= 0.0 || r >= 1.0 {
28 return Err(SlokitError::InvalidObjective(format!(
29 "{r} is not a ratio in the open interval (0, 1)"
30 )));
31 }
32 Ok(Self(r))
33 }
34
35 pub fn as_ratio(&self) -> f64 {
37 self.0
38 }
39
40 pub fn as_percent(&self) -> f64 {
42 self.0 * 100.0
43 }
44
45 pub fn error_budget_ratio(&self) -> f64 {
47 1.0 - self.0
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct Slo {
55 pub objective: Objective,
57 pub period: Window,
59}
60
61impl Slo {
62 pub fn new(objective: Objective, period: Window) -> Self {
64 Self { objective, period }
65 }
66
67 pub fn error_budget_ratio(&self) -> f64 {
69 self.objective.error_budget_ratio()
70 }
71
72 pub fn error_budget(&self, total_events: f64) -> ErrorBudget {
75 ErrorBudget::new(total_events, self.error_budget_ratio())
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn percent_round_trips() {
85 let o = Objective::percent(99.9).unwrap();
86 assert!((o.as_ratio() - 0.999).abs() < 1e-12);
87 assert!((o.as_percent() - 99.9).abs() < 1e-9);
88 assert!((o.error_budget_ratio() - 0.001).abs() < 1e-12);
89 }
90
91 #[test]
92 fn rejects_out_of_range_objectives() {
93 assert!(Objective::percent(0.0).is_err());
94 assert!(Objective::percent(100.0).is_err());
95 assert!(Objective::percent(150.0).is_err());
96 assert!(Objective::ratio(0.0).is_err());
97 assert!(Objective::ratio(1.0).is_err());
98 assert!(Objective::ratio(f64::NAN).is_err());
99 }
100
101 #[test]
102 fn error_budget_ratio_matches_workbook() {
103 let slo = Slo::new(Objective::percent(99.95).unwrap(), Window::days(30));
104 assert!((slo.error_budget_ratio() - 0.0005).abs() < 1e-12);
105 }
106}