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