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}