1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
macro_rules! impl_dead_band {
($name:ident, $ty:ty, $zero:expr) => {
/// Dead band filter — suppresses small changes, reports significant ones.
///
/// Only emits a new value when the sample deviates from the last reported
/// value by more than the threshold. Prevents noisy oscillation around
/// a stable value from generating unnecessary updates.
///
/// # Use Cases
/// - Reducing update frequency for slowly-changing metrics
/// - Hysteresis-free change suppression
/// - Sensor noise filtering
#[derive(Debug, Clone)]
pub struct $name {
threshold: $ty,
last_reported: $ty,
initialized: bool,
}
impl $name {
/// Creates a new dead band filter with the given threshold.
#[inline]
#[must_use]
pub fn new(threshold: $ty) -> Self {
Self {
threshold,
last_reported: $zero,
initialized: false,
}
}
/// Feeds a sample. Returns `Some(value)` if the change exceeds
/// the threshold, `None` if suppressed.
///
/// The first sample is always reported.
#[inline]
#[must_use]
pub fn update(&mut self, sample: $ty) -> Option<$ty> {
if !self.initialized {
self.last_reported = sample;
self.initialized = true;
return Option::Some(sample);
}
let delta = sample - self.last_reported;
let abs_delta = if delta < $zero { $zero - delta } else { delta };
if abs_delta > self.threshold {
self.last_reported = sample;
Option::Some(sample)
} else {
Option::None
}
}
/// Last reported value, or `None` if no sample has been processed.
#[inline]
#[must_use]
pub fn last_reported(&self) -> Option<$ty> {
if self.initialized {
Option::Some(self.last_reported)
} else {
Option::None
}
}
/// Resets to uninitialized state.
#[inline]
pub fn reset(&mut self) {
self.last_reported = $zero;
self.initialized = false;
}
}
};
}
impl_dead_band!(DeadBandF64, f64, 0.0);
impl_dead_band!(DeadBandF32, f32, 0.0);
impl_dead_band!(DeadBandI64, i64, 0);
impl_dead_band!(DeadBandI32, i32, 0);
impl_dead_band!(DeadBandI128, i128, 0);
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(clippy::float_cmp)]
fn first_sample_always_reported() {
let mut db = DeadBandF64::new(5.0);
assert_eq!(db.update(100.0), Some(100.0));
}
#[test]
fn small_changes_suppressed() {
let mut db = DeadBandF64::new(5.0);
let _ = db.update(100.0);
assert_eq!(db.update(103.0), None); // within threshold
assert_eq!(db.update(99.0), None); // within threshold
}
#[test]
#[allow(clippy::float_cmp)]
fn large_changes_reported() {
let mut db = DeadBandF64::new(5.0);
let _ = db.update(100.0);
assert_eq!(db.update(110.0), Some(110.0)); // exceeds threshold
}
#[test]
fn i64_basic() {
let mut db = DeadBandI64::new(10);
assert_eq!(db.update(100), Some(100));
assert_eq!(db.update(105), None);
assert_eq!(db.update(115), Some(115));
}
#[test]
fn reset() {
let mut db = DeadBandF64::new(5.0);
let _ = db.update(100.0);
db.reset();
assert!(db.last_reported().is_none());
}
#[test]
fn i128_basic() {
let mut db = DeadBandI128::new(10);
assert_eq!(db.update(100), Some(100));
assert_eq!(db.update(105), None);
assert_eq!(db.update(115), Some(115));
}
}