1#[derive(Debug, Clone, Copy, PartialEq)]
2pub struct Percentiles {
3 pub p50: f64,
4 pub p95: f64,
5 pub max: f64,
6}
7
8pub 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 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}