Skip to main content

har/
stats.rs

1#[derive(Debug, Clone, Copy, PartialEq)]
2pub struct Percentiles {
3    pub p50: f64,
4    pub p95: f64,
5    pub max: f64,
6}
7
8/// Nearest-rank percentiles over a set of values. Deterministic; does not mutate input.
9pub fn percentiles(values: &[f64]) -> Percentiles {
10    if values.is_empty() {
11        return Percentiles {
12            p50: 0.0,
13            p95: 0.0,
14            max: 0.0,
15        };
16    }
17    let mut v = values.to_vec();
18    v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
19    let n = v.len();
20    let pick = |p: f64| -> f64 {
21        let rank = ((p / 100.0) * n as f64).ceil() as usize;
22        let idx = rank.saturating_sub(1).min(n - 1);
23        v[idx]
24    };
25    Percentiles {
26        p50: pick(50.0),
27        p95: pick(95.0),
28        max: *v.last().unwrap(),
29    }
30}
31
32#[cfg(test)]
33mod tests {
34    use super::percentiles;
35
36    #[test]
37    fn empty_is_zero() {
38        let p = percentiles(&[]);
39        assert_eq!(p.p50, 0.0);
40        assert_eq!(p.p95, 0.0);
41        assert_eq!(p.max, 0.0);
42    }
43
44    #[test]
45    fn single_value() {
46        let p = percentiles(&[42.0]);
47        assert_eq!(p.p50, 42.0);
48        assert_eq!(p.p95, 42.0);
49        assert_eq!(p.max, 42.0);
50    }
51
52    #[test]
53    fn nearest_rank_five_values() {
54        // sorted: 10,20,30,40,50 ; p50 -> rank ceil(2.5)=3 -> 30 ; p95 -> rank ceil(4.75)=5 -> 50
55        let p = percentiles(&[50.0, 10.0, 40.0, 20.0, 30.0]);
56        assert_eq!(p.p50, 30.0);
57        assert_eq!(p.p95, 50.0);
58        assert_eq!(p.max, 50.0);
59    }
60}