#![forbid(unsafe_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Setpoint {
pub target: f64,
pub tolerance: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetpointError {
InvalidTarget,
InvalidTolerance,
InvalidMeasured,
}
impl Setpoint {
pub fn new(target: f64, tolerance: f64) -> Result<Self, SetpointError> {
if !target.is_finite() {
return Err(SetpointError::InvalidTarget);
}
if !tolerance.is_finite() || tolerance < 0.0 {
return Err(SetpointError::InvalidTolerance);
}
Ok(Self { target, tolerance })
}
pub fn is_reached(&self, measured: f64) -> bool {
measured.is_finite() && (self.target - measured).abs() <= self.tolerance
}
pub fn error(&self, measured: f64) -> f64 {
self.target - measured
}
}
pub fn within_tolerance(target: f64, measured: f64, tolerance: f64) -> Result<bool, SetpointError> {
let setpoint = Setpoint::new(target, tolerance)?;
if !measured.is_finite() {
return Err(SetpointError::InvalidMeasured);
}
Ok(setpoint.is_reached(measured))
}
#[cfg(test)]
mod tests {
use super::{Setpoint, SetpointError, within_tolerance};
#[test]
fn checks_tolerance_and_error() {
let setpoint = Setpoint::new(10.0, 0.2).unwrap();
assert!(setpoint.is_reached(9.9));
assert!(!setpoint.is_reached(9.6));
assert_eq!(setpoint.error(9.5), 0.5);
assert!(within_tolerance(10.0, 10.1, 0.2).unwrap());
}
#[test]
fn rejects_invalid_inputs() {
assert_eq!(
Setpoint::new(f64::NAN, 0.1),
Err(SetpointError::InvalidTarget)
);
assert_eq!(
Setpoint::new(1.0, -0.1),
Err(SetpointError::InvalidTolerance)
);
assert_eq!(
within_tolerance(1.0, f64::NAN, 0.1),
Err(SetpointError::InvalidMeasured)
);
}
}