qtty_core/
lib.rs

1//! Core type system for strongly typed physical quantities.
2//!
3//! `qtty-core` provides a minimal, zero-cost units model:
4//!
5//! - A *unit* is a zero-sized marker type implementing [`Unit`].
6//! - A value tagged with a unit is a [`Quantity<U>`], backed by an `f64`.
7//! - Conversion is an explicit, type-checked scaling via [`Quantity::to`].
8//! - Derived units like velocity are expressed as [`Per<N, D>`] (e.g. `Meter/Second`).
9//!
10//! Most users should depend on `qtty` (the facade crate) unless they need direct access to these primitives.
11//!
12//! # What this crate solves
13//!
14//! - Compile-time separation of dimensions (length vs time vs angle, …).
15//! - Zero runtime overhead for unit tags (phantom types only).
16//! - A small vocabulary to express derived units via type aliases (`Per`, `DivDim`).
17//!
18//! # What this crate does not try to solve
19//!
20//! - Exact arithmetic (`Quantity` is `f64`).
21//! - General-purpose symbolic simplification of arbitrary unit expressions.
22//! - Automatic tracking of exponent dimensions (`m^2`, `s^-1`, …); only the expression forms represented by the
23//!   provided types are modeled.
24//!
25//! # Quick start
26//!
27//! Convert between predefined units:
28//!
29//! ```rust
30//! use qtty_core::length::{Kilometers, Meter};
31//!
32//! let km = Kilometers::new(1.25);
33//! let m = km.to::<Meter>();
34//! assert!((m.value() - 1250.0).abs() < 1e-12);
35//! ```
36//!
37//! Compose derived units using `/`:
38//!
39//! ```rust
40//! use qtty_core::length::{Meter, Meters};
41//! use qtty_core::time::{Second, Seconds};
42//! use qtty_core::velocity::Velocity;
43//!
44//! let d = Meters::new(100.0);
45//! let t = Seconds::new(20.0);
46//! let v: Velocity<Meter, Second> = d / t;
47//! assert!((v.value() - 5.0).abs() < 1e-12);
48//! ```
49//!
50//! # `no_std`
51//!
52//! Disable default features to build `qtty-core` without `std`:
53//!
54//! ```toml
55//! [dependencies]
56//! qtty-core = { version = "0.1.0", default-features = false }
57//! ```
58//!
59//! When `std` is disabled, floating-point math that isn't available in `core` is provided via `libm`.
60//!
61//! # Feature flags
62//!
63//! - `std` (default): enables `std` support.
64//! - `serde`: enables `serde` support for `Quantity<U>`; serialization is the raw `f64` value only.
65//!
66//! # Panics and errors
67//!
68//! This crate does not define an error type and does not return `Result` from its core operations. Conversions and
69//! arithmetic are pure `f64` computations; they do not panic on their own, but they follow IEEE-754 behavior (NaN and
70//! infinities propagate according to the underlying operation).
71//!
72//! # SemVer and stability
73//!
74//! This crate is currently `0.x`. Expect breaking changes between minor versions until `1.0`.
75
76#![deny(missing_docs)]
77#![cfg_attr(not(feature = "std"), no_std)]
78#![forbid(unsafe_code)]
79
80#[cfg(not(feature = "std"))]
81extern crate libm;
82
83// ─────────────────────────────────────────────────────────────────────────────
84// Core modules
85// ─────────────────────────────────────────────────────────────────────────────
86
87mod dimension;
88mod macros;
89mod quantity;
90mod unit;
91
92// ─────────────────────────────────────────────────────────────────────────────
93// Public re-exports of core types
94// ─────────────────────────────────────────────────────────────────────────────
95
96pub use dimension::{Dimension, Dimensionless, DivDim};
97pub use quantity::Quantity;
98pub use unit::{Per, Simplify, Unit, Unitless};
99
100#[cfg(feature = "serde")]
101pub use quantity::serde_with_unit;
102
103// ─────────────────────────────────────────────────────────────────────────────
104// Predefined unit modules (grouped by dimension)
105// ─────────────────────────────────────────────────────────────────────────────
106
107/// Predefined unit modules (grouped by dimension).
108///
109/// These are defined in `qtty-core` so they can implement formatting and helper traits without running into Rust's
110/// orphan rules.
111pub mod units;
112
113pub use units::angular;
114pub use units::frequency;
115pub use units::length;
116pub use units::mass;
117pub use units::power;
118pub use units::time;
119pub use units::unitless;
120pub use units::velocity;
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    // ─────────────────────────────────────────────────────────────────────────────
127    // Test dimension and unit for lib.rs tests
128    // ─────────────────────────────────────────────────────────────────────────────
129    #[derive(Debug)]
130    pub enum TestDim {}
131    impl Dimension for TestDim {}
132
133    #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
134    pub enum TestUnit {}
135    impl Unit for TestUnit {
136        const RATIO: f64 = 1.0;
137        type Dim = TestDim;
138        const SYMBOL: &'static str = "tu";
139    }
140    impl core::fmt::Display for Quantity<TestUnit> {
141        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
142            write!(f, "{} tu", self.value())
143        }
144    }
145
146    #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
147    pub enum DoubleTestUnit {}
148    impl Unit for DoubleTestUnit {
149        const RATIO: f64 = 2.0;
150        type Dim = TestDim;
151        const SYMBOL: &'static str = "dtu";
152    }
153    impl core::fmt::Display for Quantity<DoubleTestUnit> {
154        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
155            write!(f, "{} dtu", self.value())
156        }
157    }
158
159    #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
160    pub enum HalfTestUnit {}
161    impl Unit for HalfTestUnit {
162        const RATIO: f64 = 0.5;
163        type Dim = TestDim;
164        const SYMBOL: &'static str = "htu";
165    }
166    impl core::fmt::Display for Quantity<HalfTestUnit> {
167        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
168            write!(f, "{} htu", self.value())
169        }
170    }
171
172    type TU = Quantity<TestUnit>;
173    type Dtu = Quantity<DoubleTestUnit>;
174
175    // ─────────────────────────────────────────────────────────────────────────────
176    // Quantity core behavior
177    // ─────────────────────────────────────────────────────────────────────────────
178
179    #[test]
180    fn quantity_new_and_value() {
181        let q = TU::new(42.0);
182        assert_eq!(q.value(), 42.0);
183    }
184
185    #[test]
186    fn quantity_nan_constant() {
187        assert!(TU::NAN.value().is_nan());
188    }
189
190    #[test]
191    fn quantity_abs() {
192        assert_eq!(TU::new(-5.0).abs().value(), 5.0);
193        assert_eq!(TU::new(5.0).abs().value(), 5.0);
194        assert_eq!(TU::new(0.0).abs().value(), 0.0);
195    }
196
197    #[test]
198    fn quantity_from_f64() {
199        let q: TU = 123.456.into();
200        assert_eq!(q.value(), 123.456);
201    }
202
203    // ─────────────────────────────────────────────────────────────────────────────
204    // Conversion via `to`
205    // ─────────────────────────────────────────────────────────────────────────────
206
207    #[test]
208    fn quantity_conversion_to_same_unit() {
209        let q = TU::new(10.0);
210        let converted = q.to::<TestUnit>();
211        assert_eq!(converted.value(), 10.0);
212    }
213
214    #[test]
215    fn quantity_conversion_to_different_unit() {
216        // 1 DoubleTestUnit = 2 TestUnit (in canonical terms)
217        // So 10 TU -> 10 * (1.0 / 2.0) = 5 DTU
218        let q = TU::new(10.0);
219        let converted = q.to::<DoubleTestUnit>();
220        assert!((converted.value() - 5.0).abs() < 1e-12);
221    }
222
223    #[test]
224    fn quantity_conversion_roundtrip() {
225        let original = TU::new(100.0);
226        let converted = original.to::<DoubleTestUnit>();
227        let back = converted.to::<TestUnit>();
228        assert!((back.value() - original.value()).abs() < 1e-12);
229    }
230
231    // ─────────────────────────────────────────────────────────────────────────────
232    // Const helper methods: add/sub/mul/div/min
233    // ─────────────────────────────────────────────────────────────────────────────
234
235    #[test]
236    fn const_add() {
237        let a = TU::new(3.0);
238        let b = TU::new(7.0);
239        assert_eq!(a.add(b).value(), 10.0);
240    }
241
242    #[test]
243    fn const_sub() {
244        let a = TU::new(10.0);
245        let b = TU::new(3.0);
246        assert_eq!(a.sub(b).value(), 7.0);
247    }
248
249    #[test]
250    fn const_mul() {
251        let a = TU::new(4.0);
252        let b = TU::new(5.0);
253        assert_eq!(Quantity::mul(&a, b).value(), 20.0);
254    }
255
256    #[test]
257    fn const_div() {
258        let a = TU::new(20.0);
259        let b = TU::new(4.0);
260        assert_eq!(Quantity::div(&a, b).value(), 5.0);
261    }
262
263    #[test]
264    fn const_min() {
265        let a = TU::new(5.0);
266        let b = TU::new(3.0);
267        assert_eq!(a.min(b).value(), 3.0);
268        assert_eq!(b.min(a).value(), 3.0);
269    }
270
271    // ─────────────────────────────────────────────────────────────────────────────
272    // Operator traits: Add, Sub, Mul, Div, Neg, Rem
273    // ─────────────────────────────────────────────────────────────────────────────
274
275    #[test]
276    fn operator_add() {
277        let a = TU::new(3.0);
278        let b = TU::new(7.0);
279        assert_eq!((a + b).value(), 10.0);
280    }
281
282    #[test]
283    fn operator_sub() {
284        let a = TU::new(10.0);
285        let b = TU::new(3.0);
286        assert_eq!((a - b).value(), 7.0);
287    }
288
289    #[test]
290    fn operator_mul_by_f64() {
291        let q = TU::new(5.0);
292        assert_eq!((q * 3.0).value(), 15.0);
293        assert_eq!((3.0 * q).value(), 15.0);
294    }
295
296    #[test]
297    fn operator_div_by_f64() {
298        let q = TU::new(15.0);
299        assert_eq!((q / 3.0).value(), 5.0);
300    }
301
302    #[test]
303    fn operator_neg() {
304        let q = TU::new(5.0);
305        assert_eq!((-q).value(), -5.0);
306        assert_eq!((-(-q)).value(), 5.0);
307    }
308
309    #[test]
310    fn operator_rem() {
311        let q = TU::new(10.0);
312        assert_eq!((q % 3.0).value(), 1.0);
313    }
314
315    // ─────────────────────────────────────────────────────────────────────────────
316    // Assignment operators: AddAssign, SubAssign, DivAssign
317    // ─────────────────────────────────────────────────────────────────────────────
318
319    #[test]
320    fn operator_add_assign() {
321        let mut q = TU::new(5.0);
322        q += TU::new(3.0);
323        assert_eq!(q.value(), 8.0);
324    }
325
326    #[test]
327    fn operator_sub_assign() {
328        let mut q = TU::new(10.0);
329        q -= TU::new(3.0);
330        assert_eq!(q.value(), 7.0);
331    }
332
333    #[test]
334    fn operator_div_assign() {
335        let mut q = TU::new(20.0);
336        q /= TU::new(4.0);
337        assert_eq!(q.value(), 5.0);
338    }
339
340    // ─────────────────────────────────────────────────────────────────────────────
341    // PartialEq<f64>
342    // ─────────────────────────────────────────────────────────────────────────────
343
344    #[test]
345    fn partial_eq_f64() {
346        let q = TU::new(5.0);
347        assert!(q == 5.0);
348        assert!(!(q == 4.0));
349    }
350
351    // ─────────────────────────────────────────────────────────────────────────────
352    // Division yielding Per<N, D>
353    // ─────────────────────────────────────────────────────────────────────────────
354
355    #[test]
356    fn division_creates_per_type() {
357        let num = TU::new(100.0);
358        let den = Dtu::new(20.0);
359        let ratio: Quantity<Per<TestUnit, DoubleTestUnit>> = num / den;
360        assert!((ratio.value() - 5.0).abs() < 1e-12);
361    }
362
363    #[test]
364    fn per_ratio_conversion() {
365        let v1: Quantity<Per<DoubleTestUnit, TestUnit>> = Quantity::new(10.0);
366        let v2: Quantity<Per<TestUnit, TestUnit>> = v1.to();
367        assert!((v2.value() - 20.0).abs() < 1e-12);
368    }
369
370    #[test]
371    fn per_multiplication_recovers_numerator() {
372        let rate: Quantity<Per<TestUnit, DoubleTestUnit>> = Quantity::new(5.0);
373        let time = Dtu::new(4.0);
374        let result: TU = rate * time;
375        assert!((result.value() - 20.0).abs() < 1e-12);
376    }
377
378    #[test]
379    fn per_multiplication_commutative() {
380        let rate: Quantity<Per<TestUnit, DoubleTestUnit>> = Quantity::new(5.0);
381        let time = Dtu::new(4.0);
382        let result1: TU = rate * time;
383        let result2: TU = time * rate;
384        assert!((result1.value() - result2.value()).abs() < 1e-12);
385    }
386
387    // ─────────────────────────────────────────────────────────────────────────────
388    // Simplify trait
389    // ─────────────────────────────────────────────────────────────────────────────
390
391    #[test]
392    fn simplify_per_u_u_to_unitless() {
393        let ratio: Quantity<Per<TestUnit, TestUnit>> = Quantity::new(1.23456);
394        let unitless: Quantity<Unitless> = ratio.simplify();
395        assert!((unitless.value() - 1.23456).abs() < 1e-12);
396    }
397
398    #[test]
399    fn simplify_per_n_per_n_d_to_d() {
400        let q: Quantity<Per<TestUnit, Per<TestUnit, DoubleTestUnit>>> = Quantity::new(7.5);
401        let simplified: Dtu = q.simplify();
402        assert!((simplified.value() - 7.5).abs() < 1e-12);
403    }
404
405    // ─────────────────────────────────────────────────────────────────────────────
406    // Quantity<Per<U,U>>::asin()
407    // ─────────────────────────────────────────────────────────────────────────────
408
409    #[test]
410    fn per_u_u_asin() {
411        let ratio: Quantity<Per<TestUnit, TestUnit>> = Quantity::new(0.5);
412        let result = ratio.asin();
413        assert!((result - 0.5_f64.asin()).abs() < 1e-12);
414    }
415
416    #[test]
417    fn per_u_u_asin_boundary_values() {
418        let one: Quantity<Per<TestUnit, TestUnit>> = Quantity::new(1.0);
419        assert!((one.asin() - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
420
421        let neg_one: Quantity<Per<TestUnit, TestUnit>> = Quantity::new(-1.0);
422        assert!((neg_one.asin() - (-core::f64::consts::FRAC_PI_2)).abs() < 1e-12);
423
424        let zero: Quantity<Per<TestUnit, TestUnit>> = Quantity::new(0.0);
425        assert!((zero.asin() - 0.0).abs() < 1e-12);
426    }
427
428    // ─────────────────────────────────────────────────────────────────────────────
429    // Display formatting
430    // ─────────────────────────────────────────────────────────────────────────────
431
432    #[test]
433    fn display_simple_quantity() {
434        let q = TU::new(42.5);
435        let s = format!("{}", q);
436        assert_eq!(s, "42.5 tu");
437    }
438
439    #[test]
440    fn display_per_quantity() {
441        let q: Quantity<Per<TestUnit, DoubleTestUnit>> = Quantity::new(2.5);
442        let s = format!("{}", q);
443        assert_eq!(s, "2.5 tu/dtu");
444    }
445
446    #[test]
447    fn display_negative_value() {
448        let q = TU::new(-99.9);
449        let s = format!("{}", q);
450        assert_eq!(s, "-99.9 tu");
451    }
452
453    // ─────────────────────────────────────────────────────────────────────────────
454    // Edge cases
455    // ─────────────────────────────────────────────────────────────────────────────
456
457    #[test]
458    fn edge_case_zero() {
459        let zero = TU::new(0.0);
460        assert_eq!(zero.value(), 0.0);
461        assert_eq!((-zero).value(), 0.0);
462        assert_eq!(zero.abs().value(), 0.0);
463    }
464
465    #[test]
466    fn edge_case_negative_values() {
467        let neg = TU::new(-10.0);
468        let pos = TU::new(5.0);
469
470        assert_eq!((neg + pos).value(), -5.0);
471        assert_eq!((neg - pos).value(), -15.0);
472        assert_eq!((neg * 2.0).value(), -20.0);
473        assert_eq!(neg.abs().value(), 10.0);
474    }
475
476    #[test]
477    fn edge_case_large_values() {
478        let large = TU::new(1e100);
479        let small = TU::new(1e-100);
480        assert_eq!(large.value(), 1e100);
481        assert_eq!(small.value(), 1e-100);
482    }
483
484    #[test]
485    fn edge_case_infinity() {
486        let inf = TU::new(f64::INFINITY);
487        let neg_inf = TU::new(f64::NEG_INFINITY);
488
489        assert!(inf.value().is_infinite());
490        assert!(neg_inf.value().is_infinite());
491        assert_eq!(inf.value().signum(), 1.0);
492        assert_eq!(neg_inf.value().signum(), -1.0);
493    }
494
495    // ─────────────────────────────────────────────────────────────────────────────
496    // Serde tests
497    // ─────────────────────────────────────────────────────────────────────────────
498
499    #[cfg(feature = "serde")]
500    mod serde_tests {
501        use super::*;
502        use serde::{Deserialize, Serialize};
503
504        #[test]
505        fn serialize_quantity() {
506            let q = TU::new(42.5);
507            let json = serde_json::to_string(&q).unwrap();
508            assert_eq!(json, "42.5");
509        }
510
511        #[test]
512        fn deserialize_quantity() {
513            let json = "42.5";
514            let q: TU = serde_json::from_str(json).unwrap();
515            assert_eq!(q.value(), 42.5);
516        }
517
518        #[test]
519        fn serde_roundtrip() {
520            let original = TU::new(123.456);
521            let json = serde_json::to_string(&original).unwrap();
522            let restored: TU = serde_json::from_str(&json).unwrap();
523            assert!((restored.value() - original.value()).abs() < 1e-12);
524        }
525
526        // ─────────────────────────────────────────────────────────────────────────
527        // serde_with_unit module tests
528        // ─────────────────────────────────────────────────────────────────────────
529
530        #[derive(Serialize, Deserialize, Debug)]
531        struct TestStruct {
532            #[serde(with = "crate::serde_with_unit")]
533            distance: TU,
534        }
535
536        #[test]
537        fn serde_with_unit_serialize() {
538            let data = TestStruct {
539                distance: TU::new(42.5),
540            };
541            let json = serde_json::to_string(&data).unwrap();
542            assert!(json.contains("\"value\""));
543            assert!(json.contains("\"unit\""));
544            assert!(json.contains("42.5"));
545            assert!(json.contains("\"tu\""));
546        }
547
548        #[test]
549        fn serde_with_unit_deserialize() {
550            let json = r#"{"distance":{"value":42.5,"unit":"tu"}}"#;
551            let data: TestStruct = serde_json::from_str(json).unwrap();
552            assert_eq!(data.distance.value(), 42.5);
553        }
554
555        #[test]
556        fn serde_with_unit_deserialize_no_unit_field() {
557            // Should work without unit field for backwards compatibility
558            let json = r#"{"distance":{"value":42.5}}"#;
559            let data: TestStruct = serde_json::from_str(json).unwrap();
560            assert_eq!(data.distance.value(), 42.5);
561        }
562
563        #[test]
564        fn serde_with_unit_deserialize_wrong_unit() {
565            let json = r#"{"distance":{"value":42.5,"unit":"wrong"}}"#;
566            let result: Result<TestStruct, _> = serde_json::from_str(json);
567            assert!(result.is_err());
568            let err_msg = result.unwrap_err().to_string();
569            assert!(err_msg.contains("unit mismatch") || err_msg.contains("expected"));
570        }
571
572        #[test]
573        fn serde_with_unit_deserialize_missing_value() {
574            let json = r#"{"distance":{"unit":"tu"}}"#;
575            let result: Result<TestStruct, _> = serde_json::from_str(json);
576            assert!(result.is_err());
577            let err_msg = result.unwrap_err().to_string();
578            assert!(err_msg.contains("missing field") || err_msg.contains("value"));
579        }
580
581        #[test]
582        fn serde_with_unit_deserialize_duplicate_value() {
583            let json = r#"{"distance":{"value":42.5,"value":100.0,"unit":"tu"}}"#;
584            let result: Result<TestStruct, _> = serde_json::from_str(json);
585            // This should either error or use one of the values (implementation-dependent)
586            // but we're testing that it doesn't panic
587            let _ = result;
588        }
589
590        #[test]
591        fn serde_with_unit_deserialize_duplicate_unit() {
592            let json = r#"{"distance":{"value":42.5,"unit":"tu","unit":"tu"}}"#;
593            let result: Result<TestStruct, _> = serde_json::from_str(json);
594            // Similar to above - just ensure no panic
595            let _ = result;
596        }
597
598        #[test]
599        fn serde_with_unit_deserialize_invalid_format() {
600            // Test the expecting() method by providing wrong format
601            let json = r#"{"distance":"not_an_object"}"#;
602            let result: Result<TestStruct, _> = serde_json::from_str(json);
603            assert!(result.is_err());
604        }
605
606        #[test]
607        fn serde_with_unit_deserialize_array() {
608            // Test the expecting() method with array format
609            let json = r#"{"distance":[42.5, "tu"]}"#;
610            let result: Result<TestStruct, _> = serde_json::from_str(json);
611            assert!(result.is_err());
612        }
613
614        #[test]
615        fn serde_with_unit_roundtrip() {
616            let original = TestStruct {
617                distance: TU::new(123.456),
618            };
619            let json = serde_json::to_string(&original).unwrap();
620            let restored: TestStruct = serde_json::from_str(&json).unwrap();
621            assert!((restored.distance.value() - original.distance.value()).abs() < 1e-12);
622        }
623
624        #[test]
625        fn serde_with_unit_special_values() {
626            // Note: JSON doesn't support Infinity and NaN natively.
627            // serde_json serializes them as null, which can't be deserialized
628            // back to f64. So we'll test with very large numbers instead.
629            let test_large = TestStruct {
630                distance: TU::new(1e100),
631            };
632            let json = serde_json::to_string(&test_large).unwrap();
633            let restored: TestStruct = serde_json::from_str(&json).unwrap();
634            assert!((restored.distance.value() - 1e100).abs() < 1e88);
635
636            let test_small = TestStruct {
637                distance: TU::new(-1e-100),
638            };
639            let json = serde_json::to_string(&test_small).unwrap();
640            let restored: TestStruct = serde_json::from_str(&json).unwrap();
641            assert!((restored.distance.value() + 1e-100).abs() < 1e-112);
642        }
643    }
644}