Skip to main content

qtty_core/units/
mass.rs

1//! Mass units.
2//!
3//! The canonical scaling unit for this dimension is [`Gram`] (`Gram::RATIO == 1.0`).
4//!
5//! This module aims for practical completeness while avoiding avoidable precision loss:
6//! - **SI grams**: full prefix ladder (yocto … yotta).
7//! - **Defined non-SI**: tonne, avoirdupois units, carat, grain.
8//! - **Science/astro**: atomic mass unit (u/Da), nominal solar mass.
9//!
10//! ```rust
11//! use qtty_core::mass::{Kilograms, SolarMass};
12//!
13//! let m = Kilograms::new(1.0);
14//! let sm = m.to::<SolarMass>();
15//! assert!(sm.value() < 1.0);
16//! ```
17//!
18//! ## All mass units
19//!
20//! ```rust
21//! use qtty_core::mass::*;
22//!
23//! macro_rules! touch {
24//!     ($T:ty, $v:expr) => {{ let q = <$T>::new($v); let _c = q; assert!(q == q); }};
25//! }
26//!
27//! touch!(Grams, 1.0);     touch!(Tonnes, 1.0);   touch!(Carats, 1.0);
28//! touch!(Grains, 1.0);    touch!(Pounds, 1.0);   touch!(Ounces, 1.0);
29//! touch!(Stones, 1.0);    touch!(ShortTons, 1.0); touch!(LongTons, 1.0);
30//! touch!(AtomicMassUnits, 1.0); touch!(SolarMasses, 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/// Gram.
44#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
45#[unit(symbol = "g", dimension = Mass, ratio = 1.0)]
46pub struct Gram;
47/// A quantity measured in grams.
48pub type Grams = Quantity<Gram>;
49/// One gram.
50pub const G: Grams = Grams::new(1.0);
51
52/// Helper macro to declare a gram-based SI mass unit.
53///
54/// Each invocation of this macro defines, for a given prefix on grams:
55/// - a unit struct `$name` (e.g. `Kilogram`),
56/// - a shorthand type alias `$alias` (e.g. `Kg`),
57/// - a quantity type `$qty` (e.g. `Kilograms`), and
58/// - a constant `$one` equal to `1.0` of that quantity.
59///
60/// The `$ratio` argument is the conversion factor to grams, i.e.
61/// `$name::RATIO` such that `1 $sym = $ratio g`.
62macro_rules! si_gram {
63    ($name:ident, $sym:literal, $ratio:expr, $alias:ident, $qty:ident, $one:ident) => {
64        #[doc = concat!("SI mass unit `", stringify!($name), "` with gram-based prefix (symbol `", $sym,"`).")]
65        #[doc = concat!("By definition, `1 ", $sym, " = ", stringify!($ratio), " g`.")]
66        #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
67        #[unit(symbol = $sym, dimension = Mass, ratio = $ratio)]
68        pub struct $name;
69
70        #[doc = concat!("Shorthand alias for [`", stringify!($name), "`]." )]
71        pub type $alias = $name;
72
73        #[doc = concat!("Quantity measured in ", stringify!($name), " (",$sym,").")]
74        pub type $qty = Quantity<$alias>;
75
76        #[doc = concat!("Constant equal to one ", stringify!($name), " (1 ",$sym,").")]
77        pub const $one: $qty = $qty::new(1.0);
78    };
79}
80
81// Full SI prefix ladder (gram-based)
82si_gram!(Yoctogram, "yg", 1e-24, Yg, Yoctograms, YG);
83si_gram!(Zeptogram, "zg", 1e-21, Zg, Zeptograms, ZG);
84si_gram!(Attogram, "ag", 1e-18, Ag, Attograms, AG);
85si_gram!(Femtogram, "fg", 1e-15, Fg, Femtograms, FG);
86si_gram!(Picogram, "pg", 1e-12, Pg, Picograms, PG);
87si_gram!(Nanogram, "ng", 1e-9, Ng, Nanograms, NG);
88si_gram!(Microgram, "µg", 1e-6, Ug, Micrograms, UG);
89si_gram!(Milligram, "mg", 1e-3, Mg, Milligrams, MG);
90si_gram!(Centigram, "cg", 1e-2, Cg, Centigrams, CG);
91si_gram!(Decigram, "dg", 1e-1, Dg, Decigrams, DG);
92
93si_gram!(Decagram, "dag", 1e1, Dag, Decagrams, DAG);
94si_gram!(Hectogram, "hg", 1e2, Hg, Hectograms, HG);
95si_gram!(Kilogram, "kg", 1e3, Kg, Kilograms, KG);
96si_gram!(Megagram, "Mg", 1e6, MgG, Megagrams, MEGAGRAM);
97si_gram!(Gigagram, "Gg", 1e9, Gg, Gigagrams, GG);
98si_gram!(Teragram, "Tg", 1e12, Tg, Teragrams, TG);
99si_gram!(Petagram, "Pg", 1e15, PgG, Petagrams, PETAGRAM);
100si_gram!(Exagram, "Eg", 1e18, Eg, Exagrams, EG);
101si_gram!(Zettagram, "Zg", 1e21, ZgG, Zettagrams, ZETTAGRAM);
102si_gram!(Yottagram, "Yg", 1e24, YgG, Yottagrams, YOTTAGRAM);
103
104/// Tonne (metric ton): `1 t = 1_000_000 g` (exact).
105#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
106#[unit(symbol = "t", dimension = Mass, ratio = 1_000_000.0)]
107pub struct Tonne;
108/// Shorthand type alias for [`Tonne`].
109pub type T = Tonne;
110/// Quantity measured in tonnes.
111pub type Tonnes = Quantity<T>;
112/// One metric tonne.
113pub const TONE: Tonnes = Tonnes::new(1.0);
114
115/// Carat: `1 ct = 0.2 g` (exact).
116#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
117#[unit(symbol = "ct", dimension = Mass, ratio = 1.0 / 5.0)]
118pub struct Carat;
119/// Shorthand type alias for [`Carat`].
120pub type Ct = Carat;
121/// Quantity measured in carats.
122pub type Carats = Quantity<Ct>;
123/// One carat.
124pub const CT: Carats = Carats::new(1.0);
125
126/// Grain: `1 gr = 64.79891 mg` (exact) == `0.064_798_91 g`.
127#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
128#[unit(symbol = "gr", dimension = Mass, ratio = 6_479_891.0 / 1_000_000_000.0)]
129pub struct Grain;
130/// Shorthand type alias for [`Grain`].
131pub type Gr = Grain;
132/// Quantity measured in grains.
133pub type Grains = Quantity<Gr>;
134/// One grain.
135pub const GR: Grains = Grains::new(1.0);
136
137/// Avoirdupois pound: `1 lb = 0.45359237 kg` (exact) == `453.59237 g`.
138#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
139#[unit(symbol = "lb", dimension = Mass, ratio = 45_359_237.0 / 100_000.0)]
140pub struct Pound;
141/// Shorthand type alias for [`Pound`].
142pub type Lb = Pound;
143/// Quantity measured in pounds.
144pub type Pounds = Quantity<Lb>;
145/// One pound.
146pub const LB: Pounds = Pounds::new(1.0);
147
148/// Avoirdupois ounce: `1 oz = 1/16 lb` (exact).
149#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
150#[unit(symbol = "oz", dimension = Mass, ratio = (45_359_237.0 / 100_000.0) / 16.0)]
151pub struct Ounce;
152/// Shorthand type alias for [`Ounce`].
153pub type Oz = Ounce;
154/// Quantity measured in ounces.
155pub type Ounces = Quantity<Oz>;
156/// One ounce.
157pub const OZ: Ounces = Ounces::new(1.0);
158
159/// Avoirdupois stone: `1 st = 14 lb` (exact).
160#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
161#[unit(symbol = "st", dimension = Mass, ratio = (45_359_237.0 / 100_000.0) * 14.0)]
162pub struct Stone;
163/// Shorthand type alias for [`Stone`].
164pub type St = Stone;
165/// Quantity measured in stones.
166pub type Stones = Quantity<St>;
167/// One stone.
168pub const ST: Stones = Stones::new(1.0);
169
170/// Short ton (US customary): `2000 lb` (exact given lb).
171#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
172#[unit(symbol = "ton_us", dimension = Mass, ratio = (45_359_237.0 / 100_000.0) * 2000.0)]
173pub struct ShortTon;
174/// Quantity measured in short tons (US).
175pub type ShortTons = Quantity<ShortTon>;
176/// One short ton (US).
177pub const TON_US: ShortTons = ShortTons::new(1.0);
178
179/// Long ton (Imperial): `2240 lb` (exact given lb).
180#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
181#[unit(symbol = "ton_uk", dimension = Mass, ratio = (45_359_237.0 / 100_000.0) * 2240.0)]
182pub struct LongTon;
183/// Quantity measured in long tons (UK).
184pub type LongTons = Quantity<LongTon>;
185/// One long ton (UK).
186pub const TON_UK: LongTons = LongTons::new(1.0);
187
188/// Unified atomic mass unit (u), a.k.a. dalton (Da).
189///
190/// Stored in grams using the CODATA recommended value for `m_u` in kilograms, converted by `1 kg = 1000 g`.
191#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
192#[unit(symbol = "u", dimension = Mass, ratio = 1.660_539_068_92e-24)]
193pub struct AtomicMassUnit;
194/// Type alias shorthand for [`AtomicMassUnit`].
195pub type Dalton = AtomicMassUnit;
196/// Quantity measured in atomic mass units.
197pub type AtomicMassUnits = Quantity<AtomicMassUnit>;
198/// One atomic mass unit.
199pub const U: AtomicMassUnits = AtomicMassUnits::new(1.0);
200
201/// Nominal solar mass (IAU 2015 Resolution B3; grams per M☉).
202///
203/// This is a **conversion constant** (nominal), not a “best estimate” of the Sun’s true mass.
204#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
205#[unit(symbol = "M☉", dimension = Mass, ratio = 1.988_416e33)]
206pub struct SolarMass;
207/// A quantity measured in solar masses.
208pub type SolarMasses = Quantity<SolarMass>;
209/// One nominal solar mass.
210pub const MSUN: SolarMasses = SolarMasses::new(1.0);
211
212// Generate all bidirectional From implementations between mass units.
213crate::impl_unit_from_conversions!(
214    Gram,
215    Yoctogram,
216    Zeptogram,
217    Attogram,
218    Femtogram,
219    Picogram,
220    Nanogram,
221    Microgram,
222    Milligram,
223    Centigram,
224    Decigram,
225    Decagram,
226    Hectogram,
227    Kilogram,
228    Megagram,
229    Gigagram,
230    Teragram,
231    Petagram,
232    Exagram,
233    Zettagram,
234    Yottagram,
235    Tonne,
236    Carat,
237    Grain,
238    Pound,
239    Ounce,
240    Stone,
241    ShortTon,
242    LongTon,
243    AtomicMassUnit,
244    SolarMass
245);
246
247// Optional cross-unit operator support (`==`, `<`, etc.).
248#[cfg(feature = "cross-unit-ops")]
249crate::impl_unit_cross_unit_ops!(
250    Gram,
251    Yoctogram,
252    Zeptogram,
253    Attogram,
254    Femtogram,
255    Picogram,
256    Nanogram,
257    Microgram,
258    Milligram,
259    Centigram,
260    Decigram,
261    Decagram,
262    Hectogram,
263    Kilogram,
264    Megagram,
265    Gigagram,
266    Teragram,
267    Petagram,
268    Exagram,
269    Zettagram,
270    Yottagram,
271    Tonne,
272    Carat,
273    Grain,
274    Pound,
275    Ounce,
276    Stone,
277    ShortTon,
278    LongTon,
279    AtomicMassUnit,
280    SolarMass
281);
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use approx::{assert_abs_diff_eq, assert_relative_eq};
287    use proptest::prelude::*;
288
289    // ─────────────────────────────────────────────────────────────────────────────
290    // Basic conversions
291    // ─────────────────────────────────────────────────────────────────────────────
292
293    #[test]
294    fn gram_to_kilogram() {
295        let g = Grams::new(1000.0);
296        let kg = g.to::<Kilogram>();
297        assert_abs_diff_eq!(kg.value(), 1.0, epsilon = 1e-12);
298    }
299
300    #[test]
301    fn kilogram_to_gram() {
302        let kg = Kilograms::new(1.0);
303        let g = kg.to::<Gram>();
304        assert_abs_diff_eq!(g.value(), 1000.0, epsilon = 1e-9);
305    }
306
307    #[test]
308    fn solar_mass_to_grams() {
309        let sm = SolarMasses::new(1.0);
310        let g = sm.to::<Gram>();
311        // 1 M☉ ≈ 1.988416e33 grams
312        assert_relative_eq!(g.value(), 1.988416e33, max_relative = 1e-5);
313    }
314
315    #[test]
316    fn solar_mass_to_kilograms() {
317        let sm = SolarMasses::new(1.0);
318        let kg = sm.to::<Kilogram>();
319        // 1 M☉ ≈ 1.988416e30 kg
320        assert_relative_eq!(kg.value(), 1.988416e30, max_relative = 1e-5);
321    }
322
323    #[test]
324    fn kilograms_to_solar_mass() {
325        // Earth mass ≈ 5.97e24 kg ≈ 3e-6 M☉
326        let earth_kg = Kilograms::new(5.97e24);
327        let earth_sm = earth_kg.to::<SolarMass>();
328        assert_relative_eq!(earth_sm.value(), 3.0e-6, max_relative = 0.01);
329    }
330
331    // ─────────────────────────────────────────────────────────────────────────────
332    // Solar mass sanity checks
333    // ─────────────────────────────────────────────────────────────────────────────
334
335    #[test]
336    fn solar_mass_ratio_sanity() {
337        // 1 M☉ = 1.988416e33 g, so RATIO should be that value
338        assert_relative_eq!(SolarMass::RATIO, 1.988416e33, max_relative = 1e-5);
339    }
340
341    #[test]
342    fn solar_mass_order_of_magnitude() {
343        // The Sun's mass is about 2e30 kg
344        let sun = SolarMasses::new(1.0);
345        let kg = sun.to::<Kilogram>();
346        assert!(kg.value() > 1e30);
347        assert!(kg.value() < 1e31);
348    }
349
350    // ─────────────────────────────────────────────────────────────────────────────
351    // Roundtrip conversions
352    // ─────────────────────────────────────────────────────────────────────────────
353
354    #[test]
355    fn roundtrip_g_kg() {
356        let original = Grams::new(5000.0);
357        let converted = original.to::<Kilogram>();
358        let back = converted.to::<Gram>();
359        assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-9);
360    }
361
362    #[test]
363    fn roundtrip_kg_solar() {
364        let original = Kilograms::new(1e30);
365        let converted = original.to::<SolarMass>();
366        let back = converted.to::<Kilogram>();
367        assert_relative_eq!(back.value(), original.value(), max_relative = 1e-12);
368    }
369
370    // ─────────────────────────────────────────────────────────────────────────────
371    // Property-based tests
372    // ─────────────────────────────────────────────────────────────────────────────
373
374    proptest! {
375        #[test]
376        fn prop_roundtrip_g_kg(g in 1e-6..1e6f64) {
377            let original = Grams::new(g);
378            let converted = original.to::<Kilogram>();
379            let back = converted.to::<Gram>();
380            prop_assert!((back.value() - original.value()).abs() < 1e-9 * g.abs().max(1.0));
381        }
382
383        #[test]
384        fn prop_g_kg_ratio(g in 1e-6..1e6f64) {
385            let grams = Grams::new(g);
386            let kg = grams.to::<Kilogram>();
387            // 1000 g = 1 kg
388            prop_assert!((grams.value() / kg.value() - 1000.0).abs() < 1e-9);
389        }
390    }
391
392    // ─── Non-SI mass units ──────────────────────────────────────────────────
393
394    #[test]
395    fn tonne_to_kilogram() {
396        let t = Tonnes::new(1.0);
397        let kg = t.to::<Kilogram>();
398        assert_relative_eq!(kg.value(), 1_000.0, max_relative = 1e-12);
399    }
400
401    #[test]
402    fn carat_to_gram() {
403        let ct = Carats::new(5.0);
404        let g = ct.to::<Gram>();
405        // 1 ct = 0.2 g
406        assert_relative_eq!(g.value(), 1.0, max_relative = 1e-12);
407    }
408
409    #[test]
410    fn grain_to_milligram() {
411        let gr = Grains::new(1.0);
412        let mg = gr.to::<Milligram>();
413        // ratio in code: 6_479_891 / 1_000_000_000 g = 6.479891 mg
414        assert_relative_eq!(mg.value(), 6.479_891, max_relative = 1e-6);
415    }
416
417    #[test]
418    fn pound_to_gram() {
419        let lb = Pounds::new(1.0);
420        let g = lb.to::<Gram>();
421        // 1 lb = 453.59237 g
422        assert_relative_eq!(g.value(), 453.592_37, max_relative = 1e-9);
423    }
424
425    #[test]
426    fn ounce_to_gram() {
427        let oz = Ounces::new(16.0);
428        let g = oz.to::<Gram>();
429        // 16 oz = 1 lb = 453.59237 g
430        assert_relative_eq!(g.value(), 453.592_37, max_relative = 1e-9);
431    }
432
433    #[test]
434    fn stone_to_pound() {
435        let st = Stones::new(1.0);
436        let lb = st.to::<Pound>();
437        // 1 st = 14 lb
438        assert_relative_eq!(lb.value(), 14.0, max_relative = 1e-12);
439    }
440
441    #[test]
442    fn short_ton_to_pound() {
443        let ton = ShortTons::new(1.0);
444        let lb = ton.to::<Pound>();
445        // 1 US short ton = 2000 lb
446        assert_relative_eq!(lb.value(), 2000.0, max_relative = 1e-12);
447    }
448
449    #[test]
450    fn long_ton_to_pound() {
451        let ton = LongTons::new(1.0);
452        let lb = ton.to::<Pound>();
453        // 1 UK long ton = 2240 lb
454        assert_relative_eq!(lb.value(), 2240.0, max_relative = 1e-12);
455    }
456
457    #[test]
458    fn atomic_mass_unit_to_gram() {
459        // 1 u ≈ 1.660539e-24 g
460        let u = AtomicMassUnits::new(1.0);
461        let g = u.to::<Gram>();
462        assert_relative_eq!(g.value(), 1.660_539_068_92e-24, max_relative = 1e-6);
463    }
464
465    // ─── SI gram-prefix sampling ────────────────────────────────────────────
466
467    #[test]
468    fn milligram_to_gram() {
469        let mg = Milligrams::new(1000.0);
470        let g = mg.to::<Gram>();
471        assert_relative_eq!(g.value(), 1.0, max_relative = 1e-12);
472    }
473
474    #[test]
475    fn microgram_to_milligram() {
476        let ug = Micrograms::new(1000.0);
477        let mg = ug.to::<Milligram>();
478        assert_relative_eq!(mg.value(), 1.0, max_relative = 1e-12);
479    }
480
481    #[test]
482    fn symbols_are_correct() {
483        assert_eq!(Kilogram::SYMBOL, "kg");
484        assert_eq!(Gram::SYMBOL, "g");
485        assert_eq!(Pound::SYMBOL, "lb");
486        assert_eq!(Tonne::SYMBOL, "t");
487    }
488}