Skip to main content

use_average/
lib.rs

1//! Average helpers for `f64` slices.
2//!
3//! The crate stays intentionally small and focuses on the most common mean-style
4//! summaries without introducing a broader statistics framework.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use use_average::{arithmetic_mean, moving_average};
10//!
11//! assert_eq!(arithmetic_mean(&[2.0, 4.0, 6.0]).unwrap(), 4.0);
12//! assert_eq!(moving_average(&[1.0, 2.0, 3.0, 4.0], 2).unwrap(), vec![1.5, 2.5, 3.5]);
13//! ```
14
15#[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}