Skip to main content

use_control_signal/
lib.rs

1#![forbid(unsafe_code)]
2//! Control signal primitives.
3//!
4//! The crate keeps control signal transformations explicit: construct, clamp,
5//! scale, or saturate a finite scalar value.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_control_signal::{clamp_signal, saturate, ControlSignal};
11//!
12//! let signal = ControlSignal::new(12.0).unwrap().clamp(0.0, 10.0).unwrap();
13//! assert_eq!(signal.value(), 10.0);
14//! assert_eq!(clamp_signal(2.5, 0.0, 2.0).unwrap(), 2.0);
15//! assert_eq!(saturate(-5.0, 2.0).unwrap(), -2.0);
16//! ```
17
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct ControlSignal {
20    value: f64,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ControlSignalError {
25    InvalidValue,
26    InvalidBounds,
27    InvalidFactor,
28}
29
30impl ControlSignal {
31    pub fn new(value: f64) -> Result<Self, ControlSignalError> {
32        if !value.is_finite() {
33            return Err(ControlSignalError::InvalidValue);
34        }
35
36        Ok(Self { value })
37    }
38
39    pub fn value(&self) -> f64 {
40        self.value
41    }
42
43    pub fn clamp(self, min: f64, max: f64) -> Result<Self, ControlSignalError> {
44        Self::new(clamp_signal(self.value, min, max)?)
45    }
46
47    pub fn scale(self, factor: f64) -> Result<Self, ControlSignalError> {
48        if !factor.is_finite() {
49            return Err(ControlSignalError::InvalidFactor);
50        }
51
52        let value = self.value * factor;
53        Self::new(value)
54    }
55}
56
57pub fn clamp_signal(value: f64, min: f64, max: f64) -> Result<f64, ControlSignalError> {
58    if !value.is_finite() || !min.is_finite() || !max.is_finite() {
59        return Err(ControlSignalError::InvalidValue);
60    }
61
62    if min > max {
63        return Err(ControlSignalError::InvalidBounds);
64    }
65
66    Ok(value.clamp(min, max))
67}
68
69pub fn saturate(value: f64, limit: f64) -> Result<f64, ControlSignalError> {
70    if !limit.is_finite() || limit < 0.0 {
71        return Err(ControlSignalError::InvalidBounds);
72    }
73
74    clamp_signal(value, -limit, limit)
75}
76
77#[cfg(test)]
78mod tests {
79    use super::{ControlSignal, ControlSignalError, clamp_signal, saturate};
80
81    #[test]
82    fn clamps_and_scales_signals() {
83        let signal = ControlSignal::new(12.0).unwrap().clamp(0.0, 10.0).unwrap();
84
85        assert_eq!(signal.value(), 10.0);
86        assert_eq!(signal.scale(0.5).unwrap().value(), 5.0);
87        assert_eq!(clamp_signal(-2.0, 0.0, 3.0).unwrap(), 0.0);
88    }
89
90    #[test]
91    fn saturates_signals() {
92        assert_eq!(saturate(7.0, 5.0).unwrap(), 5.0);
93        assert_eq!(saturate(-7.0, 5.0).unwrap(), -5.0);
94    }
95
96    #[test]
97    fn rejects_invalid_values() {
98        assert_eq!(
99            ControlSignal::new(f64::NAN),
100            Err(ControlSignalError::InvalidValue)
101        );
102        assert_eq!(
103            clamp_signal(1.0, 2.0, 1.0),
104            Err(ControlSignalError::InvalidBounds)
105        );
106        assert_eq!(
107            ControlSignal::new(1.0).unwrap().scale(f64::NAN),
108            Err(ControlSignalError::InvalidFactor)
109        );
110        assert_eq!(saturate(1.0, -1.0), Err(ControlSignalError::InvalidBounds));
111    }
112}