Skip to main content

use_setpoint/
lib.rs

1#![forbid(unsafe_code)]
2//! Setpoint helpers for simple control loops.
3//!
4//! The crate keeps setpoints explicit by storing only a target and a tolerance.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use use_setpoint::{within_tolerance, Setpoint};
10//!
11//! let setpoint = Setpoint::new(10.0, 0.2).unwrap();
12//! assert!(setpoint.is_reached(9.9));
13//! assert!((setpoint.error(9.8) - 0.2).abs() < 1e-12);
14//! assert!(within_tolerance(10.0, 10.1, 0.2).unwrap());
15//! ```
16
17#[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}