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. The
57/// crate is unit-agnostic — "kg" is a convention but the engine
58/// supports any consistent unit (the space-elevator config uses
59/// tonnes). `Display` formats the bare numeric value to two decimals
60/// so hosts can suffix their own unit label.
61///
62/// # Arithmetic
63///
64/// `Add` / `AddAssign` sum normally. `Sub` / `SubAssign` **saturate
65/// at zero** — `Weight(50.0) - Weight(80.0) == Weight(0.0)` rather
66/// than panicking or returning a negative value, since the type
67/// constructor rejects negatives. Same saturating contract applies
68/// to [`Speed`] and [`Accel`].
69///
70/// ```
71/// # use elevator_core::components::Weight;
72/// let w = Weight::from(75.0);
73/// assert_eq!(w.value(), 75.0);
74/// assert_eq!(format!("{w}"), "75.00");
75/// ```
76#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
77#[serde(transparent)]
78pub struct Weight {
79    /// The inner f64 value.
80    pub(crate) value: f64,
81}
82
83impl Weight {
84    /// Zero weight.
85    pub const ZERO: Self = Self { value: 0.0 };
86
87    /// Fallible constructor — returns `Err` for NaN, infinity, or negative values.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`UnitError`] if `value` is not finite or is negative.
92    ///
93    /// ```
94    /// # use elevator_core::components::Weight;
95    /// assert!(Weight::try_new(75.0).is_ok());
96    /// assert!(Weight::try_new(f64::NAN).is_err());
97    /// assert!(Weight::try_new(-1.0).is_err());
98    /// ```
99    pub fn try_new(value: f64) -> Result<Self, UnitError> {
100        if value.is_finite() && value >= 0.0 {
101            Ok(Self { value })
102        } else {
103            Err(UnitError {
104                unit: "Weight",
105                value,
106            })
107        }
108    }
109
110    /// The inner value.
111    #[must_use]
112    pub const fn value(self) -> f64 {
113        self.value
114    }
115}
116
117impl fmt::Display for Weight {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(f, "{:.2}", self.value)
120    }
121}
122
123impl_unit_from_f64!(Weight);
124
125impl std::ops::Add for Weight {
126    type Output = Self;
127    fn add(self, rhs: Self) -> Self {
128        Self {
129            value: self.value + rhs.value,
130        }
131    }
132}
133
134impl std::ops::AddAssign for Weight {
135    fn add_assign(&mut self, rhs: Self) {
136        self.value += rhs.value;
137    }
138}
139
140impl std::ops::Sub for Weight {
141    type Output = Self;
142    fn sub(self, rhs: Self) -> Self {
143        Self {
144            value: (self.value - rhs.value).max(0.0),
145        }
146    }
147}
148
149impl std::ops::SubAssign for Weight {
150    fn sub_assign(&mut self, rhs: Self) {
151        self.value = (self.value - rhs.value).max(0.0);
152    }
153}
154
155/// Maximum travel speed (always non-negative, distance units per second).
156///
157/// Distance unit follows whatever the host chose for `Position` —
158/// the engine never converts. `Display` formats the bare numeric
159/// value to two decimals; hosts suffix their own unit label.
160///
161/// # Arithmetic
162///
163/// Same saturating contract as [`Weight`]: `Sub` / `SubAssign`
164/// clamp underflow to zero rather than producing a negative value
165/// the constructor would reject.
166///
167/// ```
168/// # use elevator_core::components::Speed;
169/// let s = Speed::from(2.0);
170/// assert_eq!(format!("{s}"), "2.00");
171/// ```
172#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
173#[serde(transparent)]
174pub struct Speed {
175    /// The inner f64 value.
176    pub(crate) value: f64,
177}
178
179impl Speed {
180    /// Fallible constructor — returns `Err` for NaN, infinity, or negative values.
181    ///
182    /// # Errors
183    ///
184    /// Returns [`UnitError`] if `value` is not finite or is negative.
185    ///
186    /// ```
187    /// # use elevator_core::components::Speed;
188    /// assert!(Speed::try_new(2.0).is_ok());
189    /// assert!(Speed::try_new(f64::INFINITY).is_err());
190    /// ```
191    pub fn try_new(value: f64) -> Result<Self, UnitError> {
192        if value.is_finite() && value >= 0.0 {
193            Ok(Self { value })
194        } else {
195            Err(UnitError {
196                unit: "Speed",
197                value,
198            })
199        }
200    }
201
202    /// The inner value.
203    #[must_use]
204    pub const fn value(self) -> f64 {
205        self.value
206    }
207}
208
209impl fmt::Display for Speed {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(f, "{:.2}", self.value)
212    }
213}
214
215impl_unit_from_f64!(Speed);
216
217impl std::ops::Add for Speed {
218    type Output = Self;
219    fn add(self, rhs: Self) -> Self {
220        Self {
221            value: self.value + rhs.value,
222        }
223    }
224}
225
226impl std::ops::AddAssign for Speed {
227    fn add_assign(&mut self, rhs: Self) {
228        self.value += rhs.value;
229    }
230}
231
232impl std::ops::Sub for Speed {
233    type Output = Self;
234    fn sub(self, rhs: Self) -> Self {
235        Self {
236            value: (self.value - rhs.value).max(0.0),
237        }
238    }
239}
240
241impl std::ops::SubAssign for Speed {
242    fn sub_assign(&mut self, rhs: Self) {
243        self.value = (self.value - rhs.value).max(0.0);
244    }
245}
246
247/// Acceleration / deceleration rate (always non-negative, distance units per second²).
248///
249/// Same unit-agnostic convention as [`Speed`] — `Display` is the bare
250/// numeric value to two decimals; hosts label the unit downstream.
251/// `Sub` / `SubAssign` saturate at zero, matching the [`Weight`]
252/// and [`Speed`] arithmetic contract.
253///
254/// ```
255/// # use elevator_core::components::Accel;
256/// let a = Accel::from(1.5);
257/// assert_eq!(format!("{a}"), "1.50");
258/// ```
259#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
260#[serde(transparent)]
261pub struct Accel {
262    /// The inner f64 value.
263    pub(crate) value: f64,
264}
265
266impl Accel {
267    /// Fallible constructor — returns `Err` for NaN, infinity, or negative values.
268    ///
269    /// # Errors
270    ///
271    /// Returns [`UnitError`] if `value` is not finite or is negative.
272    ///
273    /// ```
274    /// # use elevator_core::components::Accel;
275    /// assert!(Accel::try_new(1.5).is_ok());
276    /// assert!(Accel::try_new(-0.5).is_err());
277    /// ```
278    pub fn try_new(value: f64) -> Result<Self, UnitError> {
279        if value.is_finite() && value >= 0.0 {
280            Ok(Self { value })
281        } else {
282            Err(UnitError {
283                unit: "Accel",
284                value,
285            })
286        }
287    }
288
289    /// The inner value.
290    #[must_use]
291    pub const fn value(self) -> f64 {
292        self.value
293    }
294}
295
296impl fmt::Display for Accel {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        write!(f, "{:.2}", self.value)
299    }
300}
301
302impl_unit_from_f64!(Accel);
303
304impl std::ops::Add for Accel {
305    type Output = Self;
306    fn add(self, rhs: Self) -> Self {
307        Self {
308            value: self.value + rhs.value,
309        }
310    }
311}
312
313impl std::ops::AddAssign for Accel {
314    fn add_assign(&mut self, rhs: Self) {
315        self.value += rhs.value;
316    }
317}
318
319impl std::ops::Sub for Accel {
320    type Output = Self;
321    fn sub(self, rhs: Self) -> Self {
322        Self {
323            value: (self.value - rhs.value).max(0.0),
324        }
325    }
326}
327
328impl std::ops::SubAssign for Accel {
329    fn sub_assign(&mut self, rhs: Self) {
330        self.value = (self.value - rhs.value).max(0.0);
331    }
332}