Skip to main content

qtty_core/
unit.rs

1//! Unit types and traits.
2
3use crate::dimension::{DimDiv, DimMul, Dimension, Dimensionless};
4use crate::scalar::Scalar;
5use crate::Quantity;
6use core::fmt::{Debug, Display, Formatter, LowerExp, Result, UpperExp};
7use core::marker::PhantomData;
8
9/// Trait implemented by every **unit** type.
10///
11/// * `RATIO` is the conversion factor from this unit to the *canonical scaling unit* of the same dimension.
12///   Example: if metres are canonical (`Meter::RATIO == 1.0`), then kilometres use `Kilometer::RATIO == 1000.0`
13///   because `1 km = 1000 m`.
14///
15/// * `SYMBOL` is the printable string (e.g. `"m"` or `"km"`).
16///
17/// * `Dim` ties the unit to its underlying [`Dimension`].
18///
19/// # Invariants
20///
21/// - Implementations should be zero-sized marker types (this crate's built-in units are unit structs with no fields).
22/// - `RATIO` should be finite and non-zero.
23pub trait Unit: Copy + PartialEq + Debug + 'static {
24    /// Unit-to-canonical conversion factor.
25    const RATIO: f64;
26
27    /// Dimension to which this unit belongs.
28    type Dim: Dimension;
29
30    /// Printable symbol, shown by [`core::fmt::Display`].
31    const SYMBOL: &'static str;
32}
33
34/// Unit representing the division of two other units.
35///
36/// `Per<N, D>` corresponds to `N / D` and carries both the
37/// dimensional information and the scaling ratio between the
38/// constituent units. It is generic over any numerator and
39/// denominator units, which allows implementing arithmetic
40/// generically for all pairs without bespoke macros.
41#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
42pub struct Per<N: Unit, D: Unit>(PhantomData<(N, D)>);
43
44impl<N: Unit, D: Unit> Unit for Per<N, D>
45where
46    N::Dim: DimDiv<D::Dim>,
47    <N::Dim as DimDiv<D::Dim>>::Output: Dimension,
48{
49    const RATIO: f64 = N::RATIO / D::RATIO;
50    type Dim = <N::Dim as DimDiv<D::Dim>>::Output;
51    const SYMBOL: &'static str = "";
52}
53
54impl<N: Unit, D: Unit, S: Scalar + Display> Display for Quantity<Per<N, D>, S>
55where
56    N::Dim: DimDiv<D::Dim>,
57    <N::Dim as DimDiv<D::Dim>>::Output: Dimension,
58{
59    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
60        Display::fmt(&self.value(), f)?;
61        write!(f, " {}/{}", N::SYMBOL, D::SYMBOL)
62    }
63}
64
65impl<N: Unit, D: Unit, S: Scalar + LowerExp> LowerExp for Quantity<Per<N, D>, S>
66where
67    N::Dim: DimDiv<D::Dim>,
68    <N::Dim as DimDiv<D::Dim>>::Output: Dimension,
69{
70    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
71        LowerExp::fmt(&self.value(), f)?;
72        write!(f, " {}/{}", N::SYMBOL, D::SYMBOL)
73    }
74}
75
76impl<N: Unit, D: Unit, S: Scalar + UpperExp> UpperExp for Quantity<Per<N, D>, S>
77where
78    N::Dim: DimDiv<D::Dim>,
79    <N::Dim as DimDiv<D::Dim>>::Output: Dimension,
80{
81    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
82        UpperExp::fmt(&self.value(), f)?;
83        write!(f, " {}/{}", N::SYMBOL, D::SYMBOL)
84    }
85}
86
87/// Unit representing the product of two other units.
88///
89/// `Prod<A, B>` corresponds to `A * B` and carries both the
90/// dimensional information and the scaling ratio between the
91/// constituent units.
92#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
93pub struct Prod<A: Unit, B: Unit>(PhantomData<(A, B)>);
94
95impl<A: Unit, B: Unit> Unit for Prod<A, B>
96where
97    A::Dim: DimMul<B::Dim>,
98    <A::Dim as DimMul<B::Dim>>::Output: Dimension,
99{
100    const RATIO: f64 = A::RATIO * B::RATIO;
101    type Dim = <A::Dim as DimMul<B::Dim>>::Output;
102    const SYMBOL: &'static str = "";
103}
104
105impl<A: Unit, B: Unit, S: Scalar + Display> Display for Quantity<Prod<A, B>, S>
106where
107    A::Dim: DimMul<B::Dim>,
108    <A::Dim as DimMul<B::Dim>>::Output: Dimension,
109{
110    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
111        Display::fmt(&self.value(), f)?;
112        write!(f, " {}·{}", A::SYMBOL, B::SYMBOL)
113    }
114}
115
116impl<A: Unit, B: Unit, S: Scalar + LowerExp> LowerExp for Quantity<Prod<A, B>, S>
117where
118    A::Dim: DimMul<B::Dim>,
119    <A::Dim as DimMul<B::Dim>>::Output: Dimension,
120{
121    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
122        LowerExp::fmt(&self.value(), f)?;
123        write!(f, " {}·{}", A::SYMBOL, B::SYMBOL)
124    }
125}
126
127impl<A: Unit, B: Unit, S: Scalar + UpperExp> UpperExp for Quantity<Prod<A, B>, S>
128where
129    A::Dim: DimMul<B::Dim>,
130    <A::Dim as DimMul<B::Dim>>::Output: Dimension,
131{
132    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
133        UpperExp::fmt(&self.value(), f)?;
134        write!(f, " {}·{}", A::SYMBOL, B::SYMBOL)
135    }
136}
137
138/// Zero-sized marker type for dimensionless quantities.
139///
140/// `Unitless` represents a dimensionless unit with a conversion ratio of 1.0
141/// and an empty symbol. It is used to model the result of simplifying same-unit
142/// ratios (e.g., `Meters / Meters`) into a plain "number-like" `Quantity<Unitless>`.
143///
144/// Unlike a type alias to `f64`, this is a proper zero-sized type, which ensures
145/// that only explicitly constructed `Quantity<Unitless>` values are treated as
146/// dimensionless, not bare `f64` primitives.
147#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
148pub struct Unitless;
149
150impl Unit for Unitless {
151    const RATIO: f64 = 1.0;
152    type Dim = Dimensionless;
153    const SYMBOL: &'static str = "";
154}
155
156impl<S: Scalar + Display> Display for Quantity<Unitless, S> {
157    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
158        Display::fmt(&self.value(), f)
159    }
160}
161
162impl<S: Scalar + LowerExp> LowerExp for Quantity<Unitless, S> {
163    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
164        LowerExp::fmt(&self.value(), f)
165    }
166}
167
168impl<S: Scalar + UpperExp> UpperExp for Quantity<Unitless, S> {
169    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
170        UpperExp::fmt(&self.value(), f)
171    }
172}
173
174/// Trait for simplifying composite unit types.
175///
176/// This allows reducing complex unit expressions to simpler forms,
177/// such as `Per<U, U>` to `Unitless` or `Per<N, Per<N, D>>` to `D`.
178pub trait Simplify {
179    /// The scalar type of this quantity.
180    type Scalar: Scalar;
181    /// The simplified unit type.
182    type Out: Unit;
183    /// Convert this quantity to its simplified unit.
184    fn simplify(self) -> Quantity<Self::Out, Self::Scalar>;
185}
186
187impl<U: Unit, S: Scalar> Simplify for Quantity<Per<U, U>, S>
188where
189    U::Dim: DimDiv<U::Dim>,
190    <U::Dim as DimDiv<U::Dim>>::Output: Dimension,
191{
192    type Scalar = S;
193    type Out = Unitless;
194    /// ```rust
195    /// use qtty_core::length::Meters;
196    /// use qtty_core::{Quantity, Simplify, Unitless};
197    ///
198    /// let ratio = Meters::new(1.0) / Meters::new(2.0);
199    /// let unitless: Quantity<Unitless> = ratio.simplify();
200    /// assert!((unitless.value() - 0.5).abs() < 1e-12);
201    /// ```
202    fn simplify(self) -> Quantity<Unitless, S> {
203        Quantity::new(self.value())
204    }
205}
206
207impl<N: Unit, D: Unit, S: Scalar> Simplify for Quantity<Per<N, Per<N, D>>, S>
208where
209    N::Dim: DimDiv<D::Dim>,
210    <N::Dim as DimDiv<D::Dim>>::Output: Dimension,
211    N::Dim: DimDiv<<N::Dim as DimDiv<D::Dim>>::Output>,
212    <N::Dim as DimDiv<<N::Dim as DimDiv<D::Dim>>::Output>>::Output: Dimension,
213{
214    type Scalar = S;
215    type Out = D;
216    fn simplify(self) -> Quantity<D, S> {
217        Quantity::new(self.value())
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::units::length::{Kilometer, Meter, Meters};
225    use crate::units::time::{Hour, Second};
226    use crate::Quantity;
227
228    // ── Per: Display, LowerExp, UpperExp ──────────────────────────────────────
229
230    #[test]
231    fn per_display_formats_value_and_symbol() {
232        // 10 m/s with Display
233        let qty: Quantity<Per<Meter, Second>> = Quantity::new(10.0);
234        let s = format!("{qty}");
235        assert_eq!(s, "10 m/s");
236    }
237
238    #[test]
239    fn per_display_with_precision() {
240        let qty: Quantity<Per<Meter, Second>> = Quantity::new(1.5);
241        let s = format!("{qty:.2}");
242        assert_eq!(s, "1.50 m/s");
243    }
244
245    #[test]
246    fn per_lower_exp_formats_correctly() {
247        let qty: Quantity<Per<Meter, Second>> = Quantity::new(1000.0);
248        let s = format!("{qty:e}");
249        assert!(s.contains("e"), "Expected scientific notation, got: {s}");
250        assert!(s.ends_with("m/s"), "Expected 'm/s' suffix, got: {s}");
251    }
252
253    #[test]
254    fn per_upper_exp_formats_correctly() {
255        let qty: Quantity<Per<Meter, Second>> = Quantity::new(1000.0);
256        let s = format!("{qty:E}");
257        assert!(s.contains("E"), "Expected uppercase-E notation, got: {s}");
258        assert!(s.ends_with("m/s"), "Expected 'm/s' suffix, got: {s}");
259    }
260
261    // ── Prod: Display, LowerExp, UpperExp ─────────────────────────────────────
262
263    #[test]
264    fn prod_display_formats_value_and_symbol() {
265        let qty: Quantity<Prod<Meter, Second>> = Quantity::new(3.0);
266        let s = format!("{qty}");
267        assert_eq!(s, "3 m·s");
268    }
269
270    #[test]
271    fn prod_display_with_precision() {
272        let qty: Quantity<Prod<Meter, Second>> = Quantity::new(2.5);
273        let s = format!("{qty:.3}");
274        assert_eq!(s, "2.500 m·s");
275    }
276
277    #[test]
278    fn prod_lower_exp_formats_correctly() {
279        let qty: Quantity<Prod<Kilometer, Second>> = Quantity::new(5000.0);
280        let s = format!("{qty:.2e}");
281        assert!(s.contains("e"), "Expected scientific notation, got: {s}");
282        assert!(s.ends_with("km·s"), "Expected 'km·s' suffix, got: {s}");
283    }
284
285    #[test]
286    fn prod_upper_exp_formats_correctly() {
287        let qty: Quantity<Prod<Kilometer, Second>> = Quantity::new(5000.0);
288        let s = format!("{qty:.2E}");
289        assert!(s.contains("E"), "Expected uppercase-E notation, got: {s}");
290        assert!(s.ends_with("km·s"), "Expected 'km·s' suffix, got: {s}");
291    }
292
293    // ── Unitless: LowerExp, UpperExp ──────────────────────────────────────────
294
295    #[test]
296    fn unitless_lower_exp_formats_correctly() {
297        let qty: Quantity<Unitless> = Quantity::new(0.5);
298        let s = format!("{qty:e}");
299        assert!(s.contains("e"), "Expected scientific notation, got: {s}");
300        // No unit symbol for Unitless
301        assert!(
302            !s.contains(' '),
303            "Unitless should not have a space, got: {s}"
304        );
305    }
306
307    #[test]
308    fn unitless_upper_exp_formats_correctly() {
309        let qty: Quantity<Unitless> = Quantity::new(0.5);
310        let s = format!("{qty:E}");
311        assert!(s.contains("E"), "Expected uppercase-E notation, got: {s}");
312    }
313
314    // ── Simplify: Per<U, U> → Unitless ────────────────────────────────────────
315
316    #[test]
317    fn simplify_per_u_u_gives_unitless() {
318        let ratio = Meters::new(3.0) / Meters::new(6.0);
319        let unitless: Quantity<Unitless> = ratio.simplify();
320        assert!((unitless.value() - 0.5).abs() < 1e-12);
321    }
322
323    // ── Simplify: Per<N, Per<N, D>> → D ──────────────────────────────────────
324
325    #[test]
326    fn simplify_per_n_per_n_d_gives_d() {
327        // Quantity<Per<Meter, Per<Meter, Hour>>> should simplify to Quantity<Hour>
328        let q: Quantity<Per<Meter, Per<Meter, Hour>>> = Quantity::new(42.0);
329        let simplified: Quantity<Hour> = q.simplify();
330        assert!((simplified.value() - 42.0).abs() < 1e-12);
331    }
332}