use super::funejson::Value; fn check_counts(target_basis_points: i64, total_events: i64, bad_events: i64) { if !(1..=9999).contains(&target_basis_points) { panic!( "targetBasisPoints must be a whole number from 1 to 9999 (10000 leaves no error budget), received {}", target_basis_points ); } if total_events < 0 { panic!("totalEvents must be a whole number of at least 0, received {}", total_events); } if bad_events < 0 { panic!("badEvents must be a whole number of at least 0, received {}", bad_events); } if bad_events > total_events { panic!("badEvents must not exceed totalEvents: {} > {}", bad_events, total_events); } } // Non-negative operands only, so half-up needs no sign handling. fn half_up(n: i128, d: i128) -> i128 { (2 * n + d) / (2 * d) } /// The error budget of an SLO over one window: how many bad events it /// allows, how many are left, and how much of it is spent. Exact (i128) /// arithmetic throughout. /// /// # Panics /// Panics on a target outside 1..=9999, negative counts, or more bad events than events. pub fn error_budget(target_basis_points: i64, total_events: i64, bad_events: i64) -> ErrorBudget { check_counts(target_basis_points, total_events, bad_events); let total = total_events as i128; let bad = bad_events as i128; // The exact budget is budget_times_10000 / 10000 events; keep it as a fraction. let budget_times_10000 = total * (10000 - target_basis_points as i128); let allowed_bad = (budget_times_10000 / 10000) as i64; let consumed = if total == 0 { 0 } else { half_up(bad * 10000 * 10000, budget_times_10000) as i64 }; ErrorBudget { allowed_bad, remaining_bad: allowed_bad - bad_events, consumed_basis_points: consumed, remaining_basis_points: 10000 - consumed, exhausted: bad * 10000 > budget_times_10000, } } pub fn error_budget_to_value(b: &ErrorBudget) -> Value { Value::obj(vec![ ("allowedBad", Value::Int(b.allowed_bad)), ("remainingBad", Value::Int(b.remaining_bad)), ("consumedBasisPoints", Value::Int(b.consumed_basis_points)), ("remainingBasisPoints", Value::Int(b.remaining_basis_points)), ("exhausted", Value::Bool(b.exhausted)), ]) } // JSON cannot tell 2 from 2.0, and Python refuses a float, so any non-Int is refused here too. fn whole(v: &Value, what: &str) -> i64 { match v { Value::Int(i) => *i, Value::Float(f) => panic!("{} must be a whole number {} received {}", what, if what == "targetBasisPoints" { "from 1 to 9999 (10000 leaves no error budget)," } else { "of at least 0," }, f), _ => panic!("{} must be a whole number", what), } } pub fn fune_vector(args: &[Value]) -> Value { error_budget_to_value(&error_budget( whole(&args[0], "targetBasisPoints"), whole(&args[1], "totalEvents"), whole(&args[2], "badEvents"), )) }