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