Skip to content

Commit 4da8991

Browse files
feat: add thresholding
1 parent 8feb801 commit 4da8991

File tree

2 files changed

+185
-54
lines changed

2 files changed

+185
-54
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ documentation = "https://docs.rs/moving-average"
1313

1414

1515
[dependencies]
16+
num-traits = "0.2.19"

src/lib.rs

Lines changed: 184 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -34,44 +34,58 @@
3434
//! assert_eq!(moving_average, 15);
3535
//! ```
3636
37-
use std::ops::{AddAssign, Deref};
37+
use num_traits::ToPrimitive;
38+
use std::{fmt::Display, ops::{AddAssign, Deref}};
3839

39-
macro_rules! from_size {
40-
($($ty:ty),*) => {
41-
$(
42-
impl FromUsize for $ty {
43-
fn from_usize(value: usize) -> Self {
44-
value as Self
45-
}
46-
}
40+
#[derive(Debug, Clone)]
41+
pub struct Value(f64);
42+
43+
impl Deref for Value {
44+
type Target = f64;
45+
46+
fn deref(&self) -> &Self::Target {
47+
&self.0
48+
}
49+
}
50+
51+
impl Display for Value {
52+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53+
write!(f, "{}", self.0)
54+
}
55+
}
4756

48-
impl ToFloat64 for $ty {
49-
fn to_f64(self) -> f64 {
50-
self as f64
57+
58+
macro_rules! impl_partial_eq {
59+
($($ty:ty), *) => {
60+
$(
61+
impl PartialEq<$ty> for Value {
62+
fn eq(&self, other: &$ty) -> bool {
63+
self.0 == *other as f64
5164
}
5265
}
5366
)*
5467
};
68+
() => {
69+
70+
};
5571
}
5672

57-
macro_rules! assign_types {
73+
74+
impl_partial_eq!(usize, i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f32, f64);
75+
76+
type MovingResult = Result<Value, MovingError>;
77+
78+
macro_rules! utilities {
5879
($($ty:ty),*) => {
5980
$(
81+
6082
impl AddAssign<$ty> for Moving<$ty> {
6183
fn add_assign(&mut self, other: $ty) {
62-
self.add(other);
84+
let _ = self.add(other);
6385
}
6486
}
6587

66-
)*
67-
68-
69-
};
70-
}
7188

72-
macro_rules! partials {
73-
($($ty:ty),*) => {
74-
$(
7589
impl PartialEq<$ty> for Moving<$ty> {
7690
fn eq(&self, other: &$ty) -> bool {
7791
self.mean == *other as f64
@@ -96,6 +110,7 @@ macro_rules! partials {
96110
}
97111
}
98112

113+
99114
)*
100115

101116
};
@@ -115,7 +130,6 @@ macro_rules! partial_non {
115130
self.mean == *other
116131
}
117132
}
118-
119133
)*
120134

121135
};
@@ -125,8 +139,8 @@ macro_rules! signed {
125139
($($ty:ty), *) => {
126140
$(
127141
impl Sign for $ty {
128-
fn is_unsigned() -> bool {
129-
false
142+
fn signed() -> bool {
143+
true
130144
}
131145
}
132146
)*
@@ -136,17 +150,15 @@ macro_rules! unsigned {
136150
($($ty:ty), *) => {
137151
$(
138152
impl Sign for $ty {
139-
fn is_unsigned() -> bool {
140-
true
153+
fn signed() -> bool {
154+
false
141155
}
142156
}
143157
)*
144158
};
145159
}
146160

147-
from_size!(usize, i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f32, f64);
148-
assign_types!(usize, i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f32, f64);
149-
partials!(usize, i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f32, f64);
161+
utilities!(usize, i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f32, f64);
150162
partial_non!(usize, i8, i16, i32, i64, i128, u8, u16, u32, u64, u128);
151163
signed!(i8, i16, i32, i64, i128, f32, f64);
152164
unsigned!(usize, u8, u16, u32, u64, u128);
@@ -155,37 +167,128 @@ unsigned!(usize, u8, u16, u32, u64, u128);
155167
pub struct Moving<T> {
156168
count: usize,
157169
mean: f64,
170+
is_error: bool,
171+
threshold: f64,
158172
phantom: std::marker::PhantomData<T>,
159173
}
160174

161-
pub trait FromUsize {
162-
fn from_usize(value: usize) -> Self;
175+
pub trait Sign {
176+
fn signed() -> bool;
163177
}
164178

165-
pub trait ToFloat64 {
166-
fn to_f64(self) -> f64;
167-
}
168179

169-
pub trait Sign {
170-
fn is_unsigned() -> bool;
180+
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
181+
/// Represents the possible errors that can occur in the `Moving` struct.
182+
pub enum MovingError {
183+
/// Error indicating that a negative value was attempted to be added to an unsigned type.
184+
NegativeValueToUnsignedType,
185+
186+
/// Error indicating that an overflow occurred during an operation.
187+
/// Note: This is unlikely to occur with floating-point operations.
188+
Overflow,
189+
190+
/// Error indicating that an underflow occurred during an operation.
191+
Underflow,
192+
193+
/// Error indicating that the count of values has overflowed.
194+
CountOverflow,
195+
196+
/// Error indicating that a value has reached or exceeded the specified threshold.
197+
///
198+
/// This error is triggered when a value added to the `Moving` instance meets or exceeds
199+
/// the threshold value specified during the creation of the instance. This can be used
200+
/// to signal that a certain limit has been reached, which might require special handling
201+
/// or termination of further processing.
202+
ThresholdReached,
171203
}
172204

173205
impl<T> Moving<T>
174206
where
175-
T: FromUsize + ToFloat64 + Sign,
207+
T: Sign + ToPrimitive,
176208
{
209+
/// Creates a new [`Moving<T>`] instance with default values.
210+
///
211+
/// # Returns
212+
///
213+
/// A new instance of [`Moving<T>`].
214+
/// Values can ge added to this instance to calculate the moving average.
177215
pub fn new() -> Self {
178216
Self {
179217
count: 0,
180218
mean: 0.0,
219+
is_error: false,
220+
threshold: f64::MAX,
221+
phantom: std::marker::PhantomData,
222+
}
223+
}
224+
225+
/// Creates a new [`Moving<T>`] instance with a specified threshold.
226+
///
227+
/// This method initializes the `count` to 0, `mean` to 0.0, `is_error` to `false`,
228+
/// and `threshold` to the provided value.
229+
///
230+
/// # Parameters
231+
///
232+
/// - `threshold`: The threshold value to be used for the new instance.
233+
///
234+
/// # Returns
235+
///
236+
/// A new instance of [`Moving<T>`] with the specified threshold.
237+
/// Values can be added to this instance to calculate the moving average.
238+
/// When values are greater than or equal to the threshold, the [`MovingResults::ThresholdReached`] variant is returned and no further values are added.
239+
pub fn new_with_threshold(threshold: f64) -> Self {
240+
Self {
241+
count: 0,
242+
mean: 0.0,
243+
is_error: false,
244+
threshold,
181245
phantom: std::marker::PhantomData,
182246
}
183247
}
248+
/// Adds a value to the current statistical collection, updating the mean accordingly.
249+
///
250+
/// This function converts the input value to an `f64` and then updates the mean of the collection
251+
/// based on the new value.
252+
///
253+
/// # Returns
254+
/// If the mean is less than the threshold, the [`MovingResults::Value`] variant is returned with the new mean.
255+
///
256+
/// # Panics
257+
///
258+
/// Panics if the type `T` is unsigned and a negative value is attempted to be added. This is because
259+
/// negative values are not allowed for unsigned types. If negative values are needed, it is recommended
260+
/// to use signed types instead.
261+
pub fn add(&mut self, value: T) -> MovingResult {
262+
let value = value.to_f64().unwrap();
263+
264+
if self.signed() && value < 0.0 {
265+
self.is_error = true;
266+
return Err(MovingError::NegativeValueToUnsignedType);
267+
}
268+
269+
let count = self
270+
.count
271+
.checked_add(1)
272+
.ok_or(MovingError::CountOverflow)?;
273+
self.count = count;
184274

185-
pub fn add(&mut self, value: T) {
186-
let value = T::to_f64(value);
187-
self.count += 1;
188275
self.mean += (value - self.mean) / self.count as f64;
276+
277+
if self.mean.is_infinite() {
278+
self.is_error = true;
279+
return Err(MovingError::Overflow);
280+
}
281+
282+
if self.mean >= self.threshold {
283+
return Err(MovingError::ThresholdReached);
284+
}
285+
286+
Ok(Value(self.mean))
287+
288+
}
289+
290+
fn signed(&self) -> bool {
291+
T::signed()
189292
}
190293
}
191294

@@ -203,47 +306,74 @@ impl<T> std::fmt::Display for Moving<T> {
203306
}
204307
}
205308

309+
impl std::fmt::Display for MovingError {
310+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311+
write!(f, "{:?}", self)
312+
}
313+
}
206314
#[cfg(test)]
207315
mod tests {
208-
use super::*;
316+
use crate::Moving;
317+
318+
#[test]
319+
fn thresholds() {
320+
let mut moving_threshold = Moving::new_with_threshold(10.0);
321+
let result = moving_threshold.add(9);
322+
assert_eq!(*result.unwrap(), 9.0);
323+
let result = moving_threshold.add(15);
324+
assert!(result.is_err(),"{:?}", result);
325+
assert_eq!(result.unwrap_err(), crate::MovingError::ThresholdReached);
326+
}
327+
328+
#[test]
329+
fn never_overflow() {
330+
let mut moving_average: Moving<usize> = Moving::new();
331+
let result = moving_average.add(usize::MAX);
332+
assert!(result.is_ok());
333+
assert_eq!(result.unwrap(), usize::MAX as f64);
334+
let result = moving_average.add(usize::MAX);
335+
assert!(result.is_ok());
336+
337+
assert_eq!(*result.unwrap(), usize::MAX as f64);
338+
}
209339

210340
#[test]
211341
fn add_moving_average() {
212342
let mut moving_average: Moving<usize> = Moving::new();
213-
moving_average.add(10);
343+
let _ = moving_average.add(10);
214344
assert_eq!(moving_average, 10);
215-
moving_average.add(20);
345+
let _ = moving_average.add(20);
216346
assert_eq!(moving_average, 15);
217347
}
218348

219349
#[test]
220350
fn float_moving_average() {
221351
let mut moving_average: Moving<f32> = Moving::new();
222-
moving_average.add(10.0);
223-
moving_average.add(20.0);
352+
let _ = moving_average.add(10.0);
353+
let _ = moving_average.add(20.0);
224354
assert_eq!(moving_average, 15.0);
225355
}
226356

227357
#[test]
228358
fn assign_add() {
229359
let mut moving_average: Moving<usize> = Moving::new();
230-
moving_average.add(10);
360+
let _ = moving_average.add(10);
231361
moving_average += 20;
232362
assert_eq!(moving_average, 15);
233363
}
234364

235365
#[test]
236366
fn assign_add_float() {
237367
let mut moving_average: Moving<f32> = Moving::new();
238-
moving_average.add(10.0);
368+
let _ = moving_average.add(10.0);
239369
moving_average += 20.0;
240370
assert_eq!(moving_average, 15.0);
241371
}
242372

243373
#[test]
244374
fn assign_add_i64() {
245375
let mut moving_average: Moving<i64> = Moving::new();
246-
moving_average.add(10);
376+
let _ = moving_average.add(10);
247377
moving_average += 20;
248378
assert_eq!(moving_average, 15);
249379
}
@@ -258,24 +388,24 @@ mod tests {
258388
#[test]
259389
fn binary_operations() {
260390
let mut moving_average: Moving<usize> = Moving::new();
261-
moving_average.add(10);
262-
moving_average.add(20);
391+
let _ = moving_average.add(10);
392+
let _ = moving_average.add(20);
263393
assert!(moving_average < usize::MAX)
264394
}
265395

266396
#[test]
267397
fn binary_operations_float() {
268398
let mut moving_average: Moving<f32> = Moving::new();
269-
moving_average.add(10.0);
270-
moving_average.add(20.0);
399+
let _ = moving_average.add(10.0);
400+
let _ = moving_average.add(20.0);
271401
assert!(moving_average < f32::MAX)
272402
}
273403

274404
#[test]
275405
fn many_operations() {
276406
let mut moving_average: Moving<_> = Moving::new();
277407
for i in 0..1000 {
278-
moving_average.add(i);
408+
let _ = moving_average.add(i);
279409
}
280410
assert_eq!(moving_average, 999.0 / 2.0);
281411
}

0 commit comments

Comments
 (0)