pub fn percentile(values: &[f64], p: f64) -> Option<f64> {
let mut sorted: Vec<f64> = values.iter().copied().filter(|v| !v.is_nan()).collect();
if sorted.is_empty() {
return None;
}
sorted.sort_by(f64::total_cmp);
let p = p.clamp(0.0, 100.0);
let rank = (p / 100.0 * sorted.len() as f64).ceil() as usize;
let index = rank.saturating_sub(1).min(sorted.len() - 1);
Some(sorted[index])
}
pub fn mean_of_present<I: IntoIterator<Item = Option<f64>>>(values: I) -> Option<f64> {
let mut sum = 0.0;
let mut count = 0usize;
for value in values.into_iter().flatten() {
sum += value;
count += 1;
}
(count > 0).then(|| sum / count as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_percentile_is_always_a_value_that_was_actually_measured() {
let values = [10.0, 20.0, 30.0, 40.0];
for p in [0.0, 25.0, 50.0, 75.0, 95.0, 100.0] {
let got = percentile(&values, p).expect("non-empty");
assert!(values.contains(&got), "p{p} produced {got}");
}
assert_eq!(percentile(&values, 50.0), Some(20.0));
assert_eq!(percentile(&values, 95.0), Some(40.0));
assert_eq!(percentile(&values, 0.0), Some(10.0));
}
#[test]
fn a_percentile_of_nothing_is_none_rather_than_zero() {
assert_eq!(percentile(&[], 95.0), None);
assert_eq!(percentile(&[f64::NAN], 95.0), None);
}
#[test]
fn a_mean_ignores_absent_values_instead_of_reading_them_as_zero() {
assert_eq!(
mean_of_present([Some(100.0), None, Some(300.0), None]),
Some(200.0)
);
assert_eq!(mean_of_present([None, None]), None);
assert_eq!(mean_of_present(Vec::<Option<f64>>::new()), None);
}
}