use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
#[error("input slice cannot be empty")]
pub struct CannotBeEmpty;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct Percentiles<T> {
pub minimum: T,
pub mean: f64,
pub median: f64,
pub p90: T,
pub p99: T,
}
pub trait AsF64Lossy: Copy {
fn as_f64_lossy(self) -> f64;
}
macro_rules! impl_as_f64_lossy {
($T:ty) => {
impl AsF64Lossy for $T {
fn as_f64_lossy(self) -> f64 {
self as f64
}
}
};
($($T:ty),* $(,)?) => {
$(impl_as_f64_lossy!($T);)*
}
}
impl_as_f64_lossy!(i8, i16, i32, i64, u8, u16, u32, u64, usize, f32, f64);
pub fn mean<T>(x: &[T]) -> Result<f64, CannotBeEmpty>
where
T: AsF64Lossy + std::iter::Sum,
{
if x.is_empty() {
return Err(CannotBeEmpty);
}
let s: T = x.iter().copied().sum();
Ok(s.as_f64_lossy() / x.len() as f64)
}
pub fn max_f64(x: &[f64]) -> Result<f64, CannotBeEmpty> {
x.iter().copied().reduce(f64::max).ok_or(CannotBeEmpty)
}
pub fn compute_percentiles<T>(x: &mut [T]) -> Result<Percentiles<T>, CannotBeEmpty>
where
T: std::cmp::Ord + std::ops::Add<Output = T> + AsF64Lossy + std::iter::Sum,
{
let mean = mean(x)?;
x.sort_unstable();
let len = x.len();
let half = len / 2;
let median = if len % 2 == 1 {
x[half].as_f64_lossy()
} else {
(x[half - 1].as_f64_lossy() + x[half].as_f64_lossy()) / 2.0
};
let p90 = x[((9 * len) / 10).min(len - 1)];
let p99 = x[((99 * len) / 100).min(len - 1)];
Ok(Percentiles {
minimum: x[0],
mean,
median,
p90,
p99,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mean() {
let empty: &[f32] = &[];
assert!(matches!(mean(empty).unwrap_err(), CannotBeEmpty));
let input = [
-2.049918,
0.12130953,
-0.17400686,
0.7511493,
0.26361275,
1.2661924,
1.023522,
-2.8727458,
-1.0132318,
0.531649,
-0.8730961,
1.0494779,
1.8957608,
0.45292637,
0.1296239,
0.06079646,
-1.3347862,
0.122092366,
-0.82615733,
1.3791777,
1.5189241,
-0.8614088,
-0.62131107,
-2.0626633,
-0.49564686,
];
let r = mean(&input).unwrap();
assert!((r - -0.10475030175999998).abs() <= 3.0e-17);
}
#[test]
fn test_max() {
assert_eq!(max_f64(&[1.0, -1.0, f64::NEG_INFINITY]).unwrap(), 1.0);
assert!(matches!(max_f64(&[]).unwrap_err(), CannotBeEmpty));
assert_eq!(
max_f64(&[1.0, -1.0, f64::NAN, f64::NEG_INFINITY]).unwrap(),
1.0
);
}
#[test]
fn test_compute_percentils() {
{
let empty: &mut [u64] = &mut [];
assert!(matches!(
compute_percentiles(empty).unwrap_err(),
CannotBeEmpty
));
}
{
let v: &mut [u64] = &mut [10];
let p = compute_percentiles(v).unwrap();
let e = Percentiles {
minimum: 10,
mean: 10.0,
median: 10.0,
p90: 10,
p99: 10,
};
assert_eq!(p, e);
}
{
let v: &mut [u64] = &mut [2, 1];
let p = compute_percentiles(v).unwrap();
let e = Percentiles {
minimum: 1,
mean: 1.5,
median: 1.5,
p90: 2,
p99: 2,
};
assert_eq!(p, e);
}
{
let v: &mut [u64] = &mut [2, 1, 3];
let p = compute_percentiles(v).unwrap();
let e = Percentiles {
minimum: 1,
mean: 2.0,
median: 2.0,
p90: 3,
p99: 3,
};
assert_eq!(p, e);
}
{
let v: &mut [u64] = &mut [2, 1, 3, 4, 9, 6, 7, 5, 8];
let p = compute_percentiles(v).unwrap();
let e = Percentiles {
minimum: 1,
mean: 5.0,
median: 5.0,
p90: 9,
p99: 9,
};
assert_eq!(p, e);
}
{
let v: &mut [u64] = &mut [2, 10, 1, 3, 4, 9, 6, 7, 5, 8];
let p = compute_percentiles(v).unwrap();
let e = Percentiles {
minimum: 1,
mean: 5.5,
median: 5.5,
p90: 10,
p99: 10,
};
assert_eq!(p, e);
}
{
let v: &mut [u64] = &mut [2, 10, 1, 3, 4, 9, 6, 11, 7, 5, 8];
let p = compute_percentiles(v).unwrap();
let e = Percentiles {
minimum: 1,
mean: 6.0,
median: 6.0,
p90: 10,
p99: 11,
};
assert_eq!(p, e);
}
}
}