Skip to main content

qtty_core/units/mass/
mod.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Mass units.
5//!
6//! The canonical scaling unit for this dimension is [`Gram`] (`Gram::RATIO == 1.0`).
7//!
8//! This module aims for practical completeness while avoiding avoidable precision loss:
9//! - **SI grams**: full prefix ladder (yocto … yotta).
10//! - **Defined non-SI**: tonne, avoirdupois units, carat, grain.
11//! - **Science/astro**: atomic mass unit (u/Da), nominal solar mass.
12//!
13//! ```rust
14//! use qtty_core::mass::{Kilograms, Gram};
15//!
16//! let m = Kilograms::new(1.0);
17//! let g = m.to::<Gram>();
18//! assert_eq!(g.value(), 1000.0);
19//! ```
20//!
21//! ## All mass units (default)
22//!
23//! ```rust
24//! use qtty_core::mass::*;
25//!
26//! macro_rules! touch {
27//!     ($T:ty, $v:expr) => {{ let q = <$T>::new($v); let _c = q; assert!(q == q); }};
28//! }
29//!
30//! touch!(Grams, 1.0);     touch!(Tonnes, 1.0);
31//! ```
32
33use crate::{Quantity, Unit};
34use qtty_derive::Unit;
35
36/// Re-export from the dimension module.
37pub use crate::dimension::Mass;
38
39/// Marker trait for any [`Unit`] whose dimension is [`Mass`].
40pub trait MassUnit: Unit<Dim = Mass> {}
41impl<T: Unit<Dim = Mass>> MassUnit for T {}
42
43#[cfg(feature = "customary")]
44mod customary;
45#[cfg(feature = "customary")]
46pub use customary::*;
47#[cfg(feature = "fundamental-physics")]
48mod fundamental_physics;
49#[cfg(feature = "fundamental-physics")]
50pub use fundamental_physics::*;
51#[cfg(feature = "astro")]
52mod astro;
53#[cfg(feature = "astro")]
54pub use astro::*;
55
56/// Gram.
57#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
58#[unit(symbol = "g", dimension = Mass, ratio = 1.0)]
59pub struct Gram;
60/// A quantity measured in grams.
61pub type Grams = Quantity<Gram>;
62/// One gram.
63pub const G: Grams = Grams::new(1.0);
64
65/// Helper macro to declare a gram-based SI mass unit.
66///
67/// Each invocation of this macro defines, for a given prefix on grams:
68/// - a unit struct `$name` (e.g. `Kilogram`),
69/// - a shorthand type alias `$alias` (e.g. `Kg`),
70/// - a quantity type `$qty` (e.g. `Kilograms`), and
71/// - a constant `$one` equal to `1.0` of that quantity.
72///
73/// The `$ratio` argument is the conversion factor to grams, i.e.
74/// `$name::RATIO` such that `1 $sym = $ratio g`.
75macro_rules! si_gram {
76    ($name:ident, $sym:literal, $ratio:expr, $alias:ident, $qty:ident, $one:ident) => {
77        #[doc = concat!("SI mass unit `", stringify!($name), "` with gram-based prefix (symbol `", $sym,"`).")]
78        #[doc = concat!("By definition, `1 ", $sym, " = ", stringify!($ratio), " g`.")]
79        #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
80        #[unit(symbol = $sym, dimension = Mass, ratio = $ratio)]
81        pub struct $name;
82
83        #[doc = concat!("Shorthand alias for [`", stringify!($name), "`]." )]
84        pub type $alias = $name;
85
86        #[doc = concat!("Quantity measured in ", stringify!($name), " (",$sym,").")]
87        pub type $qty = Quantity<$alias>;
88
89        #[doc = concat!("Constant equal to one ", stringify!($name), " (1 ",$sym,").")]
90        pub const $one: $qty = $qty::new(1.0);
91    };
92}
93
94// Full SI prefix ladder (gram-based)
95si_gram!(Yoctogram, "yg", 1e-24, Yg, Yoctograms, YG);
96si_gram!(Zeptogram, "zg", 1e-21, Zg, Zeptograms, ZG);
97si_gram!(Attogram, "ag", 1e-18, Ag, Attograms, AG);
98si_gram!(Femtogram, "fg", 1e-15, Fg, Femtograms, FG);
99si_gram!(Picogram, "pg", 1e-12, Pg, Picograms, PG);
100si_gram!(Nanogram, "ng", 1e-9, Ng, Nanograms, NG);
101si_gram!(Microgram, "µg", 1e-6, Ug, Micrograms, UG);
102si_gram!(Milligram, "mg", 1e-3, Mg, Milligrams, MG);
103si_gram!(Centigram, "cg", 1e-2, Cg, Centigrams, CG);
104si_gram!(Decigram, "dg", 1e-1, Dg, Decigrams, DG);
105
106si_gram!(Decagram, "dag", 1e1, Dag, Decagrams, DAG);
107si_gram!(Hectogram, "hg", 1e2, Hg, Hectograms, HG);
108si_gram!(Kilogram, "kg", 1e3, Kg, Kilograms, KG);
109si_gram!(Megagram, "Mg", 1e6, MgG, Megagrams, MEGAGRAM);
110si_gram!(Gigagram, "Gg", 1e9, Gg, Gigagrams, GG);
111si_gram!(Teragram, "Tg", 1e12, Tg, Teragrams, TG);
112si_gram!(Petagram, "Pg", 1e15, PgG, Petagrams, PETAGRAM);
113si_gram!(Exagram, "Eg", 1e18, Eg, Exagrams, EG);
114si_gram!(Zettagram, "Zg", 1e21, ZgG, Zettagrams, ZETTAGRAM);
115si_gram!(Yottagram, "Yg", 1e24, YgG, Yottagrams, YOTTAGRAM);
116
117/// Tonne (metric ton): `1 t = 1_000_000 g` (exact).
118#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
119#[unit(symbol = "t", dimension = Mass, ratio = 1_000_000.0)]
120pub struct Tonne;
121/// Shorthand type alias for [`Tonne`].
122pub type T = Tonne;
123/// Quantity measured in tonnes.
124pub type Tonnes = Quantity<T>;
125/// One metric tonne.
126pub const TONE: Tonnes = Tonnes::new(1.0);
127
128/// Canonical list of always-available (metric SI) mass units.
129///
130/// Exported (`#[doc(hidden)]`) for use in `qtty`\'s scalar alias generation and
131/// compile-time consistency checks.  Feature-gated units (customary, astro,
132/// fundamental-physics) are in their sub-modules.
133#[macro_export]
134#[doc(hidden)]
135macro_rules! mass_units {
136    ($cb:path) => {
137        $cb!(
138            Gram, Yoctogram, Zeptogram, Attogram, Femtogram, Picogram, Nanogram, Microgram,
139            Milligram, Centigram, Decigram, Decagram, Hectogram, Kilogram, Megagram, Gigagram,
140            Teragram, Petagram, Exagram, Zettagram, Yottagram, Tonne
141        );
142    };
143}
144
145// Generate bidirectional From impls between base metric SI mass units.
146mass_units!(crate::impl_unit_from_conversions);
147
148// ─────────────────────────────────────────────────────────────────────────────
149// Cross-unit ops: default (metric) units
150// ─────────────────────────────────────────────────────────────────────────────
151#[cfg(feature = "cross-unit-ops")]
152mass_units!(crate::impl_unit_cross_unit_ops);
153
154// ── Cross-feature: mass families ─────────────────────────────────────────────
155#[cfg(all(feature = "astro", feature = "customary"))]
156crate::__impl_from_each_extra_to_bases!(
157    {SolarMass}
158    Carat, Grain, Pound, Ounce, Stone, ShortTon, LongTon
159);
160#[cfg(all(feature = "astro", feature = "customary", feature = "cross-unit-ops"))]
161crate::__impl_cross_ops_each_extra_to_bases!(
162    {SolarMass}
163    Carat, Grain, Pound, Ounce, Stone, ShortTon, LongTon
164);
165
166#[cfg(all(feature = "astro", feature = "fundamental-physics"))]
167crate::__impl_from_each_extra_to_bases!(
168    {SolarMass}
169    AtomicMassUnit
170);
171#[cfg(all(
172    feature = "astro",
173    feature = "fundamental-physics",
174    feature = "cross-unit-ops"
175))]
176crate::__impl_cross_ops_each_extra_to_bases!(
177    {SolarMass}
178    AtomicMassUnit
179);
180
181#[cfg(all(feature = "customary", feature = "fundamental-physics"))]
182crate::__impl_from_each_extra_to_bases!(
183    {Carat, Grain, Pound, Ounce, Stone, ShortTon, LongTon}
184    AtomicMassUnit
185);
186#[cfg(all(
187    feature = "customary",
188    feature = "fundamental-physics",
189    feature = "cross-unit-ops"
190))]
191crate::__impl_cross_ops_each_extra_to_bases!(
192    {Carat, Grain, Pound, Ounce, Stone, ShortTon, LongTon}
193    AtomicMassUnit
194);
195
196// Compile-time check: every base mass unit is registered as BuiltinUnit.
197#[cfg(test)]
198mass_units!(crate::assert_units_are_builtin);
199
200#[cfg(all(test, feature = "std"))]
201mod tests {
202    use super::*;
203    use approx::{assert_abs_diff_eq, assert_relative_eq};
204    use proptest::prelude::*;
205
206    // ─────────────────────────────────────────────────────────────────────────────
207    // Basic conversions
208    // ─────────────────────────────────────────────────────────────────────────────
209
210    #[test]
211    fn gram_to_kilogram() {
212        let g = Grams::new(1000.0);
213        let kg = g.to::<Kilogram>();
214        assert_abs_diff_eq!(kg.value(), 1.0, epsilon = 1e-12);
215    }
216
217    #[test]
218    fn kilogram_to_gram() {
219        let kg = Kilograms::new(1.0);
220        let g = kg.to::<Gram>();
221        assert_abs_diff_eq!(g.value(), 1000.0, epsilon = 1e-9);
222    }
223
224    #[test]
225    #[cfg(feature = "astro")]
226    fn solar_mass_to_grams() {
227        let sm = SolarMasses::new(1.0);
228        let g = sm.to::<Gram>();
229        // 1 M☉ ≈ 1.988416e33 grams
230        assert_relative_eq!(g.value(), 1.988416e33, max_relative = 1e-5);
231    }
232
233    #[test]
234    #[cfg(feature = "astro")]
235    fn solar_mass_to_kilograms() {
236        let sm = SolarMasses::new(1.0);
237        let kg = sm.to::<Kilogram>();
238        // 1 M☉ ≈ 1.988416e30 kg
239        assert_relative_eq!(kg.value(), 1.988416e30, max_relative = 1e-5);
240    }
241
242    #[test]
243    #[cfg(feature = "astro")]
244    fn kilograms_to_solar_mass() {
245        // Earth mass ≈ 5.97e24 kg ≈ 3e-6 M☉
246        let earth_kg = Kilograms::new(5.97e24);
247        let earth_sm = earth_kg.to::<SolarMass>();
248        assert_relative_eq!(earth_sm.value(), 3.0e-6, max_relative = 0.01);
249    }
250
251    // ─────────────────────────────────────────────────────────────────────────────
252    // Solar mass sanity checks
253    // ─────────────────────────────────────────────────────────────────────────────
254
255    #[test]
256    #[cfg(feature = "astro")]
257    fn solar_mass_ratio_sanity() {
258        // 1 M☉ = 1.988416e33 g, so RATIO should be that value
259        assert_relative_eq!(SolarMass::RATIO, 1.988416e33, max_relative = 1e-5);
260    }
261
262    #[test]
263    #[cfg(feature = "astro")]
264    fn solar_mass_order_of_magnitude() {
265        // The Sun's mass is about 2e30 kg
266        let sun = SolarMasses::new(1.0);
267        let kg = sun.to::<Kilogram>();
268        assert!(kg.value() > 1e30);
269        assert!(kg.value() < 1e31);
270    }
271
272    // ─────────────────────────────────────────────────────────────────────────────
273    // Roundtrip conversions
274    // ─────────────────────────────────────────────────────────────────────────────
275
276    #[test]
277    fn roundtrip_g_kg() {
278        let original = Grams::new(5000.0);
279        let converted = original.to::<Kilogram>();
280        let back = converted.to::<Gram>();
281        assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-9);
282    }
283
284    #[test]
285    #[cfg(feature = "astro")]
286    fn roundtrip_kg_solar() {
287        let original = Kilograms::new(1e30);
288        let converted = original.to::<SolarMass>();
289        let back = converted.to::<Kilogram>();
290        assert_relative_eq!(back.value(), original.value(), max_relative = 1e-12);
291    }
292
293    // ─────────────────────────────────────────────────────────────────────────────
294    // Property-based tests
295    // ─────────────────────────────────────────────────────────────────────────────
296
297    proptest! {
298        #[test]
299        fn prop_roundtrip_g_kg(g in 1e-6..1e6f64) {
300            let original = Grams::new(g);
301            let converted = original.to::<Kilogram>();
302            let back = converted.to::<Gram>();
303            prop_assert!((back.value() - original.value()).abs() < 1e-9 * g.abs().max(1.0));
304        }
305
306        #[test]
307        fn prop_g_kg_ratio(g in 1e-6..1e6f64) {
308            let grams = Grams::new(g);
309            let kg = grams.to::<Kilogram>();
310            // 1000 g = 1 kg
311            prop_assert!((grams.value() / kg.value() - 1000.0).abs() < 1e-9);
312        }
313    }
314
315    // ─── Non-SI mass units ──────────────────────────────────────────────────
316
317    #[test]
318    fn tonne_to_kilogram() {
319        let t = Tonnes::new(1.0);
320        let kg = t.to::<Kilogram>();
321        assert_relative_eq!(kg.value(), 1_000.0, max_relative = 1e-12);
322    }
323
324    #[test]
325    #[cfg(feature = "customary")]
326    fn carat_to_gram() {
327        let ct = Carats::new(5.0);
328        let g = ct.to::<Gram>();
329        // 1 ct = 0.2 g
330        assert_relative_eq!(g.value(), 1.0, max_relative = 1e-12);
331    }
332
333    #[test]
334    #[cfg(feature = "customary")]
335    fn grain_to_milligram() {
336        let gr = Grains::new(1.0);
337        let mg = gr.to::<Milligram>();
338        // ratio in code: 6_479_891 / 100_000_000 g = 64.79891 mg
339        assert_relative_eq!(mg.value(), 64.798_91, max_relative = 1e-6);
340    }
341
342    #[test]
343    #[cfg(feature = "customary")]
344    fn pound_to_gram() {
345        let lb = Pounds::new(1.0);
346        let g = lb.to::<Gram>();
347        // 1 lb = 453.59237 g
348        assert_relative_eq!(g.value(), 453.592_37, max_relative = 1e-9);
349    }
350
351    #[test]
352    #[cfg(feature = "customary")]
353    fn ounce_to_gram() {
354        let oz = Ounces::new(16.0);
355        let g = oz.to::<Gram>();
356        // 16 oz = 1 lb = 453.59237 g
357        assert_relative_eq!(g.value(), 453.592_37, max_relative = 1e-9);
358    }
359
360    #[test]
361    #[cfg(feature = "customary")]
362    fn stone_to_pound() {
363        let st = Stones::new(1.0);
364        let lb = st.to::<Pound>();
365        // 1 st = 14 lb
366        assert_relative_eq!(lb.value(), 14.0, max_relative = 1e-12);
367    }
368
369    #[test]
370    #[cfg(feature = "customary")]
371    fn short_ton_to_pound() {
372        let ton = ShortTons::new(1.0);
373        let lb = ton.to::<Pound>();
374        // 1 US short ton = 2000 lb
375        assert_relative_eq!(lb.value(), 2000.0, max_relative = 1e-12);
376    }
377
378    #[test]
379    #[cfg(feature = "customary")]
380    fn long_ton_to_pound() {
381        let ton = LongTons::new(1.0);
382        let lb = ton.to::<Pound>();
383        // 1 UK long ton = 2240 lb
384        assert_relative_eq!(lb.value(), 2240.0, max_relative = 1e-12);
385    }
386
387    #[test]
388    #[cfg(feature = "fundamental-physics")]
389    fn atomic_mass_unit_to_gram() {
390        // 1 u ≈ 1.660539e-24 g
391        let u = AtomicMassUnits::new(1.0);
392        let g = u.to::<Gram>();
393        assert_relative_eq!(g.value(), 1.660_539_068_92e-24, max_relative = 1e-6);
394    }
395
396    // ─── SI gram-prefix sampling ────────────────────────────────────────────
397
398    #[test]
399    fn milligram_to_gram() {
400        let mg = Milligrams::new(1000.0);
401        let g = mg.to::<Gram>();
402        assert_relative_eq!(g.value(), 1.0, max_relative = 1e-12);
403    }
404
405    #[test]
406    fn microgram_to_milligram() {
407        let ug = Micrograms::new(1000.0);
408        let mg = ug.to::<Milligram>();
409        assert_relative_eq!(mg.value(), 1.0, max_relative = 1e-12);
410    }
411
412    #[test]
413    fn symbols_are_correct() {
414        assert_eq!(Kilogram::SYMBOL, "kg");
415        assert_eq!(Gram::SYMBOL, "g");
416        #[cfg(feature = "customary")]
417        assert_eq!(Pound::SYMBOL, "lb");
418        assert_eq!(Tonne::SYMBOL, "t");
419    }
420}