1#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct Quartiles {
19 pub lower: f64,
20 pub median: f64,
21 pub upper: f64,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum PercentileError {
26 EmptyInput,
27 InvalidPercentile,
28}
29
30pub fn median(values: &[f64]) -> Result<f64, PercentileError> {
31 percentile(values, 50.0)
32}
33
34pub fn quartiles(values: &[f64]) -> Result<Quartiles, PercentileError> {
35 Ok(Quartiles {
36 lower: percentile(values, 25.0)?,
37 median: percentile(values, 50.0)?,
38 upper: percentile(values, 75.0)?,
39 })
40}
41
42pub fn percentile(values: &[f64], percentile: f64) -> Result<f64, PercentileError> {
43 if values.is_empty() {
44 return Err(PercentileError::EmptyInput);
45 }
46
47 if !(0.0..=100.0).contains(&percentile) {
48 return Err(PercentileError::InvalidPercentile);
49 }
50
51 let mut sorted = values.to_vec();
52 sorted.sort_by(f64::total_cmp);
53
54 let last_index = sorted.len() - 1;
55 let rank = percentile / 100.0 * last_index as f64;
56 let lower_index = rank.floor() as usize;
57 let upper_index = rank.ceil() as usize;
58
59 if lower_index == upper_index {
60 return Ok(sorted[lower_index]);
61 }
62
63 let weight = rank - lower_index as f64;
64 let lower = sorted[lower_index];
65 let upper = sorted[upper_index];
66 Ok(lower + (upper - lower) * weight)
67}
68
69pub fn percentile_rank(values: &[f64], value: f64) -> Result<f64, PercentileError> {
70 if values.is_empty() {
71 return Err(PercentileError::EmptyInput);
72 }
73
74 let count = values.iter().filter(|item| **item <= value).count();
75 Ok(count as f64 / values.len() as f64 * 100.0)
76}
77
78#[cfg(test)]
79mod tests {
80 use super::{median, percentile, percentile_rank, quartiles, PercentileError, Quartiles};
81
82 fn approx_eq(left: f64, right: f64) {
83 assert!((left - right).abs() < 1.0e-10, "left={left}, right={right}");
84 }
85
86 #[test]
87 fn computes_median_and_percentiles() {
88 let values = [15.0, 20.0, 35.0, 40.0, 50.0];
89
90 approx_eq(median(&values).unwrap(), 35.0);
91 approx_eq(percentile(&values, 40.0).unwrap(), 29.0);
92 approx_eq(percentile_rank(&values, 40.0).unwrap(), 80.0);
93 }
94
95 #[test]
96 fn computes_quartiles() {
97 let values = [15.0, 20.0, 35.0, 40.0, 50.0];
98
99 assert_eq!(
100 quartiles(&values).unwrap(),
101 Quartiles {
102 lower: 20.0,
103 median: 35.0,
104 upper: 40.0,
105 }
106 );
107 }
108
109 #[test]
110 fn handles_single_value_input() {
111 assert_eq!(median(&[4.0]).unwrap(), 4.0);
112 assert_eq!(
113 quartiles(&[4.0]).unwrap(),
114 Quartiles {
115 lower: 4.0,
116 median: 4.0,
117 upper: 4.0,
118 }
119 );
120 assert_eq!(percentile_rank(&[4.0], 4.0).unwrap(), 100.0);
121 }
122
123 #[test]
124 fn rejects_invalid_inputs() {
125 assert_eq!(median(&[]), Err(PercentileError::EmptyInput));
126 assert_eq!(
127 percentile(&[1.0, 2.0], -1.0),
128 Err(PercentileError::InvalidPercentile)
129 );
130 assert_eq!(
131 percentile(&[1.0, 2.0], 101.0),
132 Err(PercentileError::InvalidPercentile)
133 );
134 }
135}