use super::funejson::Value; const POW10: [f64; 13] = [ 1.0, 10.0, 100.0, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, ]; // Half away from zero on the binary64 value, then -0 becomes 0. Only floor, // * and / are used, so every language rounds the same double the same way. fn round_to(x: f64, decimals: usize) -> f64 { let scale = POW10[decimals]; let y = x.abs() * scale; let mut r = y.floor(); if y - r >= 0.5 { r += 1.0; } let out = r / scale; (if x < 0.0 { -out } else { out }) + 0.0 } /// The p-th percentile of `values` by a named method. /// /// There is no single "95th percentile": the caller names the definition, so /// a dashboard and a billing job cannot quietly disagree. /// /// # Panics /// Panics on an empty list, a non-finite value, p outside 0..=100, an unknown /// method, or decimals outside 0..=12. pub fn percentile(values: &[f64], p: f64, method: &str, decimals: i64) -> f64 { if values.is_empty() { panic!("values must not be empty"); } for v in values { if !v.is_finite() { panic!("values must be finite numbers, received {}", v); } } if !p.is_finite() || p < 0.0 || p > 100.0 { panic!("p must be between 0 and 100, received {}", p); } if !(0..=12).contains(&decimals) { panic!("decimals must be a whole number from 0 to 12, received {}", decimals); } let mut sorted = values.to_vec(); sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()); let n = sorted.len(); let result = match method { "nearest-rank" => { // The smallest value with at least p% of the sample at or below it. let mut rank = ((p * n as f64) / 100.0).ceil() as usize; if rank < 1 { rank = 1; } sorted[rank - 1] } "linear" => { // Hyndman & Fan type 7: zero-based position (n - 1) * p / 100. let h = ((n - 1) as f64 * p) / 100.0; let lo = h.floor(); let i = lo as usize; if i >= n - 1 { sorted[n - 1] } else { sorted[i] + (h - lo) * (sorted[i + 1] - sorted[i]) } } other => panic!("unknown percentile method \"{}\"", other), }; round_to(result, decimals as usize) } pub fn fune_vector(args: &[Value]) -> Value { // Refuse what the typed signature cannot hold, with the wording TypeScript // and Python use, rather than let the conversion below quietly change it. for v in args[0].as_arr() { if !matches!(v, Value::Int(_) | Value::Float(_)) { panic!("values must be finite numbers, received {:?}", v); } } let values: Vec = args[0].as_arr().iter().map(|v| v.as_f64()).collect(); Value::Float(percentile( &values, args[1].as_f64(), args[2].as_str(), args[3].as_i64(), )) }