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);