Skip to main content

uy/
lib.rs

1// When compiling without `std`, the `core_float_math` nightly feature is required.
2#![cfg_attr(not(feature = "std"), no_std)]
3#![cfg_attr(not(feature = "std"), feature(core_float_math))]
4// Unit test code examples in the README.
5#![doc = include_str!("../README.md")]
6
7use core::ops;
8
9mod inner;
10mod quantity;
11pub mod si;
12
13pub use crate::quantity::Quantity;
14
15/// Used for multiplying a unit by 10ⁿ.
16///
17/// ```rust
18/// type Millimeter = uy::Mul<uy::si::m, uy::TenTo<-3>>;
19/// ```
20pub struct TenTo<const N: i8>;
21
22/// Multiply by a power of ten.
23pub trait MulPowerOfTen {
24    fn mul_power_of_ten(self, exp: i8) -> Self;
25}
26
27macro_rules! impl_mul_power_of_ten {
28    ($($ty:ty),*) => {
29        $(
30            impl MulPowerOfTen for $ty {
31                fn mul_power_of_ten(self, exp: i8) -> Self {
32                    if exp < 0 {
33                        self * (10 as $ty).pow(-exp as u32)
34                    } else {
35                        self / (10 as $ty).pow(exp as u32)
36                    }
37                }
38            }
39        )*
40    };
41}
42
43impl_mul_power_of_ten!(i8, i16, i32, i64, isize, u8, u16, u32, u64, u128);
44
45impl MulPowerOfTen for f32 {
46    #[cfg(feature = "std")]
47    fn mul_power_of_ten(self, exp: i8) -> Self {
48        self * 10f32.powi(-exp as i32)
49    }
50
51    #[cfg(not(feature = "std"))]
52    fn mul_power_of_ten(self, exp: i8) -> Self {
53        self * core::f32::math::powi(10.0, -exp as i32)
54    }
55}
56
57impl MulPowerOfTen for f64 {
58    #[cfg(feature = "std")]
59    fn mul_power_of_ten(self, exp: i8) -> Self {
60        self * 10f64.powi(-exp as i32)
61    }
62
63    #[cfg(not(feature = "std"))]
64    fn mul_power_of_ten(self, exp: i8) -> Self {
65        self * core::f64::math::powi(10.0, -exp as i32)
66    }
67}
68
69/// Marker trait for unit systems.
70pub trait Unit {}
71
72macro_rules! power_of_ten_unit_system {
73    ($system:ident { $($unit:ident),* }) => {
74        ::paste::paste! {
75            pub struct [<Typenum $system>]<EXP, $([<$unit:camel>]),*>(core::marker::PhantomData<(EXP, $([<$unit:camel>]),*)>);
76
77            impl<const EXP: i8, $(const [<$unit:upper>]: i8),*> crate::inner::ToConst for [<Typenum $system>]<crate::inner::Const<EXP>, $(crate::inner::Const<{ [<$unit:upper>] }>),*> {
78                type Output = $system<EXP, $({ [<$unit:upper>] }),*>;
79                fn to_const(self) -> Self::Output { $system }
80            }
81
82            /// Encoding of the unit system in the type system.
83            #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
84            pub struct $system<const EXP: i8, $(const [<$unit:upper>]: i8),*>;
85
86            /// Encoding of the unit system as a value that can be interacted with at runtime.
87            #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
88            #[allow(non_snake_case)]
89            pub struct[<Runtime $system>] {
90                $(pub $unit: i8),*
91            }
92
93            /// Trait for converting from typesystem units to runtime units.
94            pub trait [<ToRuntime $system>] {
95                const VALUE: [<Runtime $system>];
96            }
97
98            impl<const EXP: i8, $(const [<$unit:upper>]: i8),*> [<ToRuntime $system>] for $system<EXP, $({ [<$unit:upper>] }),*> {
99                const VALUE: [<Runtime $system>] = [<Runtime $system>] {
100                    $($unit: [<$unit:upper>]),*
101                };
102            }
103
104            impl<const EXP: i8, $(const [<$unit:upper>]: i8),*> crate::Unit for $system<EXP, $({ [<$unit:upper>] }),*> {}
105
106            impl<
107                const EXP: i8,
108                const N: i8,
109                $(const [<$unit:upper>]: i8),*
110            > core::ops::Mul<crate::TenTo<{ N }>> for $system<EXP, $({ [<$unit:upper>] }),*>
111            where
112                crate::inner::Const<EXP>: core::ops::Add<crate::inner::Const<N>>,
113                [<Typenum $system>]<
114                    <crate::inner::Const<EXP> as core::ops::Add<crate::inner::Const<N>>>::Output,
115                    $( crate::inner::Const<{ [<$unit:upper>] }> ),*
116                >: crate::inner::ToConst,
117            {
118                type Output = <[<Typenum $system>]<
119                    <crate::inner::Const<EXP> as core::ops::Add<crate::inner::Const<N>>>::Output,
120                    $( crate::inner::Const<{ [<$unit:upper>] }> ),*
121                > as crate::inner::ToConst>::Output;
122
123                fn mul(self, _rhs: crate::TenTo<N>) -> Self::Output {
124                    crate::inner::ToConst::to_const([<Typenum $system>](core::marker::PhantomData))
125                }
126            }
127
128            impl<
129                const EXP: i8,
130                const N: i8,
131                $(const [<$unit:upper>]: i8),*
132            > core::ops::Div<crate::TenTo<N>> for $system<EXP, $({ [<$unit:upper>] }),*>
133            where
134                crate::inner::Const<EXP>: core::ops::Sub<crate::inner::Const<N>>,
135                [<Typenum $system>]<
136                    <crate::inner::Const<EXP> as core::ops::Sub<crate::inner::Const<N>>>::Output,
137                    $( crate::inner::Const<{ [<$unit:upper>] }> ),*
138                >: crate::inner::ToConst,
139            {
140                type Output = <[<Typenum $system>]<
141                    <crate::inner::Const<EXP> as core::ops::Sub<crate::inner::Const<N>>>::Output,
142                    $( crate::inner::Const<{ [<$unit:upper>] }> ),*
143                > as crate::inner::ToConst>::Output;
144
145                fn div(self, _rhs: crate::TenTo<N>) -> Self::Output {
146                    crate::inner::ToConst::to_const([<Typenum $system>](core::marker::PhantomData))
147                }
148            }
149
150            impl<
151                const EXP1: i8,
152                const EXP2: i8,
153                $(const [<$unit:upper 1>]: i8, const [<$unit:upper 2>]: i8),*
154            > core::ops::Mul<$system<EXP2, $({ [<$unit:upper 2>] }),*>> for $system<EXP1, $({ [<$unit:upper 1>] }),*>
155            where
156                crate::inner::Const<EXP1>: core::ops::Add<crate::inner::Const<EXP2>>,
157
158                $( crate::inner::Const<{ [<$unit:upper 1>] }>: core::ops::Add<crate::inner::Const<{ [<$unit:upper 2>] }>>, )*
159                [<Typenum $system>]<
160                    <crate::inner::Const<EXP1> as core::ops::Add<crate::inner::Const<EXP2>>>::Output,
161                    $( <crate::inner::Const<{ [<$unit:upper 1>] }> as core::ops::Add<crate::inner::Const<{ [<$unit:upper 2>] }>>>::Output ),*
162                >: crate::inner::ToConst,
163            {
164                type Output = <[<Typenum $system>]<
165                    <crate::inner::Const<EXP1> as core::ops::Add<crate::inner::Const<EXP2>>>::Output,
166                    $( <crate::inner::Const<{ [<$unit:upper 1>] }> as core::ops::Add<crate::inner::Const<{ [<$unit:upper 2>] }>>>::Output ),*
167                > as crate::inner::ToConst>::Output;
168
169                fn mul(self, _rhs: $system<EXP2, $({ [<$unit:upper 2>] }),*>) -> Self::Output {
170                    crate::inner::ToConst::to_const([<Typenum $system>](core::marker::PhantomData))
171                }
172            }
173
174            impl<
175                const EXP1: i8,
176                const EXP2: i8,
177                $(const [<$unit:upper 1>]: i8, const [<$unit:upper 2>]: i8),*
178            > core::ops::Div<$system<EXP2, $([<$unit:upper 2>]),*>> for $system<EXP1, $([<$unit:upper 1>]),*>
179            where
180                crate::inner::Const<EXP1>: core::ops::Sub<crate::inner::Const<EXP2>>,
181
182                $( crate::inner::Const<[<$unit:upper 1>]>: core::ops::Sub<crate::inner::Const<[<$unit:upper 2>]>>, )*
183                [<Typenum $system>]<
184                    <crate::inner::Const<EXP1> as core::ops::Sub<crate::inner::Const<EXP2>>>::Output,
185                    $( <crate::inner::Const<[<$unit:upper 1>]> as core::ops::Sub<crate::inner::Const<[<$unit:upper 2>]>>>::Output ),*
186                >: crate::inner::ToConst,
187            {
188                type Output = <[<Typenum $system>]<
189                    <crate::inner::Const<EXP1> as core::ops::Sub<crate::inner::Const<EXP2>>>::Output,
190                    $( <crate::inner::Const<[<$unit:upper 1>]> as core::ops::Sub<crate::inner::Const<[<$unit:upper 2>]>>>::Output ),*
191                > as crate::inner::ToConst>::Output;
192
193                fn div(self, _rhs: $system<EXP2, $([<$unit:upper 2>]),*>) -> Self::Output {
194                    crate::inner::ToConst::to_const([<Typenum $system>](core::marker::PhantomData))
195                }
196            }
197
198            impl<
199                T,
200                const EXP1: i8,
201                const EXP2: i8,
202                $(const [<$unit:upper>]: i8),*
203            > crate::UnitConvert<T, $system<EXP1, $([<$unit:upper>]),*>> for $system<EXP2, $([<$unit:upper>]),*>
204            where
205                T: crate::MulPowerOfTen,
206            {
207                fn unit_convert(val: T) -> T {
208                    val.mul_power_of_ten(EXP2 - EXP1)
209                }
210            }
211
212            /// Base units for this unit system.
213            pub mod base {
214                #![allow(non_camel_case_types)]
215                use super::$system;
216
217                // Generate dimensionless (all zeros)
218                $crate::gen_dimensionless!(@acc $system; $($unit),*;);
219
220                // Generate each base unit type alias
221                $crate::gen_base_units!(@iter $system; ; $($unit),*);
222            }
223        }
224    }
225}
226pub(crate) use power_of_ten_unit_system;
227
228/// Generate `dimensionless` type alias with all zeros.
229#[doc(hidden)]
230macro_rules! gen_dimensionless {
231    // Done accumulating - emit the type
232    (@acc $system:ident; ; $($zeros:tt)*) => {
233        pub type dimensionless = $system<0 $($zeros)*>;
234    };
235    // Accumulate one more zero for each unit
236    (@acc $system:ident; $head:ident $(, $tail:ident)*; $($zeros:tt)*) => {
237        $crate::gen_dimensionless!(@acc $system; $($tail),*; $($zeros)*, 0);
238    };
239}
240pub(crate) use gen_dimensionless;
241
242/// Generate base unit type aliases by iterating through units.
243#[doc(hidden)]
244macro_rules! gen_base_units {
245    // Done iterating
246    (@iter $system:ident; $($before:ident),*;) => {};
247    // Process one unit, then recurse
248    (@iter $system:ident; ; $current:ident $(, $after:ident)*) => {
249        // First unit: no zeros before, just 1, then zeros after
250        $crate::gen_one_base!(@zeros_after $system; $current; [, 1]; $($after),*);
251        $crate::gen_base_units!(@iter $system; $current; $($after),*);
252    };
253    (@iter $system:ident; $($before:ident),+; $current:ident $(, $after:ident)*) => {
254        // Has units before: accumulate zeros, then 1, then zeros after
255        $crate::gen_one_base!(@zeros_before $system; $current; []; $($before),+; $($after),*);
256        $crate::gen_base_units!(@iter $system; $($before,)+ $current; $($after),*);
257    };
258}
259pub(crate) use gen_base_units;
260
261/// Generate a single base unit type alias.
262#[doc(hidden)]
263macro_rules! gen_one_base {
264    // Accumulate zeros for "before" units
265    (@zeros_before $system:ident; $current:ident; [$($acc:tt)*]; $head:ident; $($after:ident),*) => {
266        // Last "before" unit - add zero and move to "after" phase
267        $crate::gen_one_base!(@zeros_after $system; $current; [$($acc)*, 0, 1]; $($after),*);
268    };
269    (@zeros_before $system:ident; $current:ident; [$($acc:tt)*]; $head:ident, $($tail:ident),+; $($after:ident),*) => {
270        // More "before" units - add zero and continue
271        $crate::gen_one_base!(@zeros_before $system; $current; [$($acc)*, 0]; $($tail),+; $($after),*);
272    };
273    // Accumulate zeros for "after" units
274    (@zeros_after $system:ident; $current:ident; [$($acc:tt)*];) => {
275        // No more "after" units - emit the type
276        pub type $current = $system<0 $($acc)*>;
277    };
278    (@zeros_after $system:ident; $current:ident; [$($acc:tt)*]; $head:ident $(, $tail:ident)*) => {
279        // More "after" units - add zero and continue
280        $crate::gen_one_base!(@zeros_after $system; $current; [$($acc)*, 0]; $($tail),*);
281    };
282}
283pub(crate) use gen_one_base;
284
285/// Multiply a unit by another unit or [`TenTo`].
286pub type Mul<A, B> = <A as ops::Mul<B>>::Output;
287/// Divide a unit by another unit or [`TenTo`].
288pub type Div<A, B> = <A as ops::Div<B>>::Output;
289
290/// Convert a value between different units.
291pub trait UnitConvert<T, From>: Unit {
292    fn unit_convert(val: T) -> T;
293}
294
295#[cfg(test)]
296mod tests {
297    extern crate std;
298
299    use super::*;
300    use std::cmp;
301
302    // =========================================================================
303    // Quantity struct behavior
304    // =========================================================================
305
306    #[test]
307    fn quantity_new_and_deref() {
308        let q: Quantity<i32, si::m> = Quantity::new(42);
309        assert_eq!(*q, 42);
310    }
311
312    #[test]
313    fn quantity_from_trait() {
314        let q: Quantity<i32, si::m> = 42.into();
315        assert_eq!(*q, 42);
316    }
317
318    #[test]
319    fn quantity_equality_and_ordering() {
320        let a: Quantity<i32, si::m> = Quantity::new(5);
321        let b: Quantity<i32, si::m> = Quantity::new(5);
322        let c: Quantity<i32, si::m> = Quantity::new(10);
323        assert_eq!(a, b);
324        assert!(a < c);
325        assert_eq!(a.cmp(&c), cmp::Ordering::Less);
326    }
327
328    #[test]
329    fn quantity_hash() {
330        use std::collections::hash_map::DefaultHasher;
331        use std::hash::{Hash, Hasher};
332
333        let a: Quantity<i32, si::m> = Quantity::new(42);
334        let b: Quantity<i32, si::m> = Quantity::new(42);
335
336        let mut hasher_a = DefaultHasher::new();
337        let mut hasher_b = DefaultHasher::new();
338        a.hash(&mut hasher_a);
339        b.hash(&mut hasher_b);
340        assert_eq!(hasher_a.finish(), hasher_b.finish());
341    }
342
343    // =========================================================================
344    // Arithmetic operations
345    // =========================================================================
346
347    #[test]
348    fn add_sub_same_units() {
349        let a: Quantity<i32, si::m> = Quantity::new(10);
350        let b: Quantity<i32, si::m> = Quantity::new(3);
351        assert_eq!(*(a + b), 13);
352        assert_eq!(*(a - b), 7);
353    }
354
355    #[test]
356    fn mul_div_combines_units() {
357        // m * m = m^2, m^2 / m = m
358        let a: Quantity<i32, si::m> = Quantity::new(6);
359        let b: Quantity<i32, si::m> = Quantity::new(4);
360        let area: Quantity<i32, si::square_meter> = a * b;
361        assert_eq!(*area, 24);
362
363        let c: Quantity<i32, si::m> = Quantity::new(3);
364        let result: Quantity<i32, si::m> = area / c;
365        assert_eq!(*result, 8);
366    }
367
368    #[test]
369    fn division_to_dimensionless() {
370        let a: Quantity<i32, si::m> = Quantity::new(10);
371        let b: Quantity<i32, si::m> = Quantity::new(5);
372        let ratio: Quantity<i32, si::dimensionless> = a / b;
373        assert_eq!(*ratio, 2);
374    }
375
376    // =========================================================================
377    // MulPowerOfTen trait (core conversion math)
378    // =========================================================================
379
380    #[test]
381    fn mul_power_of_ten_integers() {
382        // Negative exp multiplies, positive exp divides
383        assert_eq!(5i32.mul_power_of_ten(-3), 5000);
384        assert_eq!(5000i32.mul_power_of_ten(3), 5);
385        assert_eq!(42i32.mul_power_of_ten(0), 42);
386    }
387
388    #[test]
389    fn mul_power_of_ten_floats() {
390        let up = 2.5f64.mul_power_of_ten(-3);
391        let down = 2500.0f64.mul_power_of_ten(3);
392        assert!((2500.0 - up).abs() < f64::EPSILON);
393        assert!((2.5 - down).abs() < f64::EPSILON);
394    }
395
396    // =========================================================================
397    // Unit conversions
398    // =========================================================================
399
400    #[test]
401    fn conversion_scales_correctly() {
402        // m -> mm (scale up by 1000)
403        let meters: Quantity<i32, si::m> = Quantity::new(3);
404        let mm: Quantity<i32, si::milli<si::m>> = meters.convert();
405        assert_eq!(*mm, 3000);
406
407        // mm -> m (scale down by 1000)
408        let back: Quantity<i32, si::m> = mm.convert();
409        assert_eq!(*back, 3);
410    }
411
412    #[test]
413    fn conversion_f32() {
414        let km: Quantity<f32, si::kilo<si::m>> = Quantity::new(2.5);
415        let m: Quantity<f32, si::m> = km.convert();
416        assert!((2500.0 - *m).abs() < f32::EPSILON);
417    }
418
419    #[test]
420    fn conversion_f64() {
421        let km: Quantity<f64, si::kilo<si::m>> = Quantity::new(2.5);
422        let m: Quantity<f64, si::m> = km.convert();
423        assert!((2500.0 - *m).abs() < f64::EPSILON);
424    }
425
426    #[test]
427    fn conversion_identity() {
428        let m: Quantity<i32, si::m> = Quantity::new(42);
429        let m2: Quantity<i32, si::m> = m.convert();
430        assert_eq!(*m, *m2);
431    }
432
433    // =========================================================================
434    // Type algebra (unit combination correctness)
435    // =========================================================================
436
437    #[test]
438    fn velocity_times_time_gives_distance() {
439        let velocity: Quantity<f64, si::meter_per_second> = Quantity::new(10.0);
440        let time: Quantity<f64, si::s> = Quantity::new(5.0);
441        let distance: Quantity<f64, si::m> = velocity * time;
442        assert!((50.0 - *distance).abs() < f64::EPSILON);
443    }
444
445    #[test]
446    fn derived_unit_algebra() {
447        // kg * (m / s^2) = N
448        let mass: Quantity<f64, si::kg> = Quantity::new(10.0);
449        let accel: Quantity<f64, si::meter_per_second_squared> = Quantity::new(5.0);
450        let force: Quantity<f64, si::N> = mass * accel;
451        assert!((50.0 - *force).abs() < f64::EPSILON);
452
453        // N * m = J
454        let distance: Quantity<f64, si::m> = Quantity::new(2.0);
455        let energy: Quantity<f64, si::J> = force * distance;
456        assert!((100.0 - *energy).abs() < f64::EPSILON);
457    }
458}