use core::fmt;
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SampleStatistics {
pub count: usize,
pub minimum: f64,
pub maximum: f64,
pub mean: f64,
pub median: f64,
pub quantile: f64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SampleStatisticsError {
InvalidQuantile,
NonFiniteSample {
index: usize,
},
NonFiniteMean,
}
impl fmt::Display for SampleStatisticsError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidQuantile => {
formatter.write_str("quantile must be greater than zero and at most one")
}
Self::NonFiniteSample { index } => write!(formatter, "sample {index} is not finite"),
Self::NonFiniteMean => formatter.write_str("sample mean is not finite"),
}
}
}
impl core::error::Error for SampleStatisticsError {}
#[allow(clippy::cast_precision_loss)]
pub fn summarize_samples(
samples: &mut [f64],
numerator: u64,
denominator: u64,
) -> Result<Option<SampleStatistics>, SampleStatisticsError> {
if numerator == 0 || numerator > denominator {
return Err(SampleStatisticsError::InvalidQuantile);
}
for (index, sample) in samples.iter().enumerate() {
if !sample.is_finite() {
return Err(SampleStatisticsError::NonFiniteSample { index });
}
}
if samples.is_empty() {
return Ok(None);
}
samples.sort_unstable_by(f64::total_cmp);
let count = samples.len();
let mean = samples
.iter()
.map(|sample| sample / count as f64)
.sum::<f64>();
if !mean.is_finite() {
return Err(SampleStatisticsError::NonFiniteMean);
}
let middle = count / 2;
let median = if count.is_multiple_of(2) {
samples[middle - 1].midpoint(samples[middle])
} else {
samples[middle]
};
let rank = ((count as u128) * u128::from(numerator)).div_ceil(u128::from(denominator));
let index = (rank - 1) as usize;
Ok(Some(SampleStatistics {
count,
minimum: samples[0],
maximum: samples[count - 1],
mean,
median,
quantile: samples[index],
}))
}