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_from_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// Optional cross-unit operator support (`==`, `<`, etc.).
233#[cfg(feature = "cross-unit-ops")]
234crate::impl_unit_cross_unit_ops!(
235    Gram,
236    Yoctogram,
237    Zeptogram,
238    Attogram,
239    Femtogram,
240    Picogram,
241    Nanogram,
242    Microgram,
243    Milligram,
244    Centigram,
245    Decigram,
246    Decagram,
247    Hectogram,
248    Kilogram,
249    Megagram,
250    Gigagram,
251    Teragram,
252    Petagram,
253    Exagram,
254    Zettagram,
255    Yottagram,
256    Tonne,
257    Carat,
258    Grain,
259    Pound,
260    Ounce,
261    Stone,
262    ShortTon,
263    LongTon,
264    AtomicMassUnit,
265    SolarMass
266);
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use approx::{assert_abs_diff_eq, assert_relative_eq};
272    use proptest::prelude::*;
273
274    // ─────────────────────────────────────────────────────────────────────────────
275    // Basic conversions
276    // ─────────────────────────────────────────────────────────────────────────────
277
278    #[test]
279    fn gram_to_kilogram() {
280        let g = Grams::new(1000.0);
281        let kg = g.to::<Kilogram>();
282        assert_abs_diff_eq!(kg.value(), 1.0, epsilon = 1e-12);
283    }
284
285    #[test]
286    fn kilogram_to_gram() {
287        let kg = Kilograms::new(1.0);
288        let g = kg.to::<Gram>();
289        assert_abs_diff_eq!(g.value(), 1000.0, epsilon = 1e-9);
290    }
291
292    #[test]
293    fn solar_mass_to_grams() {
294        let sm = SolarMasses::new(1.0);
295        let g = sm.to::<Gram>();
296        // 1 M☉ ≈ 1.988416e33 grams
297        assert_relative_eq!(g.value(), 1.988416e33, max_relative = 1e-5);
298    }
299
300    #[test]
301    fn solar_mass_to_kilograms() {
302        let sm = SolarMasses::new(1.0);
303        let kg = sm.to::<Kilogram>();
304        // 1 M☉ ≈ 1.988416e30 kg
305        assert_relative_eq!(kg.value(), 1.988416e30, max_relative = 1e-5);
306    }
307
308    #[test]
309    fn kilograms_to_solar_mass() {
310        // Earth mass ≈ 5.97e24 kg ≈ 3e-6 M☉
311        let earth_kg = Kilograms::new(5.97e24);
312        let earth_sm = earth_kg.to::<SolarMass>();
313        assert_relative_eq!(earth_sm.value(), 3.0e-6, max_relative = 0.01);
314    }
315
316    // ─────────────────────────────────────────────────────────────────────────────
317    // Solar mass sanity checks
318    // ─────────────────────────────────────────────────────────────────────────────
319
320    #[test]
321    fn solar_mass_ratio_sanity() {
322        // 1 M☉ = 1.988416e33 g, so RATIO should be that value
323        assert_relative_eq!(SolarMass::RATIO, 1.988416e33, max_relative = 1e-5);
324    }
325
326    #[test]
327    fn solar_mass_order_of_magnitude() {
328        // The Sun's mass is about 2e30 kg
329        let sun = SolarMasses::new(1.0);
330        let kg = sun.to::<Kilogram>();
331        assert!(kg.value() > 1e30);
332        assert!(kg.value() < 1e31);
333    }
334
335    // ─────────────────────────────────────────────────────────────────────────────
336    // Roundtrip conversions
337    // ─────────────────────────────────────────────────────────────────────────────
338
339    #[test]
340    fn roundtrip_g_kg() {
341        let original = Grams::new(5000.0);
342        let converted = original.to::<Kilogram>();
343        let back = converted.to::<Gram>();
344        assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-9);
345    }
346
347    #[test]
348    fn roundtrip_kg_solar() {
349        let original = Kilograms::new(1e30);
350        let converted = original.to::<SolarMass>();
351        let back = converted.to::<Kilogram>();
352        assert_relative_eq!(back.value(), original.value(), max_relative = 1e-12);
353    }
354
355    // ─────────────────────────────────────────────────────────────────────────────
356    // Property-based tests
357    // ─────────────────────────────────────────────────────────────────────────────
358
359    proptest! {
360        #[test]
361        fn prop_roundtrip_g_kg(g in 1e-6..1e6f64) {
362            let original = Grams::new(g);
363            let converted = original.to::<Kilogram>();
364            let back = converted.to::<Gram>();
365            prop_assert!((back.value() - original.value()).abs() < 1e-9 * g.abs().max(1.0));
366        }
367
368        #[test]
369        fn prop_g_kg_ratio(g in 1e-6..1e6f64) {
370            let grams = Grams::new(g);
371            let kg = grams.to::<Kilogram>();
372            // 1000 g = 1 kg
373            prop_assert!((grams.value() / kg.value() - 1000.0).abs() < 1e-9);
374        }
375    }
376}