1#![forbid(unsafe_code)]
2#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct Setpoint {
19 pub target: f64,
20 pub tolerance: f64,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum SetpointError {
25 InvalidTarget,
26 InvalidTolerance,
27 InvalidMeasured,
28}
29
30impl Setpoint {
31 pub fn new(target: f64, tolerance: f64) -> Result<Self, SetpointError> {
32 if !target.is_finite() {
33 return Err(SetpointError::InvalidTarget);
34 }
35
36 if !tolerance.is_finite() || tolerance < 0.0 {
37 return Err(SetpointError::InvalidTolerance);
38 }
39
40 Ok(Self { target, tolerance })
41 }
42
43 pub fn is_reached(&self, measured: f64) -> bool {
44 measured.is_finite() && (self.target - measured).abs() <= self.tolerance
45 }
46
47 pub fn error(&self, measured: f64) -> f64 {
48 self.target - measured
49 }
50}
51
52pub fn within_tolerance(target: f64, measured: f64, tolerance: f64) -> Result<bool, SetpointError> {
53 let setpoint = Setpoint::new(target, tolerance)?;
54 if !measured.is_finite() {
55 return Err(SetpointError::InvalidMeasured);
56 }
57
58 Ok(setpoint.is_reached(measured))
59}
60
61#[cfg(test)]
62mod tests {
63 use super::{Setpoint, SetpointError, within_tolerance};
64
65 #[test]
66 fn checks_tolerance_and_error() {
67 let setpoint = Setpoint::new(10.0, 0.2).unwrap();
68
69 assert!(setpoint.is_reached(9.9));
70 assert!(!setpoint.is_reached(9.6));
71 assert_eq!(setpoint.error(9.5), 0.5);
72 assert!(within_tolerance(10.0, 10.1, 0.2).unwrap());
73 }
74
75 #[test]
76 fn rejects_invalid_inputs() {
77 assert_eq!(
78 Setpoint::new(f64::NAN, 0.1),
79 Err(SetpointError::InvalidTarget)
80 );
81 assert_eq!(
82 Setpoint::new(1.0, -0.1),
83 Err(SetpointError::InvalidTolerance)
84 );
85 assert_eq!(
86 within_tolerance(1.0, f64::NAN, 0.1),
87 Err(SetpointError::InvalidMeasured)
88 );
89 }
90}