use crate::error::AnalyticsError;
#[derive(Debug, Clone)]
pub struct Percentiles {
sorted: Vec<f64>,
}
impl Percentiles {
pub fn from_sorted(data: &[f64]) -> Result<Self, AnalyticsError> {
if data.is_empty() {
return Err(AnalyticsError::InvalidInput(
"data slice must not be empty".into(),
));
}
Ok(Self {
sorted: data.to_vec(),
})
}
pub fn from_unsorted(data: &[f64]) -> Result<Self, AnalyticsError> {
if data.is_empty() {
return Err(AnalyticsError::InvalidInput(
"data slice must not be empty".into(),
));
}
let mut sorted = data.to_vec();
sorted.sort_by(|a, b| a.total_cmp(b));
Ok(Self { sorted })
}
pub fn p(&self, n: u8) -> Result<f64, AnalyticsError> {
if n > 100 {
return Err(AnalyticsError::InvalidInput(
"percentile n must be in [1, 100]".into(),
));
}
let len = self.sorted.len();
let rank = if n == 0 {
0usize
} else {
let raw = (n as f64 / 100.0 * len as f64).ceil() as usize;
raw.saturating_sub(1).min(len - 1)
};
Ok(self.sorted[rank])
}
#[must_use]
pub fn median(&self) -> f64 {
self.p(50).unwrap_or(f64::NAN)
}
#[must_use]
pub fn len(&self) -> usize {
self.sorted.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.sorted.is_empty()
}
}
pub fn compute_percentiles(data: &[f64], ns: &[u8]) -> Result<Vec<f64>, AnalyticsError> {
let p = Percentiles::from_sorted(data)?;
ns.iter().map(|&n| p.p(n)).collect::<Result<Vec<_>, _>>()
}
#[cfg(test)]
mod tests {
use super::*;
fn sorted(v: &[f64]) -> Percentiles {
Percentiles::from_sorted(v).expect("non-empty")
}
#[test]
fn empty_slice_errors() {
assert!(Percentiles::from_sorted(&[]).is_err());
assert!(Percentiles::from_unsorted(&[]).is_err());
}
#[test]
fn single_element() {
let p = sorted(&[42.0]);
assert_eq!(p.p(1).unwrap(), 42.0);
assert_eq!(p.p(50).unwrap(), 42.0);
assert_eq!(p.p(100).unwrap(), 42.0);
}
#[test]
fn p100_is_max() {
let data: Vec<f64> = (1..=10).map(|x| x as f64).collect();
let p = sorted(&data);
assert_eq!(p.p(100).unwrap(), 10.0);
}
#[test]
fn p50_median_five_elements() {
let p = sorted(&[1.0, 2.0, 3.0, 4.0, 5.0]);
assert_eq!(p.p(50).unwrap(), 3.0);
assert_eq!(p.median(), 3.0);
}
#[test]
fn p95_ten_elements() {
let data: Vec<f64> = (1..=10).map(|x| x as f64).collect();
let p = sorted(&data);
assert_eq!(p.p(95).unwrap(), 10.0);
}
#[test]
fn from_unsorted_produces_correct_result() {
let data = vec![5.0, 1.0, 3.0, 2.0, 4.0];
let p = Percentiles::from_unsorted(&data).expect("non-empty");
assert_eq!(p.p(100).unwrap(), 5.0);
assert_eq!(p.p(1).unwrap(), 1.0);
}
#[test]
fn n_above_100_errors() {
let p = sorted(&[1.0, 2.0, 3.0]);
assert!(p.p(101).is_err());
}
#[test]
fn compute_percentiles_batch() {
let data: Vec<f64> = (1..=100).map(|x| x as f64).collect();
let result = compute_percentiles(&data, &[25, 50, 75, 99]).expect("ok");
assert_eq!(result.len(), 4);
assert_eq!(result[3], 99.0);
}
#[test]
fn len_and_is_empty() {
let p = sorted(&[1.0, 2.0, 3.0]);
assert_eq!(p.len(), 3);
assert!(!p.is_empty());
}
}