macro_rules! impl_hysteresis {
($name:ident, $ty:ty) => {
#[derive(Debug, Clone)]
pub struct $name {
low: $ty,
high: $ty,
state: bool,
}
impl $name {
#[inline]
pub fn new(
low_threshold: $ty,
high_threshold: $ty,
) -> Result<Self, crate::ConfigError> {
#[allow(clippy::neg_cmp_op_on_partial_ord)]
if !(low_threshold < high_threshold) {
return Err(crate::ConfigError::Invalid(
"low threshold must be less than high",
));
}
Ok(Self {
low: low_threshold,
high: high_threshold,
state: false,
})
}
/// Feeds a sample. Returns the current state.
#[inline]
#[must_use]
pub fn update(&mut self, sample: $ty) -> bool {
if sample >= self.high {
self.state = true;
} else if sample <= self.low {
self.state = false;
}
self.state
}
/// Current state.
#[inline]
#[must_use]
pub fn state(&self) -> bool {
self.state
}
/// Resets state to false.
#[inline]
pub fn reset(&mut self) {
self.state = false;
}
}
};
}
impl_hysteresis!(HysteresisF64, f64);
impl_hysteresis!(HysteresisF32, f32);
impl_hysteresis!(HysteresisI64, i64);
impl_hysteresis!(HysteresisI32, i32);
impl_hysteresis!(HysteresisI128, i128);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rising_crosses_high() {
let mut h = HysteresisF64::new(30.0, 70.0).unwrap();
assert!(!h.update(50.0)); // between thresholds, starts false
assert!(h.update(80.0)); // crosses high
}
#[test]
fn falling_crosses_low() {
let mut h = HysteresisF64::new(30.0, 70.0).unwrap();
let _ = h.update(80.0); // true
assert!(h.update(50.0)); // between, stays true
assert!(!h.update(20.0)); // crosses low
}
#[test]
fn no_oscillation_at_boundary() {
let mut h = HysteresisF64::new(30.0, 70.0).unwrap();
let _ = h.update(80.0); // true
// Oscillate between thresholds — state should not change
for _ in 0..10 {
assert!(h.update(50.0));
assert!(h.update(60.0));
assert!(h.update(40.0));
}
}
#[test]
fn i64_basic() {
let mut h = HysteresisI64::new(30, 70).unwrap();
assert!(!h.update(50));
assert!(h.update(75));
assert!(h.update(50)); // between, stays true
assert!(!h.update(25));
}
#[test]
fn reset() {
let mut h = HysteresisF64::new(30.0, 70.0).unwrap();
let _ = h.update(80.0);
h.reset();
assert!(!h.state());
}
#[test]
fn rejects_invalid_thresholds() {
assert!(matches!(
HysteresisF64::new(70.0, 30.0),
Err(crate::ConfigError::Invalid(_))
));
}
#[test]
fn i128_basic() {
let mut h = HysteresisI128::new(30, 70).unwrap();
assert!(!h.update(50));
assert!(h.update(75));
assert!(!h.update(25));
}
}