1#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum AverageError {
17 EmptyInput,
18 MismatchedLengths,
19 ZeroWeightSum,
20 NegativeValue,
21 ZeroValue,
22 InvalidWindow,
23}
24
25pub fn arithmetic_mean(values: &[f64]) -> Result<f64, AverageError> {
26 if values.is_empty() {
27 return Err(AverageError::EmptyInput);
28 }
29
30 Ok(values.iter().sum::<f64>() / values.len() as f64)
31}
32
33pub fn weighted_mean(values: &[f64], weights: &[f64]) -> Result<f64, AverageError> {
34 if values.is_empty() || weights.is_empty() {
35 return Err(AverageError::EmptyInput);
36 }
37
38 if values.len() != weights.len() {
39 return Err(AverageError::MismatchedLengths);
40 }
41
42 let weight_sum: f64 = weights.iter().sum();
43 if weight_sum == 0.0 {
44 return Err(AverageError::ZeroWeightSum);
45 }
46
47 let weighted_sum: f64 = values
48 .iter()
49 .zip(weights.iter())
50 .map(|(value, weight)| value * weight)
51 .sum();
52
53 Ok(weighted_sum / weight_sum)
54}
55
56pub fn geometric_mean(values: &[f64]) -> Result<f64, AverageError> {
57 if values.is_empty() {
58 return Err(AverageError::EmptyInput);
59 }
60
61 if values.iter().any(|value| *value < 0.0) {
62 return Err(AverageError::NegativeValue);
63 }
64
65 if values.contains(&0.0) {
66 return Ok(0.0);
67 }
68
69 let log_sum: f64 = values.iter().map(|value| value.ln()).sum();
70 Ok((log_sum / values.len() as f64).exp())
71}
72
73pub fn harmonic_mean(values: &[f64]) -> Result<f64, AverageError> {
74 if values.is_empty() {
75 return Err(AverageError::EmptyInput);
76 }
77
78 if values.contains(&0.0) {
79 return Err(AverageError::ZeroValue);
80 }
81
82 let reciprocal_sum: f64 = values.iter().map(|value| 1.0 / value).sum();
83 Ok(values.len() as f64 / reciprocal_sum)
84}
85
86pub fn moving_average(values: &[f64], window_size: usize) -> Result<Vec<f64>, AverageError> {
87 if values.is_empty() {
88 return Err(AverageError::EmptyInput);
89 }
90
91 if window_size == 0 || window_size > values.len() {
92 return Err(AverageError::InvalidWindow);
93 }
94
95 values
96 .windows(window_size)
97 .map(arithmetic_mean)
98 .collect::<Result<Vec<_>, _>>()
99}
100
101#[cfg(test)]
102mod tests {
103 use super::{
104 arithmetic_mean, geometric_mean, harmonic_mean, moving_average, weighted_mean, AverageError,
105 };
106
107 fn approx_eq(left: f64, right: f64) {
108 assert!((left - right).abs() < 1.0e-10, "left={left}, right={right}");
109 }
110
111 #[test]
112 fn computes_arithmetic_mean() {
113 approx_eq(arithmetic_mean(&[2.0, 4.0, 6.0, 8.0]).unwrap(), 5.0);
114 }
115
116 #[test]
117 fn computes_weighted_mean() {
118 approx_eq(
119 weighted_mean(&[80.0, 90.0, 70.0], &[0.2, 0.5, 0.3]).unwrap(),
120 82.0,
121 );
122 }
123
124 #[test]
125 fn computes_geometric_mean() {
126 approx_eq(geometric_mean(&[1.0, 4.0, 16.0]).unwrap(), 4.0);
127 }
128
129 #[test]
130 fn computes_harmonic_mean() {
131 approx_eq(harmonic_mean(&[1.0, 2.0, 4.0]).unwrap(), 12.0 / 7.0);
132 }
133
134 #[test]
135 fn computes_moving_average() {
136 assert_eq!(
137 moving_average(&[1.0, 2.0, 3.0, 4.0, 5.0], 3).unwrap(),
138 vec![2.0, 3.0, 4.0]
139 );
140 }
141
142 #[test]
143 fn returns_errors_for_invalid_average_inputs() {
144 assert_eq!(arithmetic_mean(&[]), Err(AverageError::EmptyInput));
145 assert_eq!(
146 weighted_mean(&[1.0, 2.0], &[1.0]),
147 Err(AverageError::MismatchedLengths)
148 );
149 assert_eq!(
150 weighted_mean(&[1.0, 2.0], &[0.0, 0.0]),
151 Err(AverageError::ZeroWeightSum)
152 );
153 assert_eq!(
154 geometric_mean(&[-1.0, 2.0]),
155 Err(AverageError::NegativeValue)
156 );
157 assert_eq!(harmonic_mean(&[1.0, 0.0]), Err(AverageError::ZeroValue));
158 assert_eq!(
159 moving_average(&[1.0, 2.0], 0),
160 Err(AverageError::InvalidWindow)
161 );
162 }
163
164 #[test]
165 fn handles_single_value_inputs() {
166 approx_eq(arithmetic_mean(&[9.0]).unwrap(), 9.0);
167 approx_eq(weighted_mean(&[9.0], &[3.0]).unwrap(), 9.0);
168 approx_eq(geometric_mean(&[9.0]).unwrap(), 9.0);
169 approx_eq(harmonic_mean(&[9.0]).unwrap(), 9.0);
170 assert_eq!(moving_average(&[9.0], 1).unwrap(), vec![9.0]);
171 }
172}