Skip to main content

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        debug_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(
222            *self
223                .buffer
224                .oldest_ordered()
225                .next()
226                .expect("Buffer should be full"),
227        )
228    }
229}
230
231#[expect(clippy::let_underscore_must_use, reason = "Desirable in tests")]
232#[expect(clippy::let_underscore_untyped, reason = "Desirable in tests")]
233#[expect(clippy::cast_possible_truncation, reason = "Desirable in tests")]
234#[expect(clippy::cast_possible_wrap, reason = "Desirable in tests")]
235#[cfg(test)]
236mod tests {
237    use super::MovingAverage;
238
239    #[test]
240    fn given_new_moving_average_when_average_value_then_return_same_value() {
241        let mut sut = MovingAverage::<u32, u64, 1>::new();
242        let expected: u32 = 44;
243        assert_eq!(expected, sut.average(expected));
244    }
245
246    #[test]
247    fn given_two_item_moving_average_when_average_twice_value_then_return_average_of_those_values()
248    {
249        let mut sut = MovingAverage::<u32, u64, 2>::new();
250        let first: u32 = 22;
251        let second: u32 = 44;
252        let expected = (first + second) / 2;
253        let _ = sut.average(first);
254        assert_eq!(expected, sut.average(second));
255    }
256
257    #[test]
258    fn given_two_item_moving_average_when_average_called_thrice_then_return_average_of_the_last_two_values()
259     {
260        let mut sut = MovingAverage::<u32, u64, 2>::new();
261        let first: u32 = 22;
262        let second: u32 = 44;
263        let third: u32 = 66;
264        let expected = (second + third) / 2;
265        let _ = sut.average(first);
266        let _ = sut.average(second);
267        assert_eq!(expected, sut.average(third));
268    }
269
270    #[test]
271    fn given_two_signed_item_moving_average_when_average_called_thrice_then_return_average_of_the_last_two_values()
272     {
273        let mut sut = MovingAverage::<i32, i64, 2>::new();
274        let first: i32 = -22;
275        let second: i32 = 44;
276        let third: i32 = -66;
277        let expected = (second + third) / 2_i32;
278        let _ = sut.average(first);
279        let _ = sut.average(second);
280        assert_eq!(expected, sut.average(third));
281    }
282
283    #[test]
284    fn given_large_item_moving_average_when_average_called_thrice_then_return_average_of_the_last_two_values()
285     {
286        const DEPTH: usize = 128;
287        let mut sut = MovingAverage::<i32, i64, DEPTH>::new();
288        let first: i32 = -22;
289        let second: i32 = 44;
290        let third: i32 = -66;
291        let expected = (first + second + third + (((DEPTH as i32) - 3_i32) * first)) / DEPTH as i32;
292        let _ = sut.average(first);
293        let _ = sut.average(second);
294        assert_eq!(expected, sut.average(third));
295    }
296
297    // Issue 1 replication case
298    // https://github.com/Radiator-Labs/no-std-moving-average-rs/issues/1
299    #[test]
300    fn given_enough_items_to_roll_over_when_average_called_then_return_average_of_the_correct_set_of_values()
301     {
302        let mut sut = MovingAverage::<u16, u32, 4>::new();
303        let sequence = [
304            100_u16, 200_u16, 300_u16, 400_u16, 100_u16, 200_u16, 300_u16, 400_u16, 100_u16,
305            200_u16, 300_u16, 400_u16,
306        ];
307        let expected = [
308            100_u16, 125_u16, 175_u16, 250_u16, 250_u16, 250_u16, 250_u16, 250_u16, 250_u16,
309            250_u16, 250_u16, 250_u16,
310        ];
311
312        for (i, val) in sequence.iter().enumerate() {
313            let avg = sut.average(*val);
314            assert_eq!(
315                expected[i], avg,
316                "Failed at {i}, is {avg}, should be {}",
317                expected[i]
318            );
319        }
320    }
321
322    #[test]
323    #[should_panic(expected = "N * T.max() must fit in TCALC")]
324    fn confirm_n_times_t_max_fits_in_tcalc() {
325        let _sut = MovingAverage::<u8, u16, 512>::new();
326    }
327
328    // fails at compile time, due to missing conversions
329    // #[test]
330    // #[should_panic(expected = "T must be an integer type")]
331    // fn confirm_t_is_an_integer_type() {
332    //     let _sut = MovingAverage::<f32, u64, 2>::new();
333    // }
334
335    // checked at compile time
336    // #[test]
337    // #[should_panic(expected = "TCALC must be larger than T")]
338    // fn confirm_tcalc_must_be_larger_than_t() {
339    //     let _sut = MovingAverage::<u32, u32, 1>::new();
340    // }
341
342    // checked at compile time
343    // #[test]
344    // #[should_panic(expected = "N must be non-zero")]
345    // fn confirm_n_must_be_non_zero() {
346    //     let _sut = MovingAverage::<u32, u64, 0>::new();
347    // }
348}