use super::model::{MetricDef, RegistryError};
use cel::{Context, Program, Value};
use std::collections::BTreeMap;
pub(crate) fn register_agg(ctx: &mut Context, pops: std::sync::Arc<Populations>) {
use std::sync::Arc;
ctx.add_function(
"agg",
move |key: Arc<String>, reducer: Arc<String>, population: Arc<String>| -> f64 {
pops.reduce_for(&key, &reducer, &population)
},
);
}
pub(crate) fn reduce(vals: &[f64], reducer: &str) -> Option<f64> {
if vals.is_empty() {
return None;
}
match reducer {
"sum" => Some(vals.iter().sum()),
"avg" | "mean" => Some(vals.iter().sum::<f64>() / vals.len() as f64),
"min" => Some(vals.iter().copied().fold(f64::INFINITY, f64::min)),
"max" => Some(vals.iter().copied().fold(f64::NEG_INFINITY, f64::max)),
"count" => Some(vals.len() as f64),
"median" => percentile(vals, 50.0),
_ if reducer
.strip_prefix("top")
.and_then(|rest| rest.split('_').next())
.is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())) =>
{
let rest = reducer.strip_prefix("top").unwrap();
let (num, base) = match rest.split_once('_') {
Some((n, b)) => (n, b),
None => (rest, "avg"),
};
let n: usize = num.parse().ok()?;
let mut s = vals.to_vec();
s.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); s.truncate(n);
reduce(&s, base)
}
r if r.starts_with('p') => r[1..].parse::<f64>().ok().and_then(|q| percentile(vals, q)),
_ => None,
}
}
pub(crate) fn percentile(vals: &[f64], q: f64) -> Option<f64> {
if vals.is_empty() {
return None;
}
let mut s = vals.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = s.len();
if n == 1 {
return Some(s[0]);
}
let h = (n as f64 - 1.0) * (q / 100.0);
let lo = h.floor();
let lo_i = lo as usize;
let frac = h - lo;
let v = if lo_i + 1 < n {
s[lo_i] + frac * (s[lo_i + 1] - s[lo_i])
} else {
s[lo_i]
};
Some(v)
}
pub(crate) fn exec_f64(program: &Program, ctx: &Context) -> Option<f64> {
match program.execute(ctx) {
Ok(Value::Float(v)) if v.is_finite() => Some(v),
Ok(Value::Int(v)) => Some(v as f64),
Ok(Value::UInt(v)) => Some(v as f64),
_ => None,
}
}
pub(crate) fn register_math(ctx: &mut Context) {
ctx.add_function("log2", |x: f64| x.log2());
ctx.add_function("ln", |x: f64| x.ln());
ctx.add_function("log10", |x: f64| x.log10());
ctx.add_function("pow", |x: f64, y: f64| x.powf(y));
ctx.add_function("sqrt", |x: f64| x.sqrt());
ctx.add_function("sin", |x: f64| x.sin());
ctx.add_function("cos", |x: f64| x.cos());
ctx.add_function("abs", |x: f64| x.abs());
ctx.add_function("min2", |x: f64, y: f64| x.min(y));
ctx.add_function("max2", |x: f64, y: f64| x.max(y));
}
pub(crate) fn references(formula: &str, key: &str) -> bool {
let bytes = formula.as_bytes();
let kb = key.as_bytes();
let is_word = |c: u8| c.is_ascii_alphanumeric() || c == b'_';
let mut i = 0;
while let Some(pos) = formula[i..].find(key) {
let start = i + pos;
let end = start + kb.len();
let before_ok = start == 0 || !is_word(bytes[start - 1]);
let after_ok = end == bytes.len() || !is_word(bytes[end]);
if before_ok && after_ok {
return true;
}
i = start + 1;
}
false
}
pub(crate) fn topo_order(
defs: &[(&String, &MetricDef)],
keys: &[String],
) -> Result<Vec<String>, RegistryError> {
let keyset: std::collections::BTreeSet<&str> = keys.iter().map(|s| s.as_str()).collect();
let mut deps: BTreeMap<String, std::collections::BTreeSet<String>> = BTreeMap::new();
let mut indeg: BTreeMap<String, usize> = BTreeMap::new();
for k in keys {
deps.entry(k.clone()).or_default();
indeg.entry(k.clone()).or_insert(0);
}
for (key, def) in defs {
for cand in &keyset {
if *cand != key.as_str()
&& references(&def.formula_cel, cand)
&& deps.get_mut(*key).unwrap().insert((*cand).to_string())
{
*indeg.get_mut(*key).unwrap() += 1;
}
}
}
let mut order = Vec::with_capacity(keys.len());
loop {
let ready: Vec<String> = indeg
.iter()
.filter(|&(_, &d)| d == 0)
.map(|(k, _)| k.clone())
.collect();
if ready.is_empty() {
break;
}
for k in ready {
indeg.remove(&k);
for (other, od) in deps.iter() {
if od.contains(&k)
&& let Some(d) = indeg.get_mut(other)
{
*d -= 1;
}
}
order.push(k);
}
}
if order.len() != keys.len() {
let mut remaining: Vec<String> = indeg.keys().cloned().collect();
remaining.sort();
return Err(RegistryError::Cycle { keys: remaining });
}
Ok(order)
}
#[derive(Debug, Clone, Default)]
pub struct Populations {
not_empty: BTreeMap<String, Vec<f64>>,
all: BTreeMap<String, Vec<f64>>,
}
impl Populations {
pub(crate) fn reduce_for(&self, key: &str, reducer: &str, population: &str) -> f64 {
let table = match population {
"all" => &self.all,
_ => &self.not_empty,
};
table
.get(key)
.and_then(|vals| reduce(vals, reducer))
.unwrap_or(f64::NAN)
}
pub fn build(
rows: &[BTreeMap<String, f64>],
keys: &[String],
omit_at: &BTreeMap<String, f64>,
) -> Populations {
let mut not_empty = BTreeMap::new();
let mut all = BTreeMap::new();
for key in keys {
let omit = omit_at.get(key).copied().unwrap_or(0.0);
let present: Vec<f64> = rows.iter().filter_map(|r| r.get(key).copied()).collect();
let ne: Vec<f64> = present.iter().copied().filter(|v| *v != omit).collect();
let missing = rows.len().saturating_sub(present.len());
let mut a = present;
a.extend(std::iter::repeat_n(omit, missing));
not_empty.insert(key.clone(), ne);
all.insert(key.clone(), a);
}
Populations { not_empty, all }
}
}