pub fn percentile(buckets: &[(f64, f64)], q: f64) -> Option<f64> {
if buckets.is_empty() || !(0.0..=1.0).contains(&q) {
return None;
}
let total = buckets.last().map(|(_, c)| *c)?;
if total <= 0.0 {
return None;
}
let target = total * q;
let idx = buckets.iter().position(|&(_, c)| c >= target)?;
let (le, cum) = buckets[idx];
if le.is_infinite() {
return buckets[..idx]
.iter()
.rev()
.find(|(b_le, _)| !b_le.is_infinite())
.map(|&(b_le, _)| b_le);
}
let (lower_le, lower_cum) = if idx == 0 {
(0.0_f64, 0.0_f64)
} else {
buckets[idx - 1]
};
let bucket_width = le - lower_le;
let bucket_count = cum - lower_cum;
if bucket_count <= 0.0 || bucket_width <= 0.0 {
return Some(le);
}
let frac = (target - lower_cum) / bucket_count;
Some(lower_le + frac * bucket_width)
}
pub fn fraction_le(buckets: &[(f64, f64)], threshold: f64) -> Option<f64> {
if buckets.is_empty() || threshold.is_nan() {
return None;
}
let total = buckets.last().map(|(_, c)| *c)?;
if total <= 0.0 {
return None;
}
let first = buckets[0];
if threshold <= first.0 && !first.0.is_infinite() {
if first.0 <= 0.0 {
return Some(0.0);
}
let frac = (threshold / first.0).clamp(0.0, 1.0);
return Some((first.1 * frac / total).clamp(0.0, 1.0));
}
let mut prev_le = 0.0_f64;
let mut prev_cum = 0.0_f64;
for &(le, cum) in buckets {
if le.is_infinite() {
return Some((prev_cum / total).clamp(0.0, 1.0));
}
if le >= threshold {
let bucket_width = le - prev_le;
let bucket_count = cum - prev_cum;
if bucket_width <= 0.0 || bucket_count <= 0.0 {
return Some((prev_cum / total).clamp(0.0, 1.0));
}
let frac = (threshold - prev_le) / bucket_width;
let interp = prev_cum + frac * bucket_count;
return Some((interp / total).clamp(0.0, 1.0));
}
prev_le = le;
prev_cum = cum;
}
Some(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-9
}
#[test]
fn returns_none_for_empty_buckets() {
assert_eq!(percentile(&[], 0.5), None);
}
#[test]
fn returns_none_when_total_count_is_zero() {
let buckets = vec![(0.1, 0.0), (0.5, 0.0), (f64::INFINITY, 0.0)];
assert_eq!(percentile(&buckets, 0.5), None);
}
#[test]
fn returns_none_for_invalid_quantile() {
let buckets = vec![(0.1, 50.0), (1.0, 100.0)];
assert_eq!(percentile(&buckets, -0.1), None);
assert_eq!(percentile(&buckets, 1.1), None);
}
#[test]
fn p50_interpolates_in_first_bucket() {
let buckets = vec![(0.1, 50.0), (1.0, 100.0)];
let p50 = percentile(&buckets, 0.5).expect("some");
assert!(approx(p50, 0.1), "expected 0.1, got {p50}");
}
#[test]
fn p50_interpolates_within_bucket() {
let buckets = vec![(0.1, 25.0), (1.0, 100.0)];
let p50 = percentile(&buckets, 0.5).expect("some");
assert!(approx(p50, 0.4), "expected 0.4, got {p50}");
}
#[test]
fn p99_falls_back_to_finite_le_when_inf_selected() {
let buckets = vec![(1.0, 99.0), (f64::INFINITY, 100.0)];
let p995 = percentile(&buckets, 0.995).expect("some");
assert!(approx(p995, 1.0), "expected fallback to 1.0, got {p995}");
}
#[test]
fn returns_none_when_only_inf_bucket() {
let buckets = vec![(f64::INFINITY, 10.0)];
assert_eq!(percentile(&buckets, 0.5), None);
}
#[test]
fn handles_single_finite_bucket() {
let buckets = vec![(0.5, 100.0)];
let p50 = percentile(&buckets, 0.5).expect("some");
assert!(approx(p50, 0.25), "expected 0.25, got {p50}");
}
#[test]
fn p95_lands_in_tail_bucket() {
let buckets = vec![(0.5, 900.0), (1.0, 990.0), (f64::INFINITY, 1000.0)];
let p95 = percentile(&buckets, 0.95).expect("some");
assert!(approx(p95, 0.5 + (50.0 / 90.0) * 0.5));
}
#[test]
fn fraction_le_returns_none_for_empty() {
assert_eq!(fraction_le(&[], 0.5), None);
}
#[test]
fn fraction_le_returns_none_when_total_zero() {
let buckets = vec![(0.1, 0.0), (1.0, 0.0), (f64::INFINITY, 0.0)];
assert_eq!(fraction_le(&buckets, 0.5), None);
}
#[test]
fn fraction_le_at_bucket_boundary() {
let buckets = vec![(0.1, 50.0), (1.0, 100.0)];
let f = fraction_le(&buckets, 0.1).expect("some");
assert!(approx(f, 0.5), "expected 0.5, got {f}");
}
#[test]
fn fraction_le_interpolates_within_bucket() {
let buckets = vec![(0.1, 25.0), (1.0, 100.0)];
let f = fraction_le(&buckets, 0.4).expect("some");
assert!(approx(f, 0.5), "expected 0.5, got {f}");
}
#[test]
fn fraction_le_below_first_bucket_is_linear_from_zero() {
let buckets = vec![(0.1, 100.0), (1.0, 100.0)];
let f = fraction_le(&buckets, 0.05).expect("some");
assert!(approx(f, 0.5), "expected 0.5, got {f}");
}
#[test]
fn fraction_le_above_finite_bounds_caps_at_finite_cum() {
let buckets = vec![(1.0, 99.0), (f64::INFINITY, 100.0)];
let f = fraction_le(&buckets, 5.0).expect("some");
assert!(approx(f, 0.99), "expected 0.99, got {f}");
}
#[test]
fn fraction_le_threshold_well_above_finite_no_inf_saturates() {
let buckets = vec![(0.1, 10.0), (1.0, 100.0)];
let f = fraction_le(&buckets, 5.0).expect("some");
assert!(approx(f, 1.0), "expected 1.0, got {f}");
}
#[test]
fn fraction_le_threshold_below_first_le_with_zero_or_negative_le() {
let buckets = vec![(0.0, 0.0), (1.0, 100.0)];
let f = fraction_le(&buckets, -1.0).expect("some");
assert!(approx(f, 0.0), "expected 0.0, got {f}");
}
}