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
18use crate::{Dimension, Quantity, Unit};
19use qtty_derive::Unit;
20
21/// Dimension tag for mass.
22pub enum Mass {}
23impl Dimension for Mass {}
24
25/// Marker trait for any [`Unit`] whose dimension is [`Mass`].
26pub trait MassUnit: Unit<Dim = Mass> {}
27impl<T: Unit<Dim = Mass>> MassUnit for T {}
28
29/// Gram.
30#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
31#[unit(symbol = "g", dimension = Mass, ratio = 1.0)]
32pub struct Gram;
33/// A quantity measured in grams.
34pub type Grams = Quantity<Gram>;
35/// One gram.
36pub const G: Grams = Grams::new(1.0);
37
38/// Helper macro to declare a gram-based SI mass unit.
39///
40/// Each invocation of this macro defines, for a given prefix on grams:
41/// - a unit struct `$name` (e.g. `Kilogram`),
42/// - a shorthand type alias `$alias` (e.g. `Kg`),
43/// - a quantity type `$qty` (e.g. `Kilograms`), and
44/// - a constant `$one` equal to `1.0` of that quantity.
45///
46/// The `$ratio` argument is the conversion factor to grams, i.e.
47/// `$name::RATIO` such that `1 $sym = $ratio g`.
48macro_rules! si_gram {
49    ($name:ident, $sym:literal, $ratio:expr, $alias:ident, $qty:ident, $one:ident) => {
50        #[doc = concat!("SI mass unit `", stringify!($name), "` with gram-based prefix (symbol `", $sym,"`).")]
51        #[doc = concat!("By definition, `1 ", $sym, " = ", stringify!($ratio), " g`.")]
52        #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
53        #[unit(symbol = $sym, dimension = Mass, ratio = $ratio)]
54        pub struct $name;
55
56        #[doc = concat!("Shorthand alias for [`", stringify!($name), "`]." )]
57        pub type $alias = $name;
58
59        #[doc = concat!("Quantity measured in ", stringify!($name), " (",$sym,").")]
60        pub type $qty = Quantity<$alias>;
61
62        #[doc = concat!("Constant equal to one ", stringify!($name), " (1 ",$sym,").")]
63        pub const $one: $qty = $qty::new(1.0);
64    };
65}
66
67// Full SI prefix ladder (gram-based)
68si_gram!(Yoctogram, "yg", 1e-24, Yg, Yoctograms, YG);
69si_gram!(Zeptogram, "zg", 1e-21, Zg, Zeptograms, ZG);
70si_gram!(Attogram, "ag", 1e-18, Ag, Attograms, AG);
71si_gram!(Femtogram, "fg", 1e-15, Fg, Femtograms, FG);
72si_gram!(Picogram, "pg", 1e-12, Pg, Picograms, PG);
73si_gram!(Nanogram, "ng", 1e-9, Ng, Nanograms, NG);
74si_gram!(Microgram, "µg", 1e-6, Ug, Micrograms, UG);
75si_gram!(Milligram, "mg", 1e-3, Mg, Milligrams, MG);
76si_gram!(Centigram, "cg", 1e-2, Cg, Centigrams, CG);
77si_gram!(Decigram, "dg", 1e-1, Dg, Decigrams, DG);
78
79si_gram!(Decagram, "dag", 1e1, Dag, Decagrams, DAG);
80si_gram!(Hectogram, "hg", 1e2, Hg, Hectograms, HG);
81si_gram!(Kilogram, "kg", 1e3, Kg, Kilograms, KG);
82si_gram!(Megagram, "Mg", 1e6, MgG, Megagrams, MEGAGRAM);
83si_gram!(Gigagram, "Gg", 1e9, Gg, Gigagrams, GG);
84si_gram!(Teragram, "Tg", 1e12, Tg, Teragrams, TG);
85si_gram!(Petagram, "Pg", 1e15, PgG, Petagrams, PETAGRAM);
86si_gram!(Exagram, "Eg", 1e18, Eg, Exagrams, EG);
87si_gram!(Zettagram, "Zg", 1e21, ZgG, Zettagrams, ZETTAGRAM);
88si_gram!(Yottagram, "Yg", 1e24, YgG, Yottagrams, YOTTAGRAM);
89
90/// Tonne (metric ton): `1 t = 1_000_000 g` (exact).
91#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
92#[unit(symbol = "t", dimension = Mass, ratio = 1_000_000.0)]
93pub struct Tonne;
94/// Shorthand type alias for [`Tonne`].
95pub type T = Tonne;
96/// Quantity measured in tonnes.
97pub type Tonnes = Quantity<T>;
98/// One metric tonne.
99pub const TONE: Tonnes = Tonnes::new(1.0);
100
101/// Carat: `1 ct = 0.2 g` (exact).
102#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
103#[unit(symbol = "ct", dimension = Mass, ratio = 1.0 / 5.0)]
104pub struct Carat;
105/// Shorthand type alias for [`Carat`].
106pub type Ct = Carat;
107/// Quantity measured in carats.
108pub type Carats = Quantity<Ct>;
109/// One carat.
110pub const CT: Carats = Carats::new(1.0);
111
112/// Grain: `1 gr = 64.79891 mg` (exact) == `0.064_798_91 g`.
113#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
114#[unit(symbol = "gr", dimension = Mass, ratio = 6_479_891.0 / 1_000_000_000.0)]
115pub struct Grain;
116/// Shorthand type alias for [`Grain`].
117pub type Gr = Grain;
118/// Quantity measured in grains.
119pub type Grains = Quantity<Gr>;
120/// One grain.
121pub const GR: Grains = Grains::new(1.0);
122
123/// Avoirdupois pound: `1 lb = 0.45359237 kg` (exact) == `453.59237 g`.
124#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
125#[unit(symbol = "lb", dimension = Mass, ratio = 45_359_237.0 / 100_000.0)]
126pub struct Pound;
127/// Shorthand type alias for [`Pound`].
128pub type Lb = Pound;
129/// Quantity measured in pounds.
130pub type Pounds = Quantity<Lb>;
131/// One pound.
132pub const LB: Pounds = Pounds::new(1.0);
133
134/// Avoirdupois ounce: `1 oz = 1/16 lb` (exact).
135#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
136#[unit(symbol = "oz", dimension = Mass, ratio = (45_359_237.0 / 100_000.0) / 16.0)]
137pub struct Ounce;
138/// Shorthand type alias for [`Ounce`].
139pub type Oz = Ounce;
140/// Quantity measured in ounces.
141pub type Ounces = Quantity<Oz>;
142/// One ounce.
143pub const OZ: Ounces = Ounces::new(1.0);
144
145/// Avoirdupois stone: `1 st = 14 lb` (exact).
146#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
147#[unit(symbol = "st", dimension = Mass, ratio = (45_359_237.0 / 100_000.0) * 14.0)]
148pub struct Stone;
149/// Shorthand type alias for [`Stone`].
150pub type St = Stone;
151/// Quantity measured in stones.
152pub type Stones = Quantity<St>;
153/// One stone.
154pub const ST: Stones = Stones::new(1.0);
155
156/// Short ton (US customary): `2000 lb` (exact given lb).
157#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
158#[unit(symbol = "ton_us", dimension = Mass, ratio = (45_359_237.0 / 100_000.0) * 2000.0)]
159pub struct ShortTon;
160/// Quantity measured in short tons (US).
161pub type ShortTons = Quantity<ShortTon>;
162/// One short ton (US).
163pub const TON_US: ShortTons = ShortTons::new(1.0);
164
165/// Long ton (Imperial): `2240 lb` (exact given lb).
166#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
167#[unit(symbol = "ton_uk", dimension = Mass, ratio = (45_359_237.0 / 100_000.0) * 2240.0)]
168pub struct LongTon;
169/// Quantity measured in long tons (UK).
170pub type LongTons = Quantity<LongTon>;
171/// One long ton (UK).
172pub const TON_UK: LongTons = LongTons::new(1.0);
173
174/// Unified atomic mass unit (u), a.k.a. dalton (Da).
175///
176/// Stored in grams using the CODATA recommended value for `m_u` in kilograms, converted by `1 kg = 1000 g`.
177#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
178#[unit(symbol = "u", dimension = Mass, ratio = 1.660_539_068_92e-24)]
179pub struct AtomicMassUnit;
180/// Type alias shorthand for [`AtomicMassUnit`].
181pub type Dalton = AtomicMassUnit;
182/// Quantity measured in atomic mass units.
183pub type AtomicMassUnits = Quantity<AtomicMassUnit>;
184/// One atomic mass unit.
185pub const U: AtomicMassUnits = AtomicMassUnits::new(1.0);
186
187/// Nominal solar mass (IAU 2015 Resolution B3; grams per M☉).
188///
189/// This is a **conversion constant** (nominal), not a “best estimate” of the Sun’s true mass.
190#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
191#[unit(symbol = "M☉", dimension = Mass, ratio = 1.988_416e33)]
192pub struct SolarMass;
193/// A quantity measured in solar masses.
194pub type SolarMasses = Quantity<SolarMass>;
195/// One nominal solar mass.
196pub const MSUN: SolarMasses = SolarMasses::new(1.0);
197
198// Generate all bidirectional From implementations between mass units
199crate::impl_unit_conversions!(
200    Gram,
201    Yoctogram,
202    Zeptogram,
203    Attogram,
204    Femtogram,
205    Picogram,
206    Nanogram,
207    Microgram,
208    Milligram,
209    Centigram,
210    Decigram,
211    Decagram,
212    Hectogram,
213    Kilogram,
214    Megagram,
215    Gigagram,
216    Teragram,
217    Petagram,
218    Exagram,
219    Zettagram,
220    Yottagram,
221    Tonne,
222    Carat,
223    Grain,
224    Pound,
225    Ounce,
226    Stone,
227    ShortTon,
228    LongTon,
229    AtomicMassUnit,
230    SolarMass
231);
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use approx::{assert_abs_diff_eq, assert_relative_eq};
237    use proptest::prelude::*;
238
239    // ─────────────────────────────────────────────────────────────────────────────
240    // Basic conversions
241    // ─────────────────────────────────────────────────────────────────────────────
242
243    #[test]
244    fn gram_to_kilogram() {
245        let g = Grams::new(1000.0);
246        let kg = g.to::<Kilogram>();
247        assert_abs_diff_eq!(kg.value(), 1.0, epsilon = 1e-12);
248    }
249
250    #[test]
251    fn kilogram_to_gram() {
252        let kg = Kilograms::new(1.0);
253        let g = kg.to::<Gram>();
254        assert_abs_diff_eq!(g.value(), 1000.0, epsilon = 1e-9);
255    }
256
257    #[test]
258    fn solar_mass_to_grams() {
259        let sm = SolarMasses::new(1.0);
260        let g = sm.to::<Gram>();
261        // 1 M☉ ≈ 1.988416e33 grams
262        assert_relative_eq!(g.value(), 1.988416e33, max_relative = 1e-5);
263    }
264
265    #[test]
266    fn solar_mass_to_kilograms() {
267        let sm = SolarMasses::new(1.0);
268        let kg = sm.to::<Kilogram>();
269        // 1 M☉ ≈ 1.988416e30 kg
270        assert_relative_eq!(kg.value(), 1.988416e30, max_relative = 1e-5);
271    }
272
273    #[test]
274    fn kilograms_to_solar_mass() {
275        // Earth mass ≈ 5.97e24 kg ≈ 3e-6 M☉
276        let earth_kg = Kilograms::new(5.97e24);
277        let earth_sm = earth_kg.to::<SolarMass>();
278        assert_relative_eq!(earth_sm.value(), 3.0e-6, max_relative = 0.01);
279    }
280
281    // ─────────────────────────────────────────────────────────────────────────────
282    // Solar mass sanity checks
283    // ─────────────────────────────────────────────────────────────────────────────
284
285    #[test]
286    fn solar_mass_ratio_sanity() {
287        // 1 M☉ = 1.988416e33 g, so RATIO should be that value
288        assert_relative_eq!(SolarMass::RATIO, 1.988416e33, max_relative = 1e-5);
289    }
290
291    #[test]
292    fn solar_mass_order_of_magnitude() {
293        // The Sun's mass is about 2e30 kg
294        let sun = SolarMasses::new(1.0);
295        let kg = sun.to::<Kilogram>();
296        assert!(kg.value() > 1e30);
297        assert!(kg.value() < 1e31);
298    }
299
300    // ─────────────────────────────────────────────────────────────────────────────
301    // Roundtrip conversions
302    // ─────────────────────────────────────────────────────────────────────────────
303
304    #[test]
305    fn roundtrip_g_kg() {
306        let original = Grams::new(5000.0);
307        let converted = original.to::<Kilogram>();
308        let back = converted.to::<Gram>();
309        assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-9);
310    }
311
312    #[test]
313    fn roundtrip_kg_solar() {
314        let original = Kilograms::new(1e30);
315        let converted = original.to::<SolarMass>();
316        let back = converted.to::<Kilogram>();
317        assert_relative_eq!(back.value(), original.value(), max_relative = 1e-12);
318    }
319
320    // ─────────────────────────────────────────────────────────────────────────────
321    // Property-based tests
322    // ─────────────────────────────────────────────────────────────────────────────
323
324    proptest! {
325        #[test]
326        fn prop_roundtrip_g_kg(g in 1e-6..1e6f64) {
327            let original = Grams::new(g);
328            let converted = original.to::<Kilogram>();
329            let back = converted.to::<Gram>();
330            prop_assert!((back.value() - original.value()).abs() < 1e-9 * g.abs().max(1.0));
331        }
332
333        #[test]
334        fn prop_g_kg_ratio(g in 1e-6..1e6f64) {
335            let grams = Grams::new(g);
336            let kg = grams.to::<Kilogram>();
337            // 1000 g = 1 kg
338            prop_assert!((grams.value() / kg.value() - 1000.0).abs() < 1e-9);
339        }
340    }
341}