no_std_moving_average/
moving_average.rs1use core::{
2 cmp::PartialOrd,
3 fmt::Debug,
4 mem::size_of,
5 ops::{Add, Div, Mul, Sub},
6};
7use heapless::HistoryBuffer;
8
9pub 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#[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 #[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 }