use std::time::Duration;
#[derive(Debug, Clone)]
pub struct Statistics {
samples: Vec<Duration>,
}
impl Statistics {
#[must_use]
pub fn from_durations(durations: &[Duration]) -> Self {
let mut samples = durations.to_vec();
samples.sort();
Self { samples }
}
#[must_use]
pub fn mean(&self) -> Duration {
if self.samples.is_empty() {
return Duration::ZERO;
}
let sum: Duration = self.samples.iter().sum();
sum / self.samples.len() as u32
}
#[must_use]
pub fn median(&self) -> Duration {
if self.samples.is_empty() {
return Duration::ZERO;
}
let mid = self.samples.len() / 2;
if self.samples.len().is_multiple_of(2) {
let sum = self.samples[mid - 1] + self.samples[mid];
sum / 2
} else {
self.samples[mid]
}
}
#[must_use]
pub fn min(&self) -> Duration {
self.samples.first().copied().unwrap_or(Duration::ZERO)
}
#[must_use]
pub fn max(&self) -> Duration {
self.samples.last().copied().unwrap_or(Duration::ZERO)
}
#[must_use]
pub fn std_dev(&self) -> Duration {
if self.samples.len() < 2 {
return Duration::ZERO;
}
let mean = self.mean().as_secs_f64();
let variance: f64 = self
.samples
.iter()
.map(|d| {
let diff = d.as_secs_f64() - mean;
diff * diff
})
.sum::<f64>()
/ (self.samples.len() - 1) as f64;
Duration::from_secs_f64(variance.sqrt())
}
#[must_use]
pub fn percentile(&self, p: f64) -> Duration {
if self.samples.is_empty() {
return Duration::ZERO;
}
let index = ((p / 100.0) * self.samples.len() as f64) as usize;
self.samples[index.min(self.samples.len() - 1)]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mean() {
let times = vec![
Duration::from_millis(10),
Duration::from_millis(20),
Duration::from_millis(30),
];
let stats = Statistics::from_durations(×);
assert_eq!(stats.mean(), Duration::from_millis(20));
}
#[test]
fn test_median_odd() {
let times = vec![
Duration::from_millis(10),
Duration::from_millis(20),
Duration::from_millis(30),
];
let stats = Statistics::from_durations(×);
assert_eq!(stats.median(), Duration::from_millis(20));
}
#[test]
fn test_median_even() {
let times = vec![
Duration::from_millis(10),
Duration::from_millis(20),
Duration::from_millis(30),
Duration::from_millis(40),
];
let stats = Statistics::from_durations(×);
assert_eq!(stats.median(), Duration::from_millis(25));
}
#[test]
fn test_min_max() {
let times = vec![
Duration::from_millis(30),
Duration::from_millis(10),
Duration::from_millis(20),
];
let stats = Statistics::from_durations(×);
assert_eq!(stats.min(), Duration::from_millis(10));
assert_eq!(stats.max(), Duration::from_millis(30));
}
#[test]
fn test_std_dev() {
let times = vec![
Duration::from_millis(10),
Duration::from_millis(20),
Duration::from_millis(30),
];
let stats = Statistics::from_durations(×);
let stddev_ms = stats.std_dev().as_millis();
assert!(
(9..=11).contains(&stddev_ms),
"StdDev should be ~10ms, got {stddev_ms}"
);
}
#[test]
fn test_percentiles() {
let times = vec![
Duration::from_millis(10),
Duration::from_millis(20),
Duration::from_millis(30),
Duration::from_millis(40),
Duration::from_millis(50),
];
let stats = Statistics::from_durations(×);
assert_eq!(stats.percentile(0.0), Duration::from_millis(10));
assert_eq!(stats.percentile(50.0), Duration::from_millis(30));
assert_eq!(stats.percentile(100.0), Duration::from_millis(50));
}
#[test]
fn test_empty_samples() {
let times: Vec<Duration> = vec![];
let stats = Statistics::from_durations(×);
assert_eq!(stats.mean(), Duration::ZERO);
assert_eq!(stats.median(), Duration::ZERO);
assert_eq!(stats.min(), Duration::ZERO);
assert_eq!(stats.max(), Duration::ZERO);
assert_eq!(stats.std_dev(), Duration::ZERO);
assert_eq!(stats.percentile(50.0), Duration::ZERO);
}
#[test]
fn prop_mean_bounds() {
use proptest::prelude::*;
proptest!(|(times in prop::collection::vec(0u64..10000, 2..100))| {
let durations: Vec<Duration> = times.iter()
.map(|&t| Duration::from_millis(t))
.collect();
let stats = Statistics::from_durations(&durations);
prop_assert!(stats.mean() >= stats.min());
prop_assert!(stats.mean() <= stats.max());
});
}
#[test]
fn prop_median_bounds() {
use proptest::prelude::*;
proptest!(|(times in prop::collection::vec(0u64..10000, 2..100))| {
let durations: Vec<Duration> = times.iter()
.map(|&t| Duration::from_millis(t))
.collect();
let stats = Statistics::from_durations(&durations);
prop_assert!(stats.median() >= stats.min());
prop_assert!(stats.median() <= stats.max());
});
}
#[test]
fn test_statistics_debug() {
let times = vec![Duration::from_millis(10), Duration::from_millis(20)];
let stats = Statistics::from_durations(×);
let debug_str = format!("{:?}", stats);
assert!(debug_str.contains("Statistics"));
assert!(debug_str.contains("samples"));
}
#[test]
fn test_statistics_clone() {
let times = vec![Duration::from_millis(10), Duration::from_millis(20)];
let stats = Statistics::from_durations(×);
let cloned = stats.clone();
assert_eq!(stats.mean(), cloned.mean());
assert_eq!(stats.median(), cloned.median());
}
#[test]
fn test_single_sample() {
let times = vec![Duration::from_millis(42)];
let stats = Statistics::from_durations(×);
assert_eq!(stats.mean(), Duration::from_millis(42));
assert_eq!(stats.median(), Duration::from_millis(42));
assert_eq!(stats.min(), Duration::from_millis(42));
assert_eq!(stats.max(), Duration::from_millis(42));
assert_eq!(stats.std_dev(), Duration::ZERO); assert_eq!(stats.percentile(50.0), Duration::from_millis(42));
}
#[test]
fn test_percentile_edge_cases() {
let times = vec![
Duration::from_millis(10),
Duration::from_millis(20),
Duration::from_millis(30),
];
let stats = Statistics::from_durations(×);
assert_eq!(stats.percentile(0.0), Duration::from_millis(10));
assert_eq!(stats.percentile(100.0), Duration::from_millis(30));
assert_eq!(stats.percentile(99.0), Duration::from_millis(30));
}
#[test]
fn test_samples_are_sorted() {
let times = vec![
Duration::from_millis(30),
Duration::from_millis(10),
Duration::from_millis(20),
];
let stats = Statistics::from_durations(×);
assert_eq!(stats.min(), Duration::from_millis(10));
assert_eq!(stats.max(), Duration::from_millis(30));
}
}