use crate::error::StaticError;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PercentileSummary {
pub p90: f64,
pub p50: f64,
pub p10: f64,
pub mean: f64,
}
impl PercentileSummary {
pub fn from_realizations(values: &[f64]) -> Result<Self, StaticError> {
if values.is_empty() {
return Err(StaticError::InvalidInput(
"cannot summarise zero realizations (need at least 1)".into(),
));
}
let s = petektools::sampling::reservoir_summary(values)?;
Ok(Self {
p90: s.p90,
p50: s.p50,
p10: s.p10,
mean: s.mean,
})
}
#[must_use]
pub fn swanson_mean(&self) -> f64 {
0.3 * self.p10 + 0.4 * self.p50 + 0.3 * self.p90
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn petroleum_convention_ordering() {
let v: Vec<f64> = (1..=1000).map(f64::from).collect();
let s = PercentileSummary::from_realizations(&v).unwrap();
assert!(s.p90 < s.p50 && s.p50 < s.p10);
assert!((s.p90 - 100.9).abs() < 1.0);
assert!((s.p10 - 900.1).abs() < 1.0);
assert!((s.mean - 500.5).abs() < 1e-9);
}
#[test]
fn zero_realizations_is_a_typed_error_not_a_panic() {
let err = PercentileSummary::from_realizations(&[]).unwrap_err();
assert!(matches!(err, StaticError::InvalidInput(_)));
}
}