no_std_moving_average/
moving_average.rs

1use core::{
2    cmp::PartialOrd,
3    fmt::Debug,
4    mem::size_of,
5    ops::{Add, Div, Mul, Sub},
6};
7use heapless::HistoryBuffer;
8
9/// # Intent
10/// Creates a Moving Average filter for integer values,
11/// in a nostd context. The filter uses a minimal calculation
12/// approach, and does not sum the entire buffer when finding
13/// the average.
14///
15/// # Instantiating `MovingAverage`
16///
17/// The `MovingAverage` type is generic over three values:
18///
19/// * T - the data type being averaged
20/// * TCALC - a larger data type for calculating the average
21///   * Must fit the value `N * T::MAX`
22/// * N - the depth of the average
23///   * Must be non-zero
24///
25/// # Example
26///
27/// ```rust
28/// use no_std_moving_average::MovingAverage;
29///
30/// let mut sut = MovingAverage::<u32, u64, 2>::new();
31/// let first: u32 = 22;
32/// let second: u32 = 44;
33/// let third: u32 = 66;
34/// let expected = (second + third) / 2;
35/// let _ = sut.average(first);
36/// let _ = sut.average(second);
37/// let result = sut.average(third);
38///
39/// assert_eq!(expected, result);
40/// ```
41///
42/// # Static and Allocation Asserts
43///
44/// A combination of compile-time and allocation time
45/// assertions are used to ensure `MovingAverage` is
46/// instantiated correctly. Once instantiated, there
47/// are no known Panics when operating `MovingAverage`.
48///
49/// ## T and TCALC must be Integer/Unsigned types
50///
51/// ```compile_fail
52/// use no_std_moving_average::MovingAverage;
53/// let _sut = MovingAverage::<f32, u64, 2>::new();
54/// ```
55///
56/// ```compile_fail
57/// use no_std_moving_average::MovingAverage;
58/// let _sut = MovingAverage::<u32, f64, 2>::new();
59/// ```
60///
61/// ```compile_fail
62/// use no_std_moving_average::MovingAverage;
63/// let _sut = MovingAverage::<f32, f64, 2>::new();
64/// ```
65///
66/// ## TCALC must be larger than T
67///
68/// ```compile_fail
69/// use no_std_moving_average::MovingAverage;
70/// let _sut = MovingAverage::<u32, u32, 1>::new();
71/// ```
72///
73/// ## N must be non-zero
74///
75/// ```compile_fail
76/// use no_std_moving_average::MovingAverage;
77/// let _sut = MovingAverage::<u32, u64, 0>::new();
78/// ```
79///
80/// ## N * `T::MAX` must fit in TCALC
81///
82/// ```should_panic
83/// use no_std_moving_average::MovingAverage;
84/// let _sut = MovingAverage::<u8, u16, 512>::new();
85/// ```
86///
87pub struct MovingAverage<T, TCALC, const N: usize>
88where
89    T: Sized + PartialEq + TryFrom<TCALC, Error: Debug> + Clone + Copy,
90    TCALC: Sized
91        + Add<TCALC, Output = TCALC>
92        + Sub<TCALC, Output = TCALC>
93        + Div<Output = TCALC>
94        + Mul<Output = TCALC>
95        + PartialEq
96        + PartialOrd
97        + From<T>
98        + TryFrom<usize, Error: Debug>
99        + Clone
100        + Copy,
101{
102    num: TCALC,
103    sum: Option<TCALC>,
104    buffer: HistoryBuffer<T, N>,
105}
106
107/// # Panics
108/// Panics if TCALC not larger than T, compile-time assert.
109/// Panics if N is zero, compile-time assert.
110/// : These panics should never occur due to compile-time assert checks.
111/// Panics if unable to convert from usize to TCALC.
112/// Panics if N * `T::MAX` won't fit in TCALC.
113/// : These panics happen at allocation time, so should be found predictably.
114#[expect(clippy::unwrap_used, reason = "Made safe by compile-time asserts")]
115impl<T, TCALC, const N: usize> Default for MovingAverage<T, TCALC, N>
116where
117    T: Sized + PartialEq + TryFrom<TCALC, Error: Debug> + Clone + Copy,
118    TCALC: Sized
119        + Add<TCALC, Output = TCALC>
120        + Sub<TCALC, Output = TCALC>
121        + Div<Output = TCALC>
122        + Mul<Output = TCALC>
123        + PartialEq
124        + PartialOrd
125        + From<T>
126        + TryFrom<usize, Error: Debug>
127        + Clone
128        + Copy,
129{
130    #[expect(
131        clippy::cast_possible_truncation,
132        reason = "no size_of return bigger than u32"
133    )]
134    fn default() -> Self {
135        const {
136            assert!(
137                size_of::<TCALC>() > size_of::<T>(),
138                "TCALC must be larger than T"
139            );
140            assert!(N > 0, "N must be non-zero");
141        }
142        assert!(
143            (2_u128.pow((size_of::<T>() as u32) * 8) * u128::try_from(N).unwrap())
144                <= 2_u128.pow((size_of::<TCALC>() as u32) * 8),
145            "N * T.max() must fit in TCALC"
146        );
147        Self {
148            num: TCALC::try_from(N).unwrap(),
149            sum: None,
150            buffer: HistoryBuffer::new(),
151        }
152    }
153}
154
155impl<T, TCALC, const N: usize> MovingAverage<T, TCALC, N>
156where
157    T: Sized + PartialEq + TryFrom<TCALC, Error: Debug> + Clone + Copy,
158    TCALC: Sized
159        + Add<TCALC, Output = TCALC>
160        + Sub<TCALC, Output = TCALC>
161        + Div<Output = TCALC>
162        + Mul<Output = TCALC>
163        + PartialEq
164        + PartialOrd
165        + From<T>
166        + TryFrom<usize, Error: Debug>
167        + Clone
168        + Copy,
169{
170    #[must_use]
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// # Panics
176    /// Panics if unable to convert from TCALC to T.
177    /// This panic should never occur due to compile-time assert checks.
178    #[must_use]
179    pub fn average(&mut self, input: T) -> T {
180        let new_value = TCALC::from(input);
181        let prev_sum = self.get_or_init_and_get_sum(input);
182        let remove = self.insert_new_value_pop_oldest_value(input);
183        self.create_average(new_value, prev_sum, remove)
184    }
185
186    fn get_or_init_and_get_sum(&mut self, input: T) -> TCALC {
187        let new_value = TCALC::from(input);
188        if let Some(sum) = self.sum {
189            sum
190        } else {
191            for _ in 0..N {
192                self.buffer.write(input);
193            }
194            self.num * new_value
195        }
196    }
197
198    fn insert_new_value_pop_oldest_value(&mut self, input: T) -> TCALC {
199        let remove = self.get_remove_value();
200        self.buffer.write(input);
201        remove
202    }
203
204    #[expect(clippy::expect_used, reason = "Made safe by compile-time asserts")]
205    fn create_average(&mut self, new_value: TCALC, prev_sum: TCALC, remove: TCALC) -> T {
206        let new_sum = prev_sum + new_value - remove;
207        self.sum = Some(new_sum);
208        let average_as_tcalc = new_sum / self.num;
209        T::try_from(average_as_tcalc).expect("Converting from TCALC to T should be safe")
210    }
211
212    #[expect(clippy::expect_used, reason = "Made safe by compile-time asserts")]
213    fn get_remove_value(&self) -> TCALC {
214        #[cfg(test)]
215        assert!(
216            self.buffer.len() == N,
217            "Buffer len {} different than capacity {N}.",
218            self.buffer.len()
219        );
220
221        TCALC::from(*self.buffer.first().expect("Buffer should be full"))
222    }
223}
224
225#[expect(clippy::let_underscore_must_use, reason = "Desirable in tests")]
226#[expect(clippy::let_underscore_untyped, reason = "Desirable in tests")]
227#[expect(clippy::cast_possible_truncation, reason = "Desirable in tests")]
228#[expect(clippy::cast_possible_wrap, reason = "Desirable in tests")]
229#[cfg(test)]
230mod tests {
231    use super::MovingAverage;
232
233    #[test]
234    fn given_new_moving_average_when_average_value_then_return_same_value() {
235        let mut sut = MovingAverage::<u32, u64, 1>::new();
236        let expected: u32 = 44;
237        assert_eq!(expected, sut.average(expected));
238    }
239
240    #[test]
241    fn given_two_item_moving_average_when_average_twice_value_then_return_average_of_those_values()
242    {
243        let mut sut = MovingAverage::<u32, u64, 2>::new();
244        let first: u32 = 22;
245        let second: u32 = 44;
246        let expected = (first + second) / 2;
247        let _ = sut.average(first);
248        assert_eq!(expected, sut.average(second));
249    }
250
251    #[test]
252    fn given_two_item_moving_average_when_average_called_thrice_then_return_average_of_the_last_two_values()
253     {
254        let mut sut = MovingAverage::<u32, u64, 2>::new();
255        let first: u32 = 22;
256        let second: u32 = 44;
257        let third: u32 = 66;
258        let expected = (second + third) / 2;
259        let _ = sut.average(first);
260        let _ = sut.average(second);
261        assert_eq!(expected, sut.average(third));
262    }
263
264    #[test]
265    fn given_two_signed_item_moving_average_when_average_called_thrice_then_return_average_of_the_last_two_values()
266     {
267        let mut sut = MovingAverage::<i32, i64, 2>::new();
268        let first: i32 = -22;
269        let second: i32 = 44;
270        let third: i32 = -66;
271        let expected = (second + third) / 2_i32;
272        let _ = sut.average(first);
273        let _ = sut.average(second);
274        assert_eq!(expected, sut.average(third));
275    }
276
277    #[test]
278    fn given_large_item_moving_average_when_average_called_thrice_then_return_average_of_the_last_two_values()
279     {
280        const DEPTH: usize = 128;
281        let mut sut = MovingAverage::<i32, i64, DEPTH>::new();
282        let first: i32 = -22;
283        let second: i32 = 44;
284        let third: i32 = -66;
285        let expected = (first + second + third + (((DEPTH as i32) - 3_i32) * first)) / DEPTH as i32;
286        let _ = sut.average(first);
287        let _ = sut.average(second);
288        assert_eq!(expected, sut.average(third));
289    }
290
291    #[test]
292    #[should_panic(expected = "N * T.max() must fit in TCALC")]
293    fn confirm_n_times_t_max_fits_in_tcalc() {
294        let _sut = MovingAverage::<u8, u16, 512>::new();
295    }
296
297    // fails at compile time, due to missing conversions
298    // #[test]
299    // #[should_panic(expected = "T must be an integer type")]
300    // fn confirm_t_is_an_integer_type() {
301    //     let _sut = MovingAverage::<f32, u64, 2>::new();
302    // }
303
304    // checked at compile time
305    // #[test]
306    // #[should_panic(expected = "TCALC must be larger than T")]
307    // fn confirm_tcalc_must_be_larger_than_t() {
308    //     let _sut = MovingAverage::<u32, u32, 1>::new();
309    // }
310
311    // checked at compile time
312    // #[test]
313    // #[should_panic(expected = "N must be non-zero")]
314    // fn confirm_n_must_be_non_zero() {
315    //     let _sut = MovingAverage::<u32, u64, 0>::new();
316    // }
317}