Skip to main content

elevator_core/components/
units.rs

1//! Physical quantity newtypes for compile-time unit safety.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Error returned when constructing a unit type from an invalid `f64`.
7///
8/// The value was not finite or was negative.
9///
10/// ```
11/// # use elevator_core::components::units::UnitError;
12/// let err = UnitError { unit: "Weight", value: f64::NAN };
13/// assert_eq!(
14///     format!("{err}"),
15///     "invalid Weight value: NaN (must be finite and non-negative)"
16/// );
17/// ```
18#[derive(Debug, Clone, PartialEq)]
19pub struct UnitError {
20    /// Name of the unit type that failed validation.
21    pub unit: &'static str,
22    /// The rejected value.
23    pub value: f64,
24}
25
26impl fmt::Display for UnitError {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        write!(
29            f,
30            "invalid {} value: {} (must be finite and non-negative)",
31            self.unit, self.value
32        )
33    }
34}
35
36impl std::error::Error for UnitError {}
37
38/// Implements `From<f64>` for a unit newtype by delegating to its `try_new`
39/// constructor and panicking on validation failure.
40///
41/// The panic is the documented contract for the infallible conversion;
42/// callers that need fallibility use `try_new` directly.
43macro_rules! impl_unit_from_f64 {
44    ($ty:ident) => {
45        #[allow(clippy::panic)]
46        impl From<f64> for $ty {
47            fn from(value: f64) -> Self {
48                Self::try_new(value).unwrap_or_else(|e| panic!("{e}"))
49            }
50        }
51    };
52}
53
54/// Weight / mass (always non-negative).
55///
56/// Used for rider weight, elevator load, and weight capacity.
57///
58/// ```
59/// # use elevator_core::components::Weight;
60/// let w = Weight::from(75.0);
61/// assert_eq!(w.value(), 75.0);
62/// assert_eq!(format!("{w}"), "75.00kg");
63/// ```
64#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
65#[serde(transparent)]
66pub struct Weight {
67    /// The inner f64 value.
68    pub(crate) value: f64,
69}
70
71impl Weight {
72    /// Zero weight.
73    pub const ZERO: Self = Self { value: 0.0 };
74
75    /// Fallible constructor — returns `Err` for NaN, infinity, or negative values.
76    ///
77    /// # Errors
78    ///
79    /// Returns [`UnitError`] if `value` is not finite or is negative.
80    ///
81    /// ```
82    /// # use elevator_core::components::Weight;
83    /// assert!(Weight::try_new(75.0).is_ok());
84    /// assert!(Weight::try_new(f64::NAN).is_err());
85    /// assert!(Weight::try_new(-1.0).is_err());
86    /// ```
87    pub fn try_new(value: f64) -> Result<Self, UnitError> {
88        if value.is_finite() && value >= 0.0 {
89            Ok(Self { value })
90        } else {
91            Err(UnitError {
92                unit: "Weight",
93                value,
94            })
95        }
96    }
97
98    /// The inner value.
99    #[must_use]
100    pub const fn value(self) -> f64 {
101        self.value
102    }
103}
104
105impl fmt::Display for Weight {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        write!(f, "{:.2}kg", self.value)
108    }
109}
110
111impl_unit_from_f64!(Weight);
112
113impl std::ops::Add for Weight {
114    type Output = Self;
115    fn add(self, rhs: Self) -> Self {
116        Self {
117            value: self.value + rhs.value,
118        }
119    }
120}
121
122impl std::ops::AddAssign for Weight {
123    fn add_assign(&mut self, rhs: Self) {
124        self.value += rhs.value;
125    }
126}
127
128impl std::ops::Sub for Weight {
129    type Output = Self;
130    fn sub(self, rhs: Self) -> Self {
131        Self {
132            value: (self.value - rhs.value).max(0.0),
133        }
134    }
135}
136
137impl std::ops::SubAssign for Weight {
138    fn sub_assign(&mut self, rhs: Self) {
139        self.value = (self.value - rhs.value).max(0.0);
140    }
141}
142
143/// Maximum travel speed (always non-negative, distance units per second).
144///
145/// ```
146/// # use elevator_core::components::Speed;
147/// let s = Speed::from(2.0);
148/// assert_eq!(format!("{s}"), "2.00m/s");
149/// ```
150#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
151#[serde(transparent)]
152pub struct Speed {
153    /// The inner f64 value.
154    pub(crate) value: f64,
155}
156
157impl Speed {
158    /// Fallible constructor — returns `Err` for NaN, infinity, or negative values.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`UnitError`] if `value` is not finite or is negative.
163    ///
164    /// ```
165    /// # use elevator_core::components::Speed;
166    /// assert!(Speed::try_new(2.0).is_ok());
167    /// assert!(Speed::try_new(f64::INFINITY).is_err());
168    /// ```
169    pub fn try_new(value: f64) -> Result<Self, UnitError> {
170        if value.is_finite() && value >= 0.0 {
171            Ok(Self { value })
172        } else {
173            Err(UnitError {
174                unit: "Speed",
175                value,
176            })
177        }
178    }
179
180    /// The inner value.
181    #[must_use]
182    pub const fn value(self) -> f64 {
183        self.value
184    }
185}
186
187impl fmt::Display for Speed {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        write!(f, "{:.2}m/s", self.value)
190    }
191}
192
193impl_unit_from_f64!(Speed);
194
195/// Acceleration / deceleration rate (always non-negative, distance units per second²).
196///
197/// ```
198/// # use elevator_core::components::Accel;
199/// let a = Accel::from(1.5);
200/// assert_eq!(format!("{a}"), "1.50m/s²");
201/// ```
202#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
203#[serde(transparent)]
204pub struct Accel {
205    /// The inner f64 value.
206    pub(crate) value: f64,
207}
208
209impl Accel {
210    /// Fallible constructor — returns `Err` for NaN, infinity, or negative values.
211    ///
212    /// # Errors
213    ///
214    /// Returns [`UnitError`] if `value` is not finite or is negative.
215    ///
216    /// ```
217    /// # use elevator_core::components::Accel;
218    /// assert!(Accel::try_new(1.5).is_ok());
219    /// assert!(Accel::try_new(-0.5).is_err());
220    /// ```
221    pub fn try_new(value: f64) -> Result<Self, UnitError> {
222        if value.is_finite() && value >= 0.0 {
223            Ok(Self { value })
224        } else {
225            Err(UnitError {
226                unit: "Accel",
227                value,
228            })
229        }
230    }
231
232    /// The inner value.
233    #[must_use]
234    pub const fn value(self) -> f64 {
235        self.value
236    }
237}
238
239impl fmt::Display for Accel {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        write!(f, "{:.2}m/s²", self.value)
242    }
243}
244
245impl_unit_from_f64!(Accel);