Skip to main content

logicaffeine_base/
quantity.rs

1//! Quantities: an exact magnitude carrying a physical [`Dimension`] and a unit.
2//!
3//! A *quantity* is a number with a unit — `2 inches`, `5 kilograms`, `9.8 m/s²`. Internally a
4//! [`Quantity`] stores its magnitude in the **SI base unit** for its dimension (metres, kilograms,
5//! seconds, …) as an exact [`Rational`], so:
6//!
7//! - addition/subtraction require the SAME dimension and just add magnitudes,
8//! - multiplication/division combine dimensions (`Length × Length = Area`),
9//! - conversion to another unit of the same dimension is exact (`÷ scale`, offset-aware), and
10//! - converting across dimensions (`Length → Mass`) is impossible — the forbidden cast.
11//!
12//! Because every unit's scale to the SI base is an exact rational (1 inch = 127/5000 m *exactly*),
13//! the whole thing is lossless: `2 inches + 5 centimetres` is `63/625 m`, which `in feet` is exactly
14//! `42/127`. A [`Unit`] may be *affine* (a nonzero `offset`, like °C/°F), so a temperature converts
15//! with scale AND offset; linear units have `offset = 0`.
16
17use crate::dimension::Dimension;
18use crate::numeric::Rational;
19
20/// A unit of measurement: its dimension and how to map a value in this unit to the SI base.
21/// `si = value · scale + offset`. Linear units have `offset = 0`; affine units (°C, °F) do not.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct Unit {
24    pub symbol: &'static str,
25    pub dimension: Dimension,
26    /// Multiply a value in this unit by `scale` to reach the SI base unit. Exact, nonzero.
27    pub scale: Rational,
28    /// The affine zero offset, added after scaling (0 for linear units).
29    pub offset: Rational,
30}
31
32impl Unit {
33    /// A linear unit (`si = value · scale`).
34    pub fn linear(symbol: &'static str, dimension: Dimension, scale: Rational) -> Unit {
35        Unit { symbol, dimension, scale, offset: Rational::zero() }
36    }
37
38    /// An affine unit (`si = value · scale + offset`) — temperatures.
39    pub fn affine(symbol: &'static str, dimension: Dimension, scale: Rational, offset: Rational) -> Unit {
40        Unit { symbol, dimension, scale, offset }
41    }
42}
43
44/// An exact quantity: a magnitude in the SI base unit, tagged with its dimension.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct Quantity {
47    /// The magnitude expressed in the SI base unit of `dimension` (metres, kilograms, …).
48    magnitude: Rational,
49    dimension: Dimension,
50}
51
52impl Quantity {
53    /// A quantity of `value` of `unit` — normalized to the SI base (`value·scale + offset`).
54    pub fn of(value: Rational, unit: &Unit) -> Quantity {
55        Quantity {
56            magnitude: value.mul(&unit.scale).add(&unit.offset),
57            dimension: unit.dimension,
58        }
59    }
60
61    /// A dimensionless quantity (a pure number) — for scaling.
62    pub fn scalar(value: Rational) -> Quantity {
63        Quantity { magnitude: value, dimension: Dimension::DIMENSIONLESS }
64    }
65
66    /// A quantity given directly by its SI-base magnitude and dimension — used for physical
67    /// constants and any value whose dimension has no single named unit (e.g. the gas constant).
68    pub fn si(magnitude: Rational, dimension: Dimension) -> Quantity {
69        Quantity { magnitude, dimension }
70    }
71
72    /// The magnitude in the SI base unit.
73    pub fn magnitude_si(&self) -> &Rational {
74        &self.magnitude
75    }
76
77    pub fn dimension(&self) -> Dimension {
78        self.dimension
79    }
80
81    /// The magnitude expressed IN `unit` — `None` if the dimensions differ (the forbidden cast).
82    /// Linear: `(si)/scale`; affine: `(si − offset)/scale`. `1 mile in feet` is exactly `5280`.
83    pub fn in_unit(&self, unit: &Unit) -> Option<Rational> {
84        if self.dimension != unit.dimension {
85            return None;
86        }
87        self.magnitude.sub(&unit.offset).div(&unit.scale)
88    }
89
90    /// `self + other` — `None` unless the dimensions match (you cannot add Length to Mass).
91    pub fn add(&self, other: &Quantity) -> Option<Quantity> {
92        if self.dimension != other.dimension {
93            return None;
94        }
95        Some(Quantity { magnitude: self.magnitude.add(&other.magnitude), dimension: self.dimension })
96    }
97
98    /// `self − other` — `None` unless the dimensions match.
99    pub fn sub(&self, other: &Quantity) -> Option<Quantity> {
100        if self.dimension != other.dimension {
101            return None;
102        }
103        Some(Quantity { magnitude: self.magnitude.sub(&other.magnitude), dimension: self.dimension })
104    }
105
106    /// `self × other` — magnitudes multiply, dimensions combine (`Length × Length = Area`).
107    pub fn mul(&self, other: &Quantity) -> Quantity {
108        Quantity {
109            magnitude: self.magnitude.mul(&other.magnitude),
110            dimension: self.dimension.mul(other.dimension),
111        }
112    }
113
114    /// `self ÷ other` — magnitudes divide, dimensions subtract. `None` on a zero divisor magnitude.
115    pub fn div(&self, other: &Quantity) -> Option<Quantity> {
116        Some(Quantity {
117            magnitude: self.magnitude.div(&other.magnitude)?,
118            dimension: self.dimension.div(other.dimension),
119        })
120    }
121
122    /// Choose the most human-readable unit from `ladder` and return `(magnitude_in_it, unit)`.
123    /// The winner is the unit whose magnitude has the smallest absolute value that is still `≥ 1`
124    /// (so `1500 m → (1.5, km)`, `0.5 km → (500, m)`); if every candidate is below 1 the largest
125    /// magnitude (closest to 1 from below) wins. Units of a different dimension are skipped, so a
126    /// ladder with no same-dimension unit yields `None`. Exact — the conversion never rounds, and
127    /// `Quantity::of(mag, unit)` reconstructs `self`. This is Design Law #3 (auto-scale to the most
128    /// human unit), kept pure at the base layer so the interpreter `Showable` and AOT both reuse it.
129    pub fn in_best_unit<'a>(&self, ladder: &'a [Unit]) -> Option<(Rational, &'a Unit)> {
130        let one = Rational::one();
131        let mut best: Option<(Rational, &Unit)> = None;
132        for u in ladder {
133            let Some(mag) = self.in_unit(u) else { continue };
134            best = Some(match best {
135                None => (mag, u),
136                Some((best_mag, best_u)) => {
137                    let (abs, best_abs) = (mag.abs(), best_mag.abs());
138                    let take = match (abs >= one, best_abs >= one) {
139                        (true, true) => abs < best_abs,   // both ≥ 1: prefer the one closest to 1 from above
140                        (true, false) => true,            // candidate qualifies, incumbent doesn't
141                        (false, true) => false,           // keep the qualifying incumbent
142                        (false, false) => abs > best_abs, // both < 1: prefer the one closest to 1 from below
143                    };
144                    if take { (mag, u) } else { (best_mag, best_u) }
145                }
146            });
147        }
148        best
149    }
150}
151
152/// The unit catalog — every scale an EXACT rational to the SI base. Organized by dimension.
153/// (Irrational-scaled units like `parsec` (648000/π·AU) and `mach` are intentionally omitted
154/// until a float-backed quantity exists; everything here is lossless.) A later wave moves this
155/// to a growable data file; the math and the names are settled here first.
156pub mod units {
157    use super::Unit;
158    use crate::dimension::Dimension;
159    use crate::numeric::Rational;
160
161    fn r(n: i64, d: i64) -> Rational {
162        Rational::from_ratio_i64(n, d).expect("nonzero denominator")
163    }
164    fn i(n: i64) -> Rational {
165        Rational::from_i64(n)
166    }
167    /// `n³` of a rational (exact).
168    fn cube(x: Rational) -> Rational {
169        x.pow(3).expect("cube exists")
170    }
171    /// `n²` of a rational (exact).
172    fn square(x: Rational) -> Rational {
173        x.pow(2).expect("square exists")
174    }
175
176    // Shared exact anchors.
177    fn inch_m() -> Rational { r(127, 5000) } // 1 inch = 0.0254 m, exactly
178    fn foot_m() -> Rational { r(381, 1250) } // 0.3048 m
179    fn lb_kg() -> Rational { r(45_359_237, 100_000_000) } // 0.45359237 kg
180    fn g0() -> Rational { r(980_665, 100_000) } // standard gravity 9.80665 m/s²
181    /// US liquid gallon = 231 in³ (exact), in m³.
182    fn us_gallon_m3() -> Rational { cube(inch_m()).mul(&i(231)) }
183    /// Imperial gallon = 4.54609 L (exact), in m³.
184    fn imp_gallon_m3() -> Rational { r(454_609, 100_000_000) }
185
186    // ============================ LENGTH (base: metre) ============================
187    pub fn metre() -> Unit { Unit::linear("m", Dimension::length(), Rational::one()) }
188    pub fn kilometre() -> Unit { Unit::linear("km", Dimension::length(), i(1000)) }
189    pub fn hectometre() -> Unit { Unit::linear("hm", Dimension::length(), i(100)) }
190    pub fn dekametre() -> Unit { Unit::linear("dam", Dimension::length(), i(10)) }
191    pub fn decimetre() -> Unit { Unit::linear("dm", Dimension::length(), r(1, 10)) }
192    pub fn centimetre() -> Unit { Unit::linear("cm", Dimension::length(), r(1, 100)) }
193    pub fn millimetre() -> Unit { Unit::linear("mm", Dimension::length(), r(1, 1000)) }
194    pub fn micrometre() -> Unit { Unit::linear("µm", Dimension::length(), r(1, 1_000_000)) }
195    pub fn nanometre() -> Unit { Unit::linear("nm", Dimension::length(), r(1, 1_000_000_000)) }
196    pub fn angstrom() -> Unit { Unit::linear("Å", Dimension::length(), r(1, 10_000_000_000)) }
197    pub fn inch() -> Unit { Unit::linear("in", Dimension::length(), inch_m()) }
198    pub fn foot() -> Unit { Unit::linear("ft", Dimension::length(), foot_m()) }
199    pub fn yard() -> Unit { Unit::linear("yd", Dimension::length(), r(1143, 1250)) } // 0.9144 m
200    pub fn mile() -> Unit { Unit::linear("mi", Dimension::length(), r(201_168, 125)) } // 1609.344 m
201    pub fn nautical_mile() -> Unit { Unit::linear("nmi", Dimension::length(), i(1852)) }
202    pub fn fathom() -> Unit { Unit::linear("ftm", Dimension::length(), r(1143, 625)) } // 2 yd
203    pub fn furlong() -> Unit { Unit::linear("fur", Dimension::length(), r(25_146, 125)) } // 201.168 m
204    pub fn chain() -> Unit { Unit::linear("ch", Dimension::length(), r(12_573, 625)) } // 20.1168 m
205    pub fn rod() -> Unit { Unit::linear("rod", Dimension::length(), r(12_573, 2500)) } // 5.0292 m
206    pub fn hand() -> Unit { Unit::linear("hand", Dimension::length(), r(127, 1250)) } // 4 in
207    pub fn point() -> Unit { Unit::linear("pt", Dimension::length(), r(127, 360_000)) } // 1/72 in
208    pub fn pica() -> Unit { Unit::linear("pica", Dimension::length(), r(127, 30_000)) } // 1/6 in
209    pub fn thou() -> Unit { Unit::linear("thou", Dimension::length(), r(127, 5_000_000)) } // 1/1000 in
210    pub fn astronomical_unit() -> Unit { Unit::linear("AU", Dimension::length(), i(149_597_870_700)) }
211    pub fn light_year() -> Unit { Unit::linear("ly", Dimension::length(), i(9_460_730_472_580_800)) } // c·Julian yr
212
213    // ============================ MASS (base: kilogram) ============================
214    pub fn kilogram() -> Unit { Unit::linear("kg", Dimension::mass(), Rational::one()) }
215    pub fn gram() -> Unit { Unit::linear("g", Dimension::mass(), r(1, 1000)) }
216    pub fn milligram() -> Unit { Unit::linear("mg", Dimension::mass(), r(1, 1_000_000)) }
217    pub fn microgram() -> Unit { Unit::linear("µg", Dimension::mass(), r(1, 1_000_000_000)) }
218    pub fn tonne() -> Unit { Unit::linear("t", Dimension::mass(), i(1000)) }
219    pub fn pound() -> Unit { Unit::linear("lb", Dimension::mass(), lb_kg()) }
220    pub fn ounce() -> Unit { Unit::linear("oz", Dimension::mass(), lb_kg().div(&i(16)).unwrap()) }
221    pub fn stone() -> Unit { Unit::linear("st", Dimension::mass(), lb_kg().mul(&i(14))) }
222    pub fn short_ton() -> Unit { Unit::linear("ton", Dimension::mass(), lb_kg().mul(&i(2000))) }
223    pub fn long_ton() -> Unit { Unit::linear("LT", Dimension::mass(), lb_kg().mul(&i(2240))) }
224    pub fn hundredweight() -> Unit { Unit::linear("cwt", Dimension::mass(), lb_kg().mul(&i(100))) }
225    pub fn grain() -> Unit { Unit::linear("gr", Dimension::mass(), lb_kg().div(&i(7000)).unwrap()) }
226    pub fn dram() -> Unit { Unit::linear("dr", Dimension::mass(), lb_kg().div(&i(256)).unwrap()) } // oz/16
227    pub fn carat() -> Unit { Unit::linear("ct", Dimension::mass(), r(1, 5000)) } // 0.2 g
228    pub fn troy_ounce() -> Unit { Unit::linear("ozt", Dimension::mass(), r(311_034_768, 10_000_000_000)) } // 31.1034768 g
229    pub fn troy_pound() -> Unit { Unit::linear("lbt", Dimension::mass(), r(311_034_768, 10_000_000_000).mul(&i(12))) }
230
231    // ============================ TIME (base: second) ============================
232    pub fn second() -> Unit { Unit::linear("s", Dimension::time(), Rational::one()) }
233    pub fn millisecond() -> Unit { Unit::linear("ms", Dimension::time(), r(1, 1000)) }
234    pub fn microsecond() -> Unit { Unit::linear("µs", Dimension::time(), r(1, 1_000_000)) }
235    pub fn nanosecond() -> Unit { Unit::linear("ns", Dimension::time(), r(1, 1_000_000_000)) }
236    pub fn minute() -> Unit { Unit::linear("min", Dimension::time(), i(60)) }
237    pub fn hour() -> Unit { Unit::linear("h", Dimension::time(), i(3600)) }
238    pub fn day() -> Unit { Unit::linear("day", Dimension::time(), i(86_400)) }
239    pub fn week() -> Unit { Unit::linear("wk", Dimension::time(), i(604_800)) }
240    pub fn fortnight() -> Unit { Unit::linear("fortnight", Dimension::time(), i(1_209_600)) }
241    pub fn julian_year() -> Unit { Unit::linear("yr", Dimension::time(), i(31_557_600)) } // 365.25 d
242    pub fn decade() -> Unit { Unit::linear("decade", Dimension::time(), i(315_576_000)) }
243    pub fn century() -> Unit { Unit::linear("century", Dimension::time(), i(3_155_760_000)) }
244
245    // ====================== TEMPERATURE (base: kelvin), AFFINE ======================
246    pub fn kelvin() -> Unit { Unit::affine("K", Dimension::temperature(), Rational::one(), Rational::zero()) }
247    pub fn celsius() -> Unit { Unit::affine("°C", Dimension::temperature(), Rational::one(), r(5463, 20)) } // +273.15
248    pub fn fahrenheit() -> Unit { Unit::affine("°F", Dimension::temperature(), r(5, 9), r(45967, 180)) }
249    pub fn rankine() -> Unit { Unit::affine("°R", Dimension::temperature(), r(5, 9), Rational::zero()) } // K = °R·5/9
250    pub fn reaumur() -> Unit { Unit::affine("°Ré", Dimension::temperature(), r(5, 4), r(5463, 20)) } // K = °Ré·5/4 + 273.15
251
252    // ============================ VOLUME (base: m³) ============================
253    pub fn cubic_metre() -> Unit { Unit::linear("m³", Dimension::volume(), Rational::one()) }
254    pub fn litre() -> Unit { Unit::linear("L", Dimension::volume(), r(1, 1000)) }
255    pub fn millilitre() -> Unit { Unit::linear("mL", Dimension::volume(), r(1, 1_000_000)) }
256    pub fn centilitre() -> Unit { Unit::linear("cL", Dimension::volume(), r(1, 100_000)) }
257    pub fn decilitre() -> Unit { Unit::linear("dL", Dimension::volume(), r(1, 10_000)) }
258    pub fn cubic_centimetre() -> Unit { Unit::linear("cc", Dimension::volume(), r(1, 1_000_000)) }
259    pub fn cubic_inch() -> Unit { Unit::linear("in³", Dimension::volume(), cube(inch_m())) }
260    pub fn cubic_foot() -> Unit { Unit::linear("ft³", Dimension::volume(), cube(foot_m())) }
261    // ---- US liquid / COOKING (anchored to the 231-in³ gallon) ----
262    pub fn us_gallon() -> Unit { Unit::linear("gal", Dimension::volume(), us_gallon_m3()) }
263    pub fn us_quart() -> Unit { Unit::linear("qt", Dimension::volume(), us_gallon_m3().div(&i(4)).unwrap()) }
264    pub fn us_pint() -> Unit { Unit::linear("pt", Dimension::volume(), us_gallon_m3().div(&i(8)).unwrap()) }
265    pub fn us_cup() -> Unit { Unit::linear("cup", Dimension::volume(), us_gallon_m3().div(&i(16)).unwrap()) }
266    pub fn us_gill() -> Unit { Unit::linear("gill", Dimension::volume(), us_gallon_m3().div(&i(32)).unwrap()) }
267    pub fn us_fluid_ounce() -> Unit { Unit::linear("fl oz", Dimension::volume(), us_gallon_m3().div(&i(128)).unwrap()) }
268    pub fn tablespoon() -> Unit { Unit::linear("tbsp", Dimension::volume(), us_gallon_m3().div(&i(256)).unwrap()) }
269    pub fn teaspoon() -> Unit { Unit::linear("tsp", Dimension::volume(), us_gallon_m3().div(&i(768)).unwrap()) }
270    pub fn oil_barrel() -> Unit { Unit::linear("bbl", Dimension::volume(), us_gallon_m3().mul(&i(42))) } // 42 US gal
271    // ---- Imperial ----
272    pub fn imperial_gallon() -> Unit { Unit::linear("imp gal", Dimension::volume(), imp_gallon_m3()) }
273    pub fn imperial_pint() -> Unit { Unit::linear("imp pt", Dimension::volume(), imp_gallon_m3().div(&i(8)).unwrap()) }
274    pub fn imperial_fluid_ounce() -> Unit { Unit::linear("imp fl oz", Dimension::volume(), imp_gallon_m3().div(&i(160)).unwrap()) }
275
276    // ============================ AREA (base: m²) ============================
277    pub fn square_metre() -> Unit { Unit::linear("m²", Dimension::area(), Rational::one()) }
278    pub fn square_kilometre() -> Unit { Unit::linear("km²", Dimension::area(), i(1_000_000)) }
279    pub fn square_centimetre() -> Unit { Unit::linear("cm²", Dimension::area(), r(1, 10_000)) }
280    pub fn square_inch() -> Unit { Unit::linear("in²", Dimension::area(), square(inch_m())) }
281    pub fn square_foot() -> Unit { Unit::linear("ft²", Dimension::area(), square(foot_m())) }
282    pub fn square_yard() -> Unit { Unit::linear("yd²", Dimension::area(), square(r(1143, 1250))) }
283    pub fn square_mile() -> Unit { Unit::linear("mi²", Dimension::area(), square(r(201_168, 125))) }
284    pub fn hectare() -> Unit { Unit::linear("ha", Dimension::area(), i(10_000)) }
285    pub fn are() -> Unit { Unit::linear("a", Dimension::area(), i(100)) }
286    pub fn acre() -> Unit { Unit::linear("ac", Dimension::area(), square(foot_m()).mul(&i(43_560))) } // 43560 ft²
287
288    // ============================ SPEED (base: m/s) ============================
289    pub fn metre_per_second() -> Unit { Unit::linear("m/s", Dimension::speed(), Rational::one()) }
290    pub fn kilometre_per_hour() -> Unit { Unit::linear("km/h", Dimension::speed(), r(5, 18)) } // 1000/3600
291    pub fn mile_per_hour() -> Unit { Unit::linear("mph", Dimension::speed(), r(201_168, 125).div(&i(3600)).unwrap()) }
292    pub fn foot_per_second() -> Unit { Unit::linear("ft/s", Dimension::speed(), foot_m()) }
293    pub fn knot() -> Unit { Unit::linear("kn", Dimension::speed(), r(1852, 3600)) } // nmi/h
294
295    // ============================ FREQUENCY (base: 1/s) ============================
296    pub fn hertz() -> Unit { Unit::linear("Hz", Dimension::frequency(), Rational::one()) }
297    pub fn kilohertz() -> Unit { Unit::linear("kHz", Dimension::frequency(), i(1000)) }
298    pub fn megahertz() -> Unit { Unit::linear("MHz", Dimension::frequency(), i(1_000_000)) }
299    pub fn gigahertz() -> Unit { Unit::linear("GHz", Dimension::frequency(), i(1_000_000_000)) }
300    pub fn rpm() -> Unit { Unit::linear("rpm", Dimension::frequency(), r(1, 60)) }
301
302    // ============================ FORCE (base: newton) ============================
303    pub fn newton() -> Unit { Unit::linear("N", Dimension::force(), Rational::one()) }
304    pub fn kilonewton() -> Unit { Unit::linear("kN", Dimension::force(), i(1000)) }
305    pub fn dyne() -> Unit { Unit::linear("dyn", Dimension::force(), r(1, 100_000)) }
306    pub fn kilogram_force() -> Unit { Unit::linear("kgf", Dimension::force(), g0()) }
307    pub fn pound_force() -> Unit { Unit::linear("lbf", Dimension::force(), lb_kg().mul(&g0())) }
308
309    // ============================ ENERGY (base: joule) ============================
310    pub fn joule() -> Unit { Unit::linear("J", Dimension::energy(), Rational::one()) }
311    pub fn kilojoule() -> Unit { Unit::linear("kJ", Dimension::energy(), i(1000)) }
312    pub fn megajoule() -> Unit { Unit::linear("MJ", Dimension::energy(), i(1_000_000)) }
313    pub fn calorie() -> Unit { Unit::linear("cal", Dimension::energy(), r(523, 125)) } // 4.184 J
314    pub fn kilocalorie() -> Unit { Unit::linear("kcal", Dimension::energy(), r(4184, 1)) }
315    pub fn watt_hour() -> Unit { Unit::linear("Wh", Dimension::energy(), i(3600)) }
316    pub fn kilowatt_hour() -> Unit { Unit::linear("kWh", Dimension::energy(), i(3_600_000)) }
317    pub fn erg() -> Unit { Unit::linear("erg", Dimension::energy(), r(1, 10_000_000)) }
318    pub fn electronvolt() -> Unit {
319        // 1.602176634e-19 J = 1602176634 / 10^28 (the 10^28 denominator overflows i64, so build it
320        // from BigInt — exact).
321        use crate::numeric::BigInt;
322        let scale = Rational::new(BigInt::from_i64(1_602_176_634), BigInt::from_i64(10).pow(28))
323            .expect("nonzero denominator");
324        Unit::linear("eV", Dimension::energy(), scale)
325    }
326
327    // ============================ POWER (base: watt) ============================
328    pub fn watt() -> Unit { Unit::linear("W", Dimension::power(), Rational::one()) }
329    pub fn kilowatt() -> Unit { Unit::linear("kW", Dimension::power(), i(1000)) }
330    pub fn megawatt() -> Unit { Unit::linear("MW", Dimension::power(), i(1_000_000)) }
331    pub fn horsepower() -> Unit { Unit::linear("hp", Dimension::power(), lb_kg().mul(&g0()).mul(&foot_m()).mul(&i(550))) } // 550 ft·lbf/s
332
333    // ============================ PRESSURE (base: pascal) ============================
334    pub fn pascal() -> Unit { Unit::linear("Pa", Dimension::pressure(), Rational::one()) }
335    pub fn kilopascal() -> Unit { Unit::linear("kPa", Dimension::pressure(), i(1000)) }
336    pub fn bar() -> Unit { Unit::linear("bar", Dimension::pressure(), i(100_000)) }
337    pub fn millibar() -> Unit { Unit::linear("mbar", Dimension::pressure(), i(100)) }
338    pub fn atmosphere() -> Unit { Unit::linear("atm", Dimension::pressure(), i(101_325)) }
339    pub fn psi() -> Unit { Unit::linear("psi", Dimension::pressure(), lb_kg().mul(&g0()).div(&square(inch_m())).unwrap()) } // lbf/in²
340
341    // ============================ INFORMATION (base: bit) ============================
342    pub fn bit() -> Unit { Unit::linear("bit", Dimension::information(), Rational::one()) }
343    pub fn byte() -> Unit { Unit::linear("B", Dimension::information(), i(8)) }
344    pub fn kilobit() -> Unit { Unit::linear("kbit", Dimension::information(), i(1000)) }
345    pub fn kilobyte() -> Unit { Unit::linear("kB", Dimension::information(), i(8000)) }
346    pub fn megabyte() -> Unit { Unit::linear("MB", Dimension::information(), i(8_000_000)) }
347    pub fn gigabyte() -> Unit { Unit::linear("GB", Dimension::information(), i(8_000_000_000)) }
348    pub fn kibibyte() -> Unit { Unit::linear("KiB", Dimension::information(), i(8 * 1024)) }
349    pub fn mebibyte() -> Unit { Unit::linear("MiB", Dimension::information(), i(8 * 1024 * 1024)) }
350    pub fn gibibyte() -> Unit { Unit::linear("GiB", Dimension::information(), i(8 * 1024 * 1024 * 1024)) }
351
352    // ============================ ANGLE (base: radian) ============================
353    // (Degrees etc. are exact multiples of a turn; radian-to-degree is irrational, so these are
354    // defined against the TURN as the natural rational anchor — full circle = 1 turn.)
355    pub fn turn() -> Unit { Unit::linear("turn", Dimension::angle(), Rational::one()) }
356    pub fn degree() -> Unit { Unit::linear("°", Dimension::angle(), r(1, 360)) }
357    pub fn gradian() -> Unit { Unit::linear("grad", Dimension::angle(), r(1, 400)) }
358    pub fn arcminute() -> Unit { Unit::linear("′", Dimension::angle(), r(1, 21_600)) } // degree/60
359    pub fn arcsecond() -> Unit { Unit::linear("″", Dimension::angle(), r(1, 1_296_000)) } // degree/3600
360
361    // ============================ ELECTRICAL ============================
362    pub fn ampere() -> Unit { Unit::linear("A", Dimension::current(), Rational::one()) }
363    pub fn milliampere() -> Unit { Unit::linear("mA", Dimension::current(), r(1, 1000)) }
364    pub fn coulomb() -> Unit { Unit::linear("C", Dimension::charge(), Rational::one()) }
365    pub fn ampere_hour() -> Unit { Unit::linear("Ah", Dimension::charge(), i(3600)) }
366    pub fn milliampere_hour() -> Unit { Unit::linear("mAh", Dimension::charge(), r(18, 5)) } // 3.6 C
367    pub fn volt() -> Unit { Unit::linear("V", Dimension::voltage(), Rational::one()) }
368    pub fn millivolt() -> Unit { Unit::linear("mV", Dimension::voltage(), r(1, 1000)) }
369    pub fn kilovolt() -> Unit { Unit::linear("kV", Dimension::voltage(), i(1000)) }
370    pub fn ohm() -> Unit { Unit::linear("Ω", Dimension::resistance(), Rational::one()) }
371
372    // ============================ AMOUNT / LUMINOUS ============================
373    pub fn mole() -> Unit { Unit::linear("mol", Dimension::amount(), Rational::one()) }
374    pub fn candela() -> Unit { Unit::linear("cd", Dimension::luminous(), Rational::one()) }
375
376    /// `mantissa · 10^pow10` as an exact rational (for values past i64, e.g. stellar masses).
377    fn e(mantissa: i64, pow10: u32) -> Rational {
378        use crate::numeric::BigInt;
379        Rational::from_bigint(BigInt::from_i64(mantissa).mul(&BigInt::from_i64(10).pow(pow10)))
380    }
381
382    // ============================ COOKING EXTRAS (volume) ============================
383    pub fn stick_of_butter() -> Unit { Unit::linear("stick", Dimension::volume(), us_gallon_m3().div(&i(32)).unwrap()) } // ½ cup = 8 tbsp
384    pub fn dash() -> Unit { Unit::linear("dash", Dimension::volume(), us_gallon_m3().div(&i(6144)).unwrap()) } // ⅛ tsp
385    pub fn pinch() -> Unit { Unit::linear("pinch", Dimension::volume(), us_gallon_m3().div(&i(12288)).unwrap()) } // ¹⁄₁₆ tsp
386    pub fn smidgen() -> Unit { Unit::linear("smidgen", Dimension::volume(), us_gallon_m3().div(&i(24576)).unwrap()) } // ¹⁄₃₂ tsp
387    // US DRY volume (anchored to the 2150.42-in³ bushel).
388    fn us_bushel_m3() -> Rational { cube(inch_m()).mul(&r(215_042, 100)) } // 2150.42 in³
389    pub fn bushel() -> Unit { Unit::linear("bu", Dimension::volume(), us_bushel_m3()) }
390    pub fn peck() -> Unit { Unit::linear("pk", Dimension::volume(), us_bushel_m3().div(&i(4)).unwrap()) }
391    pub fn dry_gallon() -> Unit { Unit::linear("dry gal", Dimension::volume(), us_bushel_m3().div(&i(8)).unwrap()) }
392    pub fn dry_quart() -> Unit { Unit::linear("dry qt", Dimension::volume(), us_bushel_m3().div(&i(32)).unwrap()) }
393    pub fn dry_pint() -> Unit { Unit::linear("dry pt", Dimension::volume(), us_bushel_m3().div(&i(64)).unwrap()) }
394
395    // ============================ NAUTICAL (length) ============================
396    pub fn cable() -> Unit { Unit::linear("cable", Dimension::length(), r(926, 5)) } // ¹⁄₁₀ nmi = 185.2 m
397    pub fn league() -> Unit { Unit::linear("lea", Dimension::length(), r(603_504, 125)) } // 3 statute miles
398    pub fn nautical_league() -> Unit { Unit::linear("nl", Dimension::length(), i(5556)) } // 3 nmi
399
400    // ============================ ASTRONOMICAL ============================
401    pub fn light_second() -> Unit { Unit::linear("ls", Dimension::length(), i(299_792_458)) } // c·1 s
402    pub fn light_minute() -> Unit { Unit::linear("lmin", Dimension::length(), i(299_792_458 * 60)) }
403    pub fn light_hour() -> Unit { Unit::linear("lh", Dimension::length(), i(299_792_458 * 3600)) }
404    pub fn light_day() -> Unit { Unit::linear("ld", Dimension::length(), i(299_792_458 * 86_400)) }
405    pub fn lunar_distance() -> Unit { Unit::linear("LD", Dimension::length(), i(384_399_000)) } // mean Earth–Moon
406    pub fn solar_radius() -> Unit { Unit::linear("R☉", Dimension::length(), i(696_340_000)) }
407    pub fn earth_radius() -> Unit { Unit::linear("R⊕", Dimension::length(), i(6_371_000)) } // mean
408    // Astronomical MASSES (measured values, exact as the conventional decimals given).
409    pub fn solar_mass() -> Unit { Unit::linear("M☉", Dimension::mass(), e(198_892, 25)) } // 1.98892e30 kg
410    pub fn earth_mass() -> Unit { Unit::linear("M⊕", Dimension::mass(), e(59_722, 20)) } // 5.9722e24 kg
411    pub fn jupiter_mass() -> Unit { Unit::linear("M♃", Dimension::mass(), e(18_982, 23)) } // 1.8982e27 kg
412
413    // ============================ RADIATION ============================
414    pub fn gray() -> Unit { Unit::linear("Gy", Dimension::absorbed_dose(), Rational::one()) } // J/kg
415    pub fn sievert() -> Unit { Unit::linear("Sv", Dimension::absorbed_dose(), Rational::one()) } // J/kg (equiv dose)
416    pub fn rad_unit() -> Unit { Unit::linear("rad", Dimension::absorbed_dose(), r(1, 100)) } // 0.01 Gy
417    pub fn rem() -> Unit { Unit::linear("rem", Dimension::absorbed_dose(), r(1, 100)) } // 0.01 Sv
418    pub fn becquerel() -> Unit { Unit::linear("Bq", Dimension::radioactivity(), Rational::one()) } // 1/s
419    pub fn curie() -> Unit { Unit::linear("Ci", Dimension::radioactivity(), i(37_000_000_000)) } // 3.7e10 Bq
420    pub fn roentgen() -> Unit { Unit::linear("R", Dimension::exposure(), r(129, 500_000)) } // 2.58e-4 C/kg
421    pub fn katal() -> Unit { Unit::linear("kat", Dimension::catalytic_activity(), Rational::one()) } // mol/s
422
423    // ============================ ILLUMINATION (photometry) ============================
424    pub fn lumen() -> Unit { Unit::linear("lm", Dimension::luminous_flux(), Rational::one()) } // cd·sr
425    pub fn lux() -> Unit { Unit::linear("lx", Dimension::illuminance(), Rational::one()) } // lm/m²
426    pub fn phot() -> Unit { Unit::linear("ph", Dimension::illuminance(), i(10_000)) } // lm/cm²
427    pub fn foot_candle() -> Unit { Unit::linear("fc", Dimension::illuminance(), square(r(1250, 381))) } // lm/ft² = 1/0.3048² lx
428    pub fn nit() -> Unit { Unit::linear("nit", Dimension::luminance(), Rational::one()) } // cd/m²
429
430    // ============================ DATA RATE (base: bit/s) ============================
431    pub fn bit_per_second() -> Unit { Unit::linear("bit/s", Dimension::data_rate(), Rational::one()) }
432    pub fn kilobit_per_second() -> Unit { Unit::linear("kbps", Dimension::data_rate(), i(1000)) }
433    pub fn megabit_per_second() -> Unit { Unit::linear("Mbps", Dimension::data_rate(), i(1_000_000)) }
434    pub fn gigabit_per_second() -> Unit { Unit::linear("Gbps", Dimension::data_rate(), i(1_000_000_000)) }
435    pub fn byte_per_second() -> Unit { Unit::linear("B/s", Dimension::data_rate(), i(8)) }
436    pub fn megabyte_per_second() -> Unit { Unit::linear("MB/s", Dimension::data_rate(), i(8_000_000)) }
437
438    // ============================ VOLUMETRIC FLOW (base: m³/s) ============================
439    pub fn cubic_metre_per_second() -> Unit { Unit::linear("m³/s", Dimension::volumetric_flow(), Rational::one()) }
440    pub fn litre_per_second() -> Unit { Unit::linear("L/s", Dimension::volumetric_flow(), r(1, 1000)) }
441    pub fn litre_per_minute() -> Unit { Unit::linear("L/min", Dimension::volumetric_flow(), r(1, 60_000)) }
442    pub fn gallon_per_minute() -> Unit { Unit::linear("gpm", Dimension::volumetric_flow(), us_gallon_m3().div(&i(60)).unwrap()) }
443    pub fn cubic_foot_per_minute() -> Unit { Unit::linear("cfm", Dimension::volumetric_flow(), cube(foot_m()).div(&i(60)).unwrap()) }
444
445    // ============================ CONCENTRATION (base: mol/m³) ============================
446    pub fn molar() -> Unit { Unit::linear("M", Dimension::molar_concentration(), i(1000)) } // mol/L
447    pub fn millimolar() -> Unit { Unit::linear("mM", Dimension::molar_concentration(), Rational::one()) }
448    pub fn micromolar() -> Unit { Unit::linear("µM", Dimension::molar_concentration(), r(1, 1000)) }
449    pub fn molal() -> Unit { Unit::linear("m", Dimension::molality(), Rational::one()) } // mol/kg
450
451    // ============================ VISCOSITY ============================
452    pub fn pascal_second() -> Unit { Unit::linear("Pa·s", Dimension::dynamic_viscosity(), Rational::one()) }
453    pub fn poise() -> Unit { Unit::linear("P", Dimension::dynamic_viscosity(), r(1, 10)) }
454    pub fn centipoise() -> Unit { Unit::linear("cP", Dimension::dynamic_viscosity(), r(1, 1000)) }
455    pub fn square_metre_per_second() -> Unit { Unit::linear("m²/s", Dimension::kinematic_viscosity(), Rational::one()) }
456    pub fn stokes() -> Unit { Unit::linear("St", Dimension::kinematic_viscosity(), r(1, 10_000)) }
457    pub fn centistokes() -> Unit { Unit::linear("cSt", Dimension::kinematic_viscosity(), r(1, 1_000_000)) }
458
459    // ============================ FUEL ECONOMY (base: m/m³ = m⁻²) ============================
460    pub fn mile_per_gallon() -> Unit { Unit::linear("mpg", Dimension::fuel_economy(), r(201_168, 125).div(&us_gallon_m3()).unwrap()) }
461    pub fn km_per_litre() -> Unit { Unit::linear("km/L", Dimension::fuel_economy(), i(1000).div(&r(1, 1000)).unwrap()) } // 10^6 m⁻²
462
463    // ============================ TORQUE (shares the energy dimension M·L²·T⁻²) ============================
464    pub fn newton_metre() -> Unit { Unit::linear("N·m", Dimension::energy(), Rational::one()) }
465    pub fn pound_foot() -> Unit { Unit::linear("lb·ft", Dimension::energy(), lb_kg().mul(&g0()).mul(&foot_m())) }
466
467    // ============================ EXTRA SI PREFIXES (extend existing dimensions) ============================
468    pub fn megametre() -> Unit { Unit::linear("Mm", Dimension::length(), i(1_000_000)) }
469    pub fn gigametre() -> Unit { Unit::linear("Gm", Dimension::length(), i(1_000_000_000)) }
470    pub fn picometre() -> Unit { Unit::linear("pm", Dimension::length(), r(1, 1_000_000_000_000)) }
471    pub fn femtometre() -> Unit { Unit::linear("fm", Dimension::length(), r(1, 1_000_000_000_000_000)) }
472    pub fn nanogram() -> Unit { Unit::linear("ng", Dimension::mass(), r(1, 1_000_000_000_000)) }
473    pub fn picosecond() -> Unit { Unit::linear("ps", Dimension::time(), r(1, 1_000_000_000_000)) }
474    pub fn gigawatt() -> Unit { Unit::linear("GW", Dimension::power(), i(1_000_000_000)) }
475    pub fn terawatt() -> Unit { Unit::linear("TW", Dimension::power(), i(1_000_000_000_000)) }
476    pub fn gigajoule() -> Unit { Unit::linear("GJ", Dimension::energy(), i(1_000_000_000)) }
477    pub fn terajoule() -> Unit { Unit::linear("TJ", Dimension::energy(), i(1_000_000_000_000)) }
478    pub fn terabit() -> Unit { Unit::linear("Tbit", Dimension::information(), i(1_000_000_000_000)) }
479    pub fn terabyte() -> Unit { Unit::linear("TB", Dimension::information(), i(8_000_000_000_000)) }
480    pub fn petabyte() -> Unit { Unit::linear("PB", Dimension::information(), i(8_000_000_000_000_000)) }
481
482    // ============================ ELECTROMAGNETISM ============================
483    pub fn siemens() -> Unit { Unit::linear("S", Dimension::conductance(), Rational::one()) }
484    pub fn millisiemens() -> Unit { Unit::linear("mS", Dimension::conductance(), r(1, 1000)) }
485    pub fn farad() -> Unit { Unit::linear("F", Dimension::capacitance(), Rational::one()) }
486    pub fn microfarad() -> Unit { Unit::linear("µF", Dimension::capacitance(), r(1, 1_000_000)) }
487    pub fn nanofarad() -> Unit { Unit::linear("nF", Dimension::capacitance(), r(1, 1_000_000_000)) }
488    pub fn picofarad() -> Unit { Unit::linear("pF", Dimension::capacitance(), r(1, 1_000_000_000_000)) }
489    pub fn weber() -> Unit { Unit::linear("Wb", Dimension::magnetic_flux(), Rational::one()) }
490    pub fn maxwell() -> Unit { Unit::linear("Mx", Dimension::magnetic_flux(), r(1, 100_000_000)) } // 1e-8 Wb
491    pub fn henry() -> Unit { Unit::linear("H", Dimension::inductance(), Rational::one()) }
492    pub fn millihenry() -> Unit { Unit::linear("mH", Dimension::inductance(), r(1, 1000)) }
493    pub fn microhenry() -> Unit { Unit::linear("µH", Dimension::inductance(), r(1, 1_000_000)) }
494    pub fn tesla() -> Unit { Unit::linear("T", Dimension::magnetic_flux_density(), Rational::one()) }
495    pub fn millitesla() -> Unit { Unit::linear("mT", Dimension::magnetic_flux_density(), r(1, 1000)) }
496    pub fn gauss() -> Unit { Unit::linear("G", Dimension::magnetic_flux_density(), r(1, 10_000)) } // 1e-4 T
497
498    // ============================ SURFACE TENSION (base: N/m) ============================
499    pub fn newton_per_metre() -> Unit { Unit::linear("N/m", Dimension::surface_tension(), Rational::one()) }
500    pub fn dyne_per_centimetre() -> Unit { Unit::linear("dyn/cm", Dimension::surface_tension(), r(1, 1000)) } // 1e-3 N/m
501
502    // ============================ COUNTING & RATIO (dimensionless) ============================
503    // The base "unit" is a bare count of one; everything here is a pure dimensionless scale factor.
504    pub fn each() -> Unit { Unit::linear("ea", Dimension::DIMENSIONLESS, Rational::one()) }
505    pub fn pair() -> Unit { Unit::linear("pair", Dimension::DIMENSIONLESS, i(2)) }
506    pub fn dozen() -> Unit { Unit::linear("dz", Dimension::DIMENSIONLESS, i(12)) }
507    pub fn baker_dozen() -> Unit { Unit::linear("baker's dz", Dimension::DIMENSIONLESS, i(13)) }
508    pub fn score() -> Unit { Unit::linear("score", Dimension::DIMENSIONLESS, i(20)) }
509    pub fn gross() -> Unit { Unit::linear("gross", Dimension::DIMENSIONLESS, i(144)) }
510    pub fn great_gross() -> Unit { Unit::linear("great gross", Dimension::DIMENSIONLESS, i(1728)) }
511    pub fn ream() -> Unit { Unit::linear("ream", Dimension::DIMENSIONLESS, i(500)) }
512    pub fn percent() -> Unit { Unit::linear("%", Dimension::DIMENSIONLESS, r(1, 100)) }
513    pub fn permille() -> Unit { Unit::linear("‰", Dimension::DIMENSIONLESS, r(1, 1000)) }
514    pub fn ppm() -> Unit { Unit::linear("ppm", Dimension::DIMENSIONLESS, r(1, 1_000_000)) }
515    pub fn ppb() -> Unit { Unit::linear("ppb", Dimension::DIMENSIONLESS, r(1, 1_000_000_000)) }
516    pub fn basis_point() -> Unit { Unit::linear("bp", Dimension::DIMENSIONLESS, r(1, 10_000)) }
517
518    /// Look up a unit by an English name, plural, or symbol (case-insensitive) — the surface-syntax
519    /// bridge so `quantity(2, "inch")` / `... in "feet"` resolve to a [`Unit`]. This is the growable
520    /// catalog's name index; unknown names return `None` (the front-end turns that into a clean
521    /// error). Spelling variants and both singular/plural map to the same unit.
522    pub fn by_name(name: &str) -> Option<Unit> {
523        let n = name.trim().to_ascii_lowercase();
524        let u = match n.as_str() {
525            // Length.
526            "meter" | "metre" | "meters" | "metres" | "m" => metre(),
527            "kilometer" | "kilometre" | "kilometers" | "kilometres" | "km" => kilometre(),
528            "centimeter" | "centimetre" | "centimeters" | "centimetres" | "cm" => centimetre(),
529            "millimeter" | "millimetre" | "millimeters" | "millimetres" | "mm" => millimetre(),
530            "micrometer" | "micrometre" | "micron" | "microns" | "µm" | "um" => micrometre(),
531            "nanometer" | "nanometre" | "nanometers" | "nanometres" | "nm" => nanometre(),
532            "inch" | "inches" | "in" => inch(),
533            "foot" | "feet" | "ft" => foot(),
534            "yard" | "yards" | "yd" => yard(),
535            "mile" | "miles" | "mi" => mile(),
536            "nautical mile" | "nautical miles" | "nmi" => nautical_mile(),
537            "angstrom" | "angstroms" | "ångström" | "å" => angstrom(),
538            "light year" | "light years" | "lightyear" | "ly" => light_year(),
539            "astronomical unit" | "astronomical units" | "au" => astronomical_unit(),
540            // Mass.
541            "kilogram" | "kilograms" | "kg" => kilogram(),
542            "gram" | "grams" | "g" => gram(),
543            "milligram" | "milligrams" | "mg" => milligram(),
544            "microgram" | "micrograms" | "µg" | "ug" => microgram(),
545            "tonne" | "tonnes" | "metric ton" | "metric tons" | "t" => tonne(),
546            "pound" | "pounds" | "lb" | "lbs" => pound(),
547            "ounce" | "ounces" | "oz" => ounce(),
548            "stone" | "stones" | "st" => stone(),
549            "carat" | "carats" | "ct" => carat(),
550            // Time.
551            "second" | "seconds" | "sec" | "secs" | "s" => second(),
552            "millisecond" | "milliseconds" | "ms" => millisecond(),
553            "microsecond" | "microseconds" | "µs" | "us" => microsecond(),
554            "nanosecond" | "nanoseconds" | "ns" => nanosecond(),
555            "minute" | "minutes" | "min" | "mins" => minute(),
556            "hour" | "hours" | "hr" | "hrs" | "h" => hour(),
557            "day" | "days" => day(),
558            "week" | "weeks" | "wk" => week(),
559            "year" | "years" | "yr" | "yrs" => julian_year(),
560            // Temperature (affine).
561            "kelvin" | "k" => kelvin(),
562            "celsius" | "centigrade" | "°c" | "c" => celsius(),
563            "fahrenheit" | "°f" | "f" => fahrenheit(),
564            "rankine" | "°r" => rankine(),
565            "reaumur" | "réaumur" | "°ré" => reaumur(),
566            // Volume (incl. cooking).
567            "liter" | "litre" | "liters" | "litres" | "l" => litre(),
568            "milliliter" | "millilitre" | "milliliters" | "millilitres" | "ml" => millilitre(),
569            "cubic meter" | "cubic metre" | "m3" | "m³" => cubic_metre(),
570            "gallon" | "gallons" | "gal" => us_gallon(),
571            "quart" | "quarts" | "qt" => us_quart(),
572            "pint" | "pints" | "pt" => us_pint(),
573            "cup" | "cups" => us_cup(),
574            "tablespoon" | "tablespoons" | "tbsp" => tablespoon(),
575            "teaspoon" | "teaspoons" | "tsp" => teaspoon(),
576            "fluid ounce" | "fluid ounces" | "fl oz" => us_fluid_ounce(),
577            // Area.
578            "square meter" | "square metre" | "square meters" | "square metres" | "m2" => square_metre(),
579            "square foot" | "square feet" | "sq ft" => square_foot(),
580            "square inch" | "square inches" | "sq in" => square_inch(),
581            "hectare" | "hectares" | "ha" => hectare(),
582            "acre" | "acres" => acre(),
583            // Speed.
584            "meter per second" | "metre per second" | "meters per second" | "m/s" => metre_per_second(),
585            "kilometer per hour" | "kilometre per hour" | "km/h" | "kph" => kilometre_per_hour(),
586            "mile per hour" | "miles per hour" | "mph" => mile_per_hour(),
587            "knot" | "knots" | "kn" => knot(),
588            // Frequency.
589            "hertz" | "hz" => hertz(),
590            "kilohertz" | "khz" => kilohertz(),
591            "megahertz" | "mhz" => megahertz(),
592            "gigahertz" | "ghz" => gigahertz(),
593            // Energy.
594            "joule" | "joules" | "j" => joule(),
595            "kilojoule" | "kilojoules" | "kj" => kilojoule(),
596            "calorie" | "calories" | "cal" => calorie(),
597            "kilocalorie" | "kilocalories" | "kcal" => kilocalorie(),
598            "watt hour" | "watt hours" | "wh" => watt_hour(),
599            "kilowatt hour" | "kilowatt hours" | "kwh" => kilowatt_hour(),
600            "electronvolt" | "electronvolts" | "ev" => electronvolt(),
601            // Power.
602            "watt" | "watts" | "w" => watt(),
603            "kilowatt" | "kilowatts" | "kw" => kilowatt(),
604            "megawatt" | "megawatts" | "mw" => megawatt(),
605            "horsepower" | "hp" => horsepower(),
606            // Pressure.
607            "pascal" | "pascals" | "pa" => pascal(),
608            "kilopascal" | "kilopascals" | "kpa" => kilopascal(),
609            "bar" | "bars" => bar(),
610            "atmosphere" | "atmospheres" | "atm" => atmosphere(),
611            "psi" => psi(),
612            // Information.
613            "bit" | "bits" => bit(),
614            "byte" | "bytes" => byte(),
615            "kilobyte" | "kilobytes" | "kb" => kilobyte(),
616            "megabyte" | "megabytes" | "mb" => megabyte(),
617            "gigabyte" | "gigabytes" | "gb" => gigabyte(),
618            // Angle (radian is irrational vs. these and is intentionally absent, like parsec/mach).
619            "turn" | "turns" => turn(),
620            "degree" | "degrees" | "deg" | "°" => degree(),
621            "gradian" | "gradians" | "grad" => gradian(),
622            "arcminute" | "arcminutes" => arcminute(),
623            "arcsecond" | "arcseconds" => arcsecond(),
624            // Radiation & catalysis.
625            "gray" | "grays" | "gy" => gray(),
626            "sievert" | "sieverts" | "sv" => sievert(),
627            "rad" | "rads" => rad_unit(),
628            "rem" | "rems" => rem(),
629            "becquerel" | "becquerels" | "bq" => becquerel(),
630            "curie" | "curies" | "ci" => curie(),
631            "roentgen" | "roentgens" => roentgen(),
632            "katal" | "katals" | "kat" => katal(),
633            // Photometry.
634            "lumen" | "lumens" | "lm" => lumen(),
635            "lux" | "lx" => lux(),
636            "phot" | "phots" => phot(),
637            "foot candle" | "foot-candle" | "footcandle" | "fc" => foot_candle(),
638            "nit" | "nits" => nit(),
639            // Electromagnetism.
640            "siemens" => siemens(),
641            "farad" | "farads" => farad(),
642            "microfarad" | "microfarads" | "µf" | "uf" => microfarad(),
643            "nanofarad" | "nanofarads" | "nf" => nanofarad(),
644            "picofarad" | "picofarads" | "pf" => picofarad(),
645            "weber" | "webers" | "wb" => weber(),
646            "maxwell" | "maxwells" | "mx" => maxwell(),
647            "henry" | "henries" => henry(),
648            "tesla" | "teslas" => tesla(),
649            "gauss" => gauss(),
650            // Viscosity.
651            "pascal second" | "pascal-second" | "pa·s" | "pas" => pascal_second(),
652            "poise" => poise(),
653            "centipoise" | "cp" => centipoise(),
654            "stokes" | "stoke" => stokes(),
655            "centistokes" | "cst" => centistokes(),
656            // Flow & concentration.
657            "gallon per minute" | "gpm" => gallon_per_minute(),
658            "cubic foot per minute" | "cfm" => cubic_foot_per_minute(),
659            "litre per second" | "liter per second" | "l/s" => litre_per_second(),
660            "molar" => molar(),
661            "millimolar" | "mm/l" => millimolar(),
662            // Data rate.
663            "bit per second" | "bits per second" | "bps" => bit_per_second(),
664            "kilobit per second" | "kbps" => kilobit_per_second(),
665            "megabit per second" | "mbps" => megabit_per_second(),
666            "gigabit per second" | "gbps" => gigabit_per_second(),
667            "byte per second" | "bytes per second" => byte_per_second(),
668            // Surface tension.
669            "newton per meter" | "newton per metre" | "n/m" => newton_per_metre(),
670            // Cooking extras.
671            "stick" | "sticks" | "stick of butter" => stick_of_butter(),
672            "dash" | "dashes" => dash(),
673            "pinch" | "pinches" => pinch(),
674            "smidgen" | "smidgens" => smidgen(),
675            // Nautical.
676            "fathom" | "fathoms" => fathom(),
677            "cable" | "cables" => cable(),
678            "league" | "leagues" => league(),
679            // Astronomical.
680            "light second" | "light-second" | "light seconds" => light_second(),
681            "light minute" | "light-minute" | "light minutes" => light_minute(),
682            // (parsec is irrational vs. metre — intentionally absent, like mach.)
683            // Counting & ratio (dimensionless).
684            "each" | "ea" | "count" => each(),
685            "pair" | "pairs" => pair(),
686            "dozen" | "dozens" | "dz" => dozen(),
687            "score" | "scores" => score(),
688            "gross" => gross(),
689            "ream" | "reams" => ream(),
690            "percent" | "percents" | "%" => percent(),
691            "permille" | "per mille" | "‰" => permille(),
692            "ppm" => ppm(),
693            "ppb" => ppb(),
694            "basis point" | "basis points" => basis_point(),
695            // Extra SI prefixes (the ones beyond the common ladder above).
696            "megameter" | "megametre" => megametre(),
697            "gigameter" | "gigametre" => gigametre(),
698            "picometer" | "picometre" | "pm" => picometre(),
699            "nanogram" | "nanograms" => nanogram(),
700            "gigawatt" | "gigawatts" | "gw" => gigawatt(),
701            "terawatt" | "terawatts" | "tw" => terawatt(),
702            "terabyte" | "terabytes" | "tb" => terabyte(),
703            "petabyte" | "petabytes" | "pb" => petabyte(),
704            _ => return None,
705        };
706        Some(u)
707    }
708}
709
710/// Physical constants as first-class [`Quantity`] values — each carries its dimension, so they
711/// compose with units under the same dimensional algebra (`E = m·c²` type-checks, and `c` can be
712/// asked for `in kilometres_per_hour`). The post-2019-SI defining constants are EXACT rationals
713/// (the SI fixes their numeric values); measured constants (G) use their CODATA value.
714pub mod constants {
715    use super::Quantity;
716    use crate::dimension::Dimension;
717    use crate::numeric::{BigInt, Rational};
718
719    /// `mantissa · 10^pow10` exactly (for large positive magnitudes like Avogadro's number).
720    fn ep(mantissa: i64, pow10: u32) -> Rational {
721        Rational::from_bigint(BigInt::from_i64(mantissa).mul(&BigInt::from_i64(10).pow(pow10)))
722    }
723    /// `mantissa · 10^(−pow10)` exactly (for tiny magnitudes like Planck's constant).
724    fn en(mantissa: i64, pow10: u32) -> Rational {
725        Rational::new(BigInt::from_i64(mantissa), BigInt::from_i64(10).pow(pow10)).unwrap()
726    }
727    fn r(n: i64, d: i64) -> Rational {
728        Rational::new(BigInt::from_i64(n), BigInt::from_i64(d)).unwrap()
729    }
730
731    /// The action dimension (energy × time = M·L²·T⁻¹), used by the Planck constant.
732    fn action() -> Dimension { Dimension::energy().mul(Dimension::time()) }
733
734    /// Speed of light in vacuum, `c` = 299 792 458 m/s (exact).
735    pub fn speed_of_light() -> Quantity { Quantity::si(ep(299_792_458, 0), Dimension::speed()) }
736    /// Planck constant, `h` = 6.626 070 15 × 10⁻³⁴ J·s (exact).
737    pub fn planck_constant() -> Quantity { Quantity::si(en(662_607_015, 42), action()) }
738    /// Elementary charge, `e` = 1.602 176 634 × 10⁻¹⁹ C (exact).
739    pub fn elementary_charge() -> Quantity { Quantity::si(en(1_602_176_634, 28), Dimension::charge()) }
740    /// Boltzmann constant, `k_B` = 1.380 649 × 10⁻²³ J/K (exact).
741    pub fn boltzmann_constant() -> Quantity {
742        Quantity::si(en(1_380_649, 29), Dimension::energy().div(Dimension::temperature()))
743    }
744    /// Avogadro constant, `N_A` = 6.022 140 76 × 10²³ mol⁻¹ (exact).
745    pub fn avogadro_constant() -> Quantity {
746        Quantity::si(ep(602_214_076, 15), Dimension::amount().recip())
747    }
748    /// Molar gas constant, `R = N_A · k_B` = 8.314 462 618… J/(mol·K) (exact, a product of exacts).
749    pub fn molar_gas_constant() -> Quantity {
750        avogadro_constant().mul(&boltzmann_constant())
751    }
752    /// Newtonian gravitational constant, `G` = 6.674 30 × 10⁻¹¹ m³·kg⁻¹·s⁻² (CODATA measured value).
753    pub fn gravitational_constant() -> Quantity {
754        let dim = Dimension::volume().div(Dimension::mass()).div(Dimension::time().powi(2));
755        Quantity::si(en(667_430, 16), dim)
756    }
757    /// Standard gravity, `g₀` = 9.806 65 m/s² (exact by definition).
758    pub fn standard_gravity() -> Quantity { Quantity::si(r(980_665, 100_000), Dimension::acceleration()) }
759    /// Standard atmosphere, `atm` = 101 325 Pa (exact by definition).
760    pub fn standard_atmosphere() -> Quantity { Quantity::si(ep(101_325, 0), Dimension::pressure()) }
761}
762
763/// The **light-travel time** across a distance — `distance / c`, exact. Returns a Time quantity, or
764/// `None` if `distance` is not a length (the dimensional guard). This is the core of "universal /
765/// space-travel time": the delay before an event at distance `d` is observed elsewhere (Earth↔Sun
766/// ≈ 499 s, Earth↔Mars ≈ 3–22 min). The result rides the exact rational tower, so it never drifts.
767pub fn light_travel_time(distance: &Quantity) -> Option<Quantity> {
768    if distance.dimension() != Dimension::length() {
769        return None;
770    }
771    distance.div(&constants::speed_of_light())
772}
773
774/// Convert a **Time**-dimensioned quantity to a whole number of nanoseconds (rounded to the nearest),
775/// the bridge from the exact `Quantity` world to the instant model. `None` if `q` is not a time, or
776/// the result does not fit `i64`.
777pub fn time_quantity_to_nanos(q: &Quantity) -> Option<i64> {
778    if q.dimension() != Dimension::time() {
779        return None;
780    }
781    // magnitude_si is in seconds; ×10⁹ → nanoseconds (still exact), rounded to the nearest whole.
782    q.magnitude_si().mul(&Rational::from_i64(1_000_000_000)).round().to_i64()
783}
784
785/// The instant (nanoseconds since the epoch) at which an event happening at `event_nanos` is
786/// **observed** by someone a distance away — `event + distance/c`. The relativistic light-delay the
787/// space-travel time model is built on. `None` if `distance` is not a length or the result overflows.
788pub fn observed_arrival_nanos(event_nanos: i64, distance: &Quantity) -> Option<i64> {
789    let delay = time_quantity_to_nanos(&light_travel_time(distance)?)?;
790    event_nanos.checked_add(delay)
791}
792
793/// A point in 3-D space, coordinates in **metres** (an inertial frame's axes). Euclidean distance
794/// is transcendental (a square root), so positions are `f64` — the one place the time tower leaves
795/// the exact-rational world, because the geometry of spacetime demands it.
796#[derive(Clone, Copy, Debug, PartialEq)]
797pub struct Position {
798    pub x: f64,
799    pub y: f64,
800    pub z: f64,
801}
802
803impl Position {
804    /// The straight-line distance to another point, in metres.
805    pub fn distance_to(self, other: Position) -> f64 {
806        let (dx, dy, dz) = (self.x - other.x, self.y - other.y, self.z - other.z);
807        (dx * dx + dy * dy + dz * dz).sqrt()
808    }
809}
810
811/// A **space-aware timestamp** — *when* (a SmoothUTC instant, nanoseconds) AND *where* (a position
812/// in space). This is the heart of the light-cone model: two events can only be causally ordered if
813/// their time separation is at least the light-travel time between their *positions*. Closer in time
814/// than that and they are **spacelike-separated** — genuinely concurrent, no signal could connect
815/// them, which is exactly where CRDTs (conflict-free, order-free merge) are the right tool.
816#[derive(Clone, Copy, Debug, PartialEq)]
817pub struct SpacetimeStamp {
818    pub instant_nanos: i64,
819    pub position: Position,
820}
821
822/// How two [`SpacetimeStamp`]s relate causally. `Before`: `self` is in the past light cone of the
823/// other (a signal from `self` could reach it). `After`: the reverse. `Concurrent`: spacelike —
824/// neither can have affected the other.
825#[derive(Clone, Copy, Debug, PartialEq, Eq)]
826pub enum CausalRelation {
827    Before,
828    After,
829    Concurrent,
830}
831
832impl SpacetimeStamp {
833    /// The light-travel time (nanoseconds) between the two events' positions — the minimum time
834    /// separation for a causal link. Symmetric.
835    pub fn light_separation_nanos(self, other: SpacetimeStamp) -> i64 {
836        let metres = self.position.distance_to(other.position);
837        (metres / 299_792_458.0 * 1e9).round() as i64
838    }
839
840    /// The causal relation to `other`, decided by the light cone: causally ordered only when the
841    /// time gap covers the spatial (light) gap; otherwise spacelike-concurrent.
842    pub fn causal_relation(self, other: SpacetimeStamp) -> CausalRelation {
843        // i128 so a far-apart pair of i64 instants can't overflow the difference.
844        let dt = other.instant_nanos as i128 - self.instant_nanos as i128;
845        let light = self.light_separation_nanos(other) as i128;
846        if dt >= light {
847            CausalRelation::Before // `other` is in self's forward light cone
848        } else if -dt >= light {
849            CausalRelation::After // self is in other's forward light cone
850        } else {
851            CausalRelation::Concurrent // spacelike — |dt| < light, no signal connects them
852        }
853    }
854
855    /// The instant this event is **observed** from `observer` (its own time + the light delay across
856    /// the distance) — the space-aware generalisation of [`observed_arrival_nanos`].
857    pub fn observed_at_nanos(self, observer: Position) -> i64 {
858        let other = SpacetimeStamp { instant_nanos: self.instant_nanos, position: observer };
859        self.instant_nanos + self.light_separation_nanos(other)
860    }
861}
862
863/// The special-relativistic **Lorentz factor** `γ = 1/√(1 − β²)` for a speed `β` as a fraction of
864/// `c`. `None` for `|β| ≥ 1` (or NaN) — nothing reaches or exceeds light speed. Inherently a float
865/// (relativity is transcendental), unlike the exact-rational quantity arithmetic.
866pub fn lorentz_factor(beta: f64) -> Option<f64> {
867    // `!(< 1.0)` also rejects NaN.
868    if !(beta.abs() < 1.0) {
869        return None;
870    }
871    Some(1.0 / (1.0 - beta * beta).sqrt())
872}
873
874/// The **proper time** (in seconds) a clock experiences over `coordinate_seconds` of coordinate
875/// time while moving at speed `β` (fraction of `c`): `coordinate / γ`. A moving clock ticks slow —
876/// this is the space-traveller's "time attenuation". `None` for `|β| ≥ 1`.
877pub fn proper_time_seconds(coordinate_seconds: f64, beta: f64) -> Option<f64> {
878    Some(coordinate_seconds / lorentz_factor(beta)?)
879}
880
881/// The time-dilation factor `γ` for a velocity given as a **speed [`Quantity`]** (β derived as
882/// `v/c`). `None` if `velocity` is not a speed or is ≥ `c`.
883pub fn time_dilation_factor(velocity: &Quantity) -> Option<f64> {
884    if velocity.dimension() != Dimension::speed() {
885        return None;
886    }
887    let c = constants::speed_of_light();
888    let beta = velocity.magnitude_si().to_f64() / c.magnitude_si().to_f64();
889    lorentz_factor(beta)
890}
891
892#[cfg(test)]
893mod tests {
894    use super::units::*;
895    use super::*;
896
897    fn r(n: i64, d: i64) -> Rational {
898        Rational::from_ratio_i64(n, d).unwrap()
899    }
900    fn i(n: i64) -> Rational {
901        Rational::from_i64(n)
902    }
903
904    /// THE GOLDEN PROOF: `2 inches + 5 centimetres in feet = 42/127`, exactly — the value that
905    /// motivated this whole type system. Also `= 63/625 m`. No floating-point anywhere.
906    #[test]
907    fn golden_two_inches_plus_five_cm_in_feet_is_exactly_42_over_127() {
908        let a = Quantity::of(i(2), &inch());
909        let b = Quantity::of(i(5), &centimetre());
910        let sum = a.add(&b).expect("both are Length");
911        assert_eq!(sum.dimension(), Dimension::length());
912        assert_eq!(sum.in_unit(&metre()).unwrap(), r(63, 625), "= 63/625 m exactly");
913        assert_eq!(sum.in_unit(&foot()).unwrap(), r(42, 127), "= 42/127 ft exactly");
914        // The lossy float answer a JSON-number language would give is NOT 42/127.
915        assert_ne!((42.0_f64 / 127.0) as f32 as f64, 42.0 / 127.0);
916    }
917
918    #[test]
919    fn exact_unit_conversions_within_a_dimension() {
920        assert_eq!(Quantity::of(i(1), &mile()).in_unit(&foot()).unwrap(), i(5280));
921        assert_eq!(Quantity::of(i(1), &foot()).in_unit(&inch()).unwrap(), i(12));
922        assert_eq!(Quantity::of(i(1), &yard()).in_unit(&foot()).unwrap(), i(3));
923        assert_eq!(Quantity::of(i(1), &kilometre()).in_unit(&metre()).unwrap(), i(1000));
924        assert_eq!(Quantity::of(i(1), &kilogram()).in_unit(&gram()).unwrap(), i(1000));
925        assert_eq!(Quantity::of(i(1), &pound()).in_unit(&ounce()).unwrap(), i(16));
926        assert_eq!(Quantity::of(i(1), &hour()).in_unit(&minute()).unwrap(), i(60));
927        // 1 pound = 453.59237 g exactly.
928        assert_eq!(Quantity::of(i(1), &pound()).in_unit(&gram()).unwrap(), r(45_359_237, 100_000));
929    }
930
931    #[test]
932    fn affine_temperature_conversion_is_offset_aware_and_exact() {
933        // 0 °C = 273.15 K; 20 °C = 68 °F; 100 °C = 212 °F; −40 °C = −40 °F (the crossover).
934        assert_eq!(Quantity::of(i(0), &celsius()).in_unit(&kelvin()).unwrap(), r(5463, 20));
935        assert_eq!(Quantity::of(i(20), &celsius()).in_unit(&fahrenheit()).unwrap(), i(68));
936        assert_eq!(Quantity::of(i(100), &celsius()).in_unit(&fahrenheit()).unwrap(), i(212));
937        assert_eq!(Quantity::of(i(-40), &celsius()).in_unit(&fahrenheit()).unwrap(), i(-40));
938        assert_eq!(Quantity::of(i(212), &fahrenheit()).in_unit(&celsius()).unwrap(), i(100));
939    }
940
941    #[test]
942    fn cross_dimension_operations_are_forbidden() {
943        let length = Quantity::of(i(1), &metre());
944        let mass = Quantity::of(i(1), &kilogram());
945        // Can't add Length to Mass; can't convert Length to a Mass unit.
946        assert!(length.add(&mass).is_none(), "Length + Mass is forbidden");
947        assert!(length.sub(&mass).is_none(), "Length − Mass is forbidden");
948        assert!(length.in_unit(&kilogram()).is_none(), "Length → Mass cast is forbidden");
949    }
950
951    #[test]
952    fn multiplication_and_division_combine_dimensions() {
953        // Length × Length = Area.
954        let area = Quantity::of(i(3), &metre()).mul(&Quantity::of(i(4), &metre()));
955        assert_eq!(area.dimension(), Dimension::area());
956        assert_eq!(area.magnitude_si(), &i(12)); // 12 m²
957        // Length ÷ Time = Speed.
958        let speed = Quantity::of(i(100), &metre()).div(&Quantity::of(i(10), &second())).unwrap();
959        assert_eq!(speed.dimension(), Dimension::speed());
960        assert_eq!(speed.magnitude_si(), &i(10)); // 10 m/s
961        // Area ÷ Length = Length.
962        let back = area.div(&Quantity::of(i(3), &metre())).unwrap();
963        assert_eq!(back.dimension(), Dimension::length());
964        // Scaling by a dimensionless quantity keeps the dimension.
965        let scaled = Quantity::of(i(2), &metre()).mul(&Quantity::scalar(i(3)));
966        assert_eq!(scaled.dimension(), Dimension::length());
967        assert_eq!(scaled.in_unit(&metre()).unwrap(), i(6));
968    }
969
970    #[test]
971    fn dimensional_algebra_gauntlet_produces_correct_derived_dimensions_and_magnitudes() {
972        // Every combination below checks BOTH the resulting dimension AND the exact SI magnitude, so
973        // the algebra can never silently produce the right unit with the wrong number (or vice versa).
974        // mass × acceleration = force.
975        let accel = Quantity::of(i(3), &metre_per_second()).div(&Quantity::of(i(1), &second())).unwrap();
976        let force = Quantity::of(i(2), &kilogram()).mul(&accel);
977        assert_eq!(force.dimension(), Dimension::force());
978        assert_eq!(force.magnitude_si(), &i(6)); // 6 N
979        // force × length = energy.
980        let energy = force.mul(&Quantity::of(i(4), &metre()));
981        assert_eq!(energy.dimension(), Dimension::energy());
982        assert_eq!(energy.magnitude_si(), &i(24)); // 24 J
983        // energy ÷ time = power.
984        let power = energy.div(&Quantity::of(i(2), &second())).unwrap();
985        assert_eq!(power.dimension(), Dimension::power());
986        assert_eq!(power.magnitude_si(), &i(12)); // 12 W
987        // force ÷ area = pressure.
988        let pressure = force.div(&Quantity::of(i(2), &square_metre())).unwrap();
989        assert_eq!(pressure.dimension(), Dimension::pressure());
990        assert_eq!(pressure.magnitude_si(), &i(3)); // 3 Pa
991        // current × time = charge.
992        let charge = Quantity::of(i(5), &ampere()).mul(&Quantity::of(i(2), &second()));
993        assert_eq!(charge.dimension(), Dimension::charge());
994        assert_eq!(charge.magnitude_si(), &i(10)); // 10 C
995        // information ÷ time = data rate.
996        let rate = Quantity::of(i(16), &bit()).div(&Quantity::of(i(2), &second())).unwrap();
997        assert_eq!(rate.dimension(), Dimension::data_rate());
998        assert_eq!(rate.in_unit(&bit_per_second()).unwrap(), i(8)); // 8 bit/s
999        // volume ÷ time = volumetric flow.
1000        let flow = Quantity::of(i(6), &cubic_metre()).div(&Quantity::of(i(2), &second())).unwrap();
1001        assert_eq!(flow.dimension(), Dimension::volumetric_flow());
1002        assert_eq!(flow.in_unit(&cubic_metre_per_second()).unwrap(), i(3));
1003        // amount ÷ volume = molar concentration (2 mol/m³ = 2 mM).
1004        let conc = Quantity::of(i(2), &mole()).div(&Quantity::of(i(1), &cubic_metre())).unwrap();
1005        assert_eq!(conc.dimension(), Dimension::molar_concentration());
1006        assert_eq!(conc.in_unit(&millimolar()).unwrap(), i(2));
1007        // area ÷ time = kinematic viscosity.
1008        let kv = Quantity::of(i(4), &square_metre()).div(&Quantity::of(i(2), &second())).unwrap();
1009        assert_eq!(kv.dimension(), Dimension::kinematic_viscosity());
1010        assert_eq!(kv.in_unit(&square_metre_per_second()).unwrap(), i(2));
1011        // pressure × time = dynamic viscosity.
1012        let dv = Quantity::of(i(4), &pascal()).mul(&Quantity::of(i(2), &second()));
1013        assert_eq!(dv.dimension(), Dimension::dynamic_viscosity());
1014        assert_eq!(dv.in_unit(&pascal_second()).unwrap(), i(8));
1015        // length ÷ volume = fuel economy (100 km / 10 L = 10 km/L).
1016        let fe = Quantity::of(i(100), &kilometre()).div(&Quantity::of(i(10), &litre())).unwrap();
1017        assert_eq!(fe.dimension(), Dimension::fuel_economy());
1018        assert_eq!(fe.in_unit(&km_per_litre()).unwrap(), i(10));
1019        // dynamic viscosity ÷ density = kinematic viscosity (μ/ρ = ν), exact magnitude.
1020        let mu = Quantity::of(i(6), &pascal_second());
1021        let rho = Quantity::of(i(2), &kilogram()).div(&Quantity::of(i(1), &cubic_metre())).unwrap();
1022        let nu = mu.div(&rho).unwrap();
1023        assert_eq!(nu.dimension(), Dimension::kinematic_viscosity());
1024        assert_eq!(nu.in_unit(&square_metre_per_second()).unwrap(), i(3)); // 6 / 2 = 3 m²/s
1025    }
1026
1027    /// A tiny deterministic RNG (SplitMix64) for the conversion-round-trip gauntlet.
1028    struct Rng(u64);
1029    impl Rng {
1030        fn next(&mut self) -> u64 {
1031            self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
1032            let mut z = self.0;
1033            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1034            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1035            z ^ (z >> 31)
1036        }
1037    }
1038
1039    #[test]
1040    fn conversion_round_trips_and_add_commutes_under_fuzz() {
1041        // Property fuzz: for any value and any two units of the same dimension, converting in and
1042        // back out is the identity, and addition commutes — all exact.
1043        let length_units = [metre(), kilometre(), centimetre(), inch(), foot(), yard(), mile()];
1044        let mass_units = [kilogram(), gram(), pound(), ounce()];
1045        let temp_units = [kelvin(), celsius(), fahrenheit()];
1046        let mut rng = Rng(0x_FACE_C0DE_1234_5678);
1047        for _ in 0..2000 {
1048            let n = (rng.next() % 4001) as i64 - 2000;
1049            let d = ((rng.next() % 99) as i64) + 1;
1050            let value = r(n, d);
1051            for set in [&length_units[..], &mass_units[..], &temp_units[..]] {
1052                let u1 = &set[(rng.next() as usize) % set.len()];
1053                let u2 = &set[(rng.next() as usize) % set.len()];
1054                let q = Quantity::of(value.clone(), u1);
1055                // Round-trip: value → SI → value (in the same unit) is exact identity.
1056                assert_eq!(q.in_unit(u1).unwrap(), value, "round-trip in {}", u1.symbol);
1057                // Re-expressing in u2 then back to u1 is also identity.
1058                let via = Quantity::of(q.in_unit(u2).unwrap(), u2);
1059                assert_eq!(via.in_unit(u1).unwrap(), value, "{} → {} → {}", u1.symbol, u2.symbol, u1.symbol);
1060                // Same-dimension addition commutes (magnitudes, so always for linear; affine too
1061                // since it is magnitude addition in SI base).
1062                let q2 = Quantity::of(value.clone(), u2);
1063                assert_eq!(q.add(&q2), q2.add(&q), "add commutes");
1064            }
1065        }
1066    }
1067
1068    #[test]
1069    fn whole_catalog_round_trips_through_every_sibling_under_fuzz() {
1070        // Extends the round-trip property to EVERY dimension group in the catalog (not just the
1071        // length/mass/temp trio above): for random rational values, value → unit → SI → sibling →
1072        // SI → original-unit is exact identity, and same-dimension conversion is always total.
1073        let groups = catalog();
1074        let mut rng = Rng(0x_C0FF_EE_D0_0D_F0_0D);
1075        for _ in 0..400 {
1076            let n = (rng.next() % 20_001) as i64 - 10_000;
1077            let d = ((rng.next() % 199) as i64) + 1;
1078            let value = r(n, d);
1079            for group in &groups {
1080                let u1 = &group[(rng.next() as usize) % group.len()];
1081                let u2 = &group[(rng.next() as usize) % group.len()];
1082                let q = Quantity::of(value.clone(), u1);
1083                let through = q.in_unit(u2).expect("same-dimension conversion is total");
1084                let back = Quantity::of(through, u2).in_unit(u1).unwrap();
1085                assert_eq!(back, value, "{} → {} → {}", u1.symbol, u2.symbol, u1.symbol);
1086            }
1087        }
1088    }
1089
1090    #[test]
1091    fn affine_temperature_fixed_points_are_exact() {
1092        // The canonical physics anchors, all exact in the rational tower.
1093        // Water freezes: 0 °C = 273.15 K = 32 °F = 491.67 °R = 0 °Ré.
1094        assert_eq!(Quantity::of(i(0), &celsius()).in_unit(&kelvin()).unwrap(), r(27315, 100));
1095        assert_eq!(Quantity::of(i(0), &celsius()).in_unit(&fahrenheit()).unwrap(), i(32));
1096        assert_eq!(Quantity::of(i(0), &celsius()).in_unit(&rankine()).unwrap(), r(49167, 100));
1097        assert_eq!(Quantity::of(i(0), &celsius()).in_unit(&reaumur()).unwrap(), i(0));
1098        // Water boils: 100 °C = 212 °F = 373.15 K = 80 °Ré.
1099        assert_eq!(Quantity::of(i(100), &celsius()).in_unit(&fahrenheit()).unwrap(), i(212));
1100        assert_eq!(Quantity::of(i(100), &celsius()).in_unit(&kelvin()).unwrap(), r(37315, 100));
1101        assert_eq!(Quantity::of(i(100), &celsius()).in_unit(&reaumur()).unwrap(), i(80));
1102        // The famous crossover: −40 °C = −40 °F exactly.
1103        assert_eq!(Quantity::of(i(-40), &celsius()).in_unit(&fahrenheit()).unwrap(), i(-40));
1104        // Absolute zero: 0 K = −273.15 °C = −459.67 °F = 0 °R.
1105        assert_eq!(Quantity::of(i(0), &kelvin()).in_unit(&celsius()).unwrap(), r(-27315, 100));
1106        assert_eq!(Quantity::of(i(0), &kelvin()).in_unit(&fahrenheit()).unwrap(), r(-45967, 100));
1107        assert_eq!(Quantity::of(i(0), &kelvin()).in_unit(&rankine()).unwrap(), i(0));
1108        // Body temperature, a non-integer anchor: 37 °C = 98.6 °F.
1109        assert_eq!(Quantity::of(i(37), &celsius()).in_unit(&fahrenheit()).unwrap(), r(986, 10));
1110        // Round-trip through every affine scale is identity for an arbitrary fractional reading.
1111        let v = r(37, 7);
1112        for u in [kelvin(), celsius(), fahrenheit(), rankine(), reaumur()] {
1113            assert_eq!(Quantity::of(v.clone(), &u).in_unit(&u).unwrap(), v, "round-trip {}", u.symbol);
1114        }
1115    }
1116
1117    #[test]
1118    fn cooking_measurements_convert_exactly() {
1119        // The US kitchen ladder — all exact, all anchored to the 231-in³ gallon.
1120        assert_eq!(Quantity::of(i(1), &us_cup()).in_unit(&tablespoon()).unwrap(), i(16), "1 cup = 16 tbsp");
1121        assert_eq!(Quantity::of(i(1), &us_cup()).in_unit(&teaspoon()).unwrap(), i(48), "1 cup = 48 tsp");
1122        assert_eq!(Quantity::of(i(1), &us_cup()).in_unit(&us_fluid_ounce()).unwrap(), i(8), "1 cup = 8 fl oz");
1123        assert_eq!(Quantity::of(i(1), &tablespoon()).in_unit(&teaspoon()).unwrap(), i(3), "1 tbsp = 3 tsp");
1124        assert_eq!(Quantity::of(i(1), &us_fluid_ounce()).in_unit(&tablespoon()).unwrap(), i(2), "1 fl oz = 2 tbsp");
1125        assert_eq!(Quantity::of(i(1), &us_pint()).in_unit(&us_cup()).unwrap(), i(2), "1 pint = 2 cups");
1126        assert_eq!(Quantity::of(i(1), &us_quart()).in_unit(&us_cup()).unwrap(), i(4), "1 quart = 4 cups");
1127        assert_eq!(Quantity::of(i(1), &us_gallon()).in_unit(&us_cup()).unwrap(), i(16), "1 gallon = 16 cups");
1128        assert_eq!(Quantity::of(i(1), &us_gallon()).in_unit(&teaspoon()).unwrap(), i(768), "1 gallon = 768 tsp");
1129        assert_eq!(Quantity::of(i(1), &oil_barrel()).in_unit(&us_gallon()).unwrap(), i(42), "1 oil barrel = 42 gal");
1130        // Cross to metric, exact: 1 US gallon = 3.785411784 L; 1 tsp ≈ 4.92892159375 mL (exact rational).
1131        assert_eq!(Quantity::of(i(1), &us_gallon()).in_unit(&litre()).unwrap(), r(473_176_473, 125_000_000));
1132        // A real recipe: 3 teaspoons + 1 tablespoon = 2 tablespoons, exactly.
1133        let mixed = Quantity::of(i(3), &teaspoon()).add(&Quantity::of(i(1), &tablespoon())).unwrap();
1134        assert_eq!(mixed.in_unit(&tablespoon()).unwrap(), i(2));
1135    }
1136
1137    #[test]
1138    fn general_cross_unit_conversions_are_exact() {
1139        // Volume / cubic.
1140        assert_eq!(Quantity::of(i(1), &cubic_metre()).in_unit(&litre()).unwrap(), i(1000));
1141        assert_eq!(Quantity::of(i(1), &cubic_foot()).in_unit(&cubic_inch()).unwrap(), i(1728));
1142        assert_eq!(Quantity::of(i(1), &litre()).in_unit(&millilitre()).unwrap(), i(1000));
1143        // Area.
1144        assert_eq!(Quantity::of(i(1), &acre()).in_unit(&square_foot()).unwrap(), i(43_560));
1145        assert_eq!(Quantity::of(i(1), &hectare()).in_unit(&square_metre()).unwrap(), i(10_000));
1146        assert_eq!(Quantity::of(i(1), &square_foot()).in_unit(&square_inch()).unwrap(), i(144));
1147        // Speed, time, frequency.
1148        assert_eq!(Quantity::of(i(1), &kilometre_per_hour()).in_unit(&metre_per_second()).unwrap(), r(5, 18));
1149        assert_eq!(Quantity::of(i(1), &day()).in_unit(&hour()).unwrap(), i(24));
1150        assert_eq!(Quantity::of(i(1), &fortnight()).in_unit(&day()).unwrap(), i(14));
1151        assert_eq!(Quantity::of(i(1), &gigahertz()).in_unit(&hertz()).unwrap(), i(1_000_000_000));
1152        // Energy / power / pressure.
1153        assert_eq!(Quantity::of(i(1), &kilocalorie()).in_unit(&calorie()).unwrap(), i(1000));
1154        assert_eq!(Quantity::of(i(1), &kilowatt_hour()).in_unit(&joule()).unwrap(), i(3_600_000));
1155        assert_eq!(Quantity::of(i(1), &calorie()).in_unit(&joule()).unwrap(), r(523, 125)); // 4.184 J
1156        assert_eq!(Quantity::of(i(1), &atmosphere()).in_unit(&pascal()).unwrap(), i(101_325));
1157        // Information (decimal vs binary prefixes).
1158        assert_eq!(Quantity::of(i(1), &byte()).in_unit(&bit()).unwrap(), i(8));
1159        assert_eq!(Quantity::of(i(1), &kibibyte()).in_unit(&byte()).unwrap(), i(1024));
1160        assert_eq!(Quantity::of(i(1), &mebibyte()).in_unit(&kibibyte()).unwrap(), i(1024));
1161        assert_eq!(Quantity::of(i(1), &megabyte()).in_unit(&byte()).unwrap(), i(1_000_000));
1162        // Angle.
1163        assert_eq!(Quantity::of(i(1), &turn()).in_unit(&degree()).unwrap(), i(360));
1164        assert_eq!(Quantity::of(i(1), &degree()).in_unit(&arcminute()).unwrap(), i(60));
1165        assert_eq!(Quantity::of(i(1), &arcminute()).in_unit(&arcsecond()).unwrap(), i(60));
1166        // Charge.
1167        assert_eq!(Quantity::of(i(1), &ampere_hour()).in_unit(&coulomb()).unwrap(), i(3600));
1168    }
1169
1170    #[test]
1171    fn extended_units_cooking_nautical_astronomical_radiation_illumination_convert_exactly() {
1172        // --- Cooking extras ---
1173        assert_eq!(Quantity::of(i(1), &teaspoon()).in_unit(&dash()).unwrap(), i(8), "1 tsp = 8 dashes");
1174        assert_eq!(Quantity::of(i(1), &teaspoon()).in_unit(&pinch()).unwrap(), i(16), "1 tsp = 16 pinches");
1175        assert_eq!(Quantity::of(i(1), &teaspoon()).in_unit(&smidgen()).unwrap(), i(32), "1 tsp = 32 smidgens");
1176        assert_eq!(Quantity::of(i(1), &dash()).in_unit(&pinch()).unwrap(), i(2), "1 dash = 2 pinches");
1177        assert_eq!(Quantity::of(i(1), &stick_of_butter()).in_unit(&tablespoon()).unwrap(), i(8), "1 stick = 8 tbsp");
1178        assert_eq!(Quantity::of(i(1), &stick_of_butter()).in_unit(&us_cup()).unwrap(), r(1, 2), "1 stick = ½ cup");
1179        // --- US dry ---
1180        assert_eq!(Quantity::of(i(1), &bushel()).in_unit(&peck()).unwrap(), i(4), "1 bushel = 4 pecks");
1181        assert_eq!(Quantity::of(i(1), &bushel()).in_unit(&dry_gallon()).unwrap(), i(8));
1182        assert_eq!(Quantity::of(i(1), &dry_gallon()).in_unit(&dry_quart()).unwrap(), i(4));
1183        // --- Nautical ---
1184        assert_eq!(Quantity::of(i(1), &nautical_mile()).in_unit(&cable()).unwrap(), i(10), "1 nmi = 10 cables");
1185        assert_eq!(Quantity::of(i(1), &nautical_league()).in_unit(&nautical_mile()).unwrap(), i(3));
1186        assert_eq!(Quantity::of(i(1), &league()).in_unit(&mile()).unwrap(), i(3), "1 league = 3 miles");
1187        // --- Astronomical (the light-time ladder is exact; AU/ly are defined) ---
1188        assert_eq!(Quantity::of(i(1), &light_minute()).in_unit(&light_second()).unwrap(), i(60));
1189        assert_eq!(Quantity::of(i(1), &light_hour()).in_unit(&light_minute()).unwrap(), i(60));
1190        assert_eq!(Quantity::of(i(1), &light_day()).in_unit(&light_hour()).unwrap(), i(24));
1191        assert_eq!(Quantity::of(i(1), &light_second()).in_unit(&metre()).unwrap(), i(299_792_458));
1192        // --- Radiation ---
1193        assert_eq!(Quantity::of(i(1), &gray()).in_unit(&rad_unit()).unwrap(), i(100), "1 Gy = 100 rad");
1194        assert_eq!(Quantity::of(i(1), &sievert()).in_unit(&rem()).unwrap(), i(100), "1 Sv = 100 rem");
1195        assert_eq!(Quantity::of(i(1), &curie()).in_unit(&becquerel()).unwrap(), i(37_000_000_000), "1 Ci = 3.7e10 Bq");
1196        assert_eq!(gray().dimension, Dimension::absorbed_dose());
1197        assert_eq!(sievert().dimension, Dimension::absorbed_dose()); // Gy and Sv share a dimension
1198        assert_eq!(becquerel().dimension, Dimension::radioactivity());
1199        assert_eq!(roentgen().dimension, Dimension::exposure());
1200        assert_eq!(katal().dimension, Dimension::catalytic_activity());
1201        // --- Illumination (photometry) ---
1202        assert_eq!(Quantity::of(i(1), &phot()).in_unit(&lux()).unwrap(), i(10_000), "1 phot = 10000 lux");
1203        assert_eq!(Quantity::of(i(1), &foot_candle()).in_unit(&lux()).unwrap(), r(1_562_500, 145_161));
1204        assert_eq!(lumen().dimension, Dimension::luminous_flux());
1205        assert_eq!(lux().dimension, Dimension::illuminance());
1206        assert_eq!(nit().dimension, Dimension::luminance());
1207    }
1208
1209    #[test]
1210    fn rate_flow_concentration_viscosity_torque_and_prefixes_convert_exactly() {
1211        // Data rate.
1212        assert_eq!(Quantity::of(i(1), &byte_per_second()).in_unit(&bit_per_second()).unwrap(), i(8));
1213        assert_eq!(Quantity::of(i(1), &gigabit_per_second()).in_unit(&megabit_per_second()).unwrap(), i(1000));
1214        assert_eq!(Quantity::of(i(1), &megabyte_per_second()).in_unit(&bit_per_second()).unwrap(), i(8_000_000));
1215        // Volumetric flow.
1216        assert_eq!(Quantity::of(i(1), &cubic_metre_per_second()).in_unit(&litre_per_second()).unwrap(), i(1000));
1217        assert_eq!(Quantity::of(i(1), &litre_per_second()).in_unit(&litre_per_minute()).unwrap(), i(60));
1218        // Concentration.
1219        assert_eq!(Quantity::of(i(1), &molar()).in_unit(&millimolar()).unwrap(), i(1000));
1220        assert_eq!(Quantity::of(i(1), &millimolar()).in_unit(&micromolar()).unwrap(), i(1000));
1221        // Viscosity (dynamic + kinematic).
1222        assert_eq!(Quantity::of(i(1), &pascal_second()).in_unit(&poise()).unwrap(), i(10));
1223        assert_eq!(Quantity::of(i(1), &poise()).in_unit(&centipoise()).unwrap(), i(100));
1224        assert_eq!(Quantity::of(i(1), &square_metre_per_second()).in_unit(&stokes()).unwrap(), i(10_000));
1225        assert_eq!(Quantity::of(i(1), &stokes()).in_unit(&centistokes()).unwrap(), i(100));
1226        // Torque shares the energy dimension (N·m ≡ J dimensionally) — convertible to joules.
1227        assert_eq!(newton_metre().dimension, Dimension::energy());
1228        assert_eq!(Quantity::of(i(1), &newton_metre()).in_unit(&joule()).unwrap(), i(1));
1229        // Fuel economy is its own (reciprocal-area) dimension.
1230        assert_eq!(km_per_litre().dimension, Dimension::fuel_economy());
1231        assert_eq!(mile_per_gallon().dimension, Dimension::fuel_economy());
1232        // Extra SI prefixes.
1233        assert_eq!(Quantity::of(i(1), &gigametre()).in_unit(&megametre()).unwrap(), i(1000));
1234        assert_eq!(Quantity::of(i(1), &megametre()).in_unit(&metre()).unwrap(), i(1_000_000));
1235        assert_eq!(Quantity::of(i(1), &terawatt()).in_unit(&gigawatt()).unwrap(), i(1000));
1236        assert_eq!(Quantity::of(i(1), &petabyte()).in_unit(&terabyte()).unwrap(), i(1000));
1237        assert_eq!(Quantity::of(i(1), &picometre()).in_unit(&femtometre()).unwrap(), i(1000));
1238        assert_eq!(Quantity::of(i(1), &terajoule()).in_unit(&gigajoule()).unwrap(), i(1000));
1239    }
1240
1241    #[test]
1242    fn electromagnetic_and_surface_tension_units_convert_exactly() {
1243        // Conductance is dimensionally the reciprocal of resistance (S = Ω⁻¹).
1244        assert_eq!(siemens().dimension, Dimension::resistance().recip());
1245        assert_eq!(Quantity::of(i(1), &siemens()).in_unit(&millisiemens()).unwrap(), i(1000));
1246        // Capacitance ladder.
1247        assert_eq!(Quantity::of(i(1), &microfarad()).in_unit(&nanofarad()).unwrap(), i(1000));
1248        assert_eq!(Quantity::of(i(1), &nanofarad()).in_unit(&picofarad()).unwrap(), i(1000));
1249        assert_eq!(Quantity::of(i(1), &farad()).in_unit(&picofarad()).unwrap(), i(1_000_000_000_000));
1250        // Magnetic flux: 1 Wb = 10⁸ maxwell.
1251        assert_eq!(Quantity::of(i(1), &weber()).in_unit(&maxwell()).unwrap(), i(100_000_000));
1252        // Inductance ladder.
1253        assert_eq!(Quantity::of(i(1), &henry()).in_unit(&millihenry()).unwrap(), i(1000));
1254        assert_eq!(Quantity::of(i(1), &millihenry()).in_unit(&microhenry()).unwrap(), i(1000));
1255        // Magnetic flux density: 1 T = 10⁴ gauss = 10³ mT.
1256        assert_eq!(Quantity::of(i(1), &tesla()).in_unit(&gauss()).unwrap(), i(10_000));
1257        assert_eq!(Quantity::of(i(1), &tesla()).in_unit(&millitesla()).unwrap(), i(1000));
1258        // Surface tension: 1 N/m = 1000 dyn/cm.
1259        assert_eq!(Quantity::of(i(1), &newton_per_metre()).in_unit(&dyne_per_centimetre()).unwrap(), i(1000));
1260        // The defining SI relations hold at the dimension level (units can never drift from physics).
1261        assert_eq!(farad().dimension, Dimension::charge().div(Dimension::voltage()));
1262        assert_eq!(henry().dimension, Dimension::magnetic_flux().div(Dimension::current()));
1263        assert_eq!(tesla().dimension, Dimension::magnetic_flux().div(Dimension::area()));
1264        // Cross-dimension stays a forbidden cast (capacitance ≠ inductance).
1265        assert!(Quantity::of(i(1), &farad()).in_unit(&henry()).is_none());
1266    }
1267
1268    #[test]
1269    fn dimensionless_counting_and_ratio_units_convert_exactly() {
1270        // Counting words are exact integer multiples of a single item.
1271        assert_eq!(Quantity::of(i(1), &dozen()).in_unit(&each()).unwrap(), i(12));
1272        assert_eq!(Quantity::of(i(1), &gross()).in_unit(&dozen()).unwrap(), i(12)); // 144 = 12 dozen
1273        assert_eq!(Quantity::of(i(1), &great_gross()).in_unit(&gross()).unwrap(), i(12));
1274        assert_eq!(Quantity::of(i(1), &score()).in_unit(&each()).unwrap(), i(20));
1275        assert_eq!(Quantity::of(i(1), &baker_dozen()).in_unit(&each()).unwrap(), i(13));
1276        assert_eq!(Quantity::of(i(1), &ream()).in_unit(&each()).unwrap(), i(500));
1277        // Ratios: 1 = 100% = 1000‰ = 10000 bp; ppm/ppb are the metrology fractions.
1278        assert_eq!(Quantity::of(i(1), &each()).in_unit(&percent()).unwrap(), i(100));
1279        assert_eq!(Quantity::of(i(1), &each()).in_unit(&permille()).unwrap(), i(1000));
1280        assert_eq!(Quantity::of(i(1), &each()).in_unit(&basis_point()).unwrap(), i(10_000));
1281        assert_eq!(Quantity::of(i(1), &percent()).in_unit(&basis_point()).unwrap(), i(100)); // 1% = 100 bp
1282        assert_eq!(Quantity::of(i(1), &percent()).in_unit(&permille()).unwrap(), i(10));
1283        assert_eq!(Quantity::of(i(1), &each()).in_unit(&ppm()).unwrap(), i(1_000_000));
1284        assert_eq!(Quantity::of(i(1), &ppm()).in_unit(&ppb()).unwrap(), i(1000));
1285        // 1 dozen = 1200% (12 × 100). The dimensionless algebra is exact across counting & ratio.
1286        assert_eq!(Quantity::of(i(1), &dozen()).in_unit(&percent()).unwrap(), i(1200));
1287        // Every counting/ratio unit is dimensionless, and converting to a dimensioned unit is forbidden.
1288        for u in [each(), pair(), dozen(), gross(), percent(), ppm(), basis_point()] {
1289            assert_eq!(u.dimension, Dimension::DIMENSIONLESS, "{} is dimensionless", u.symbol);
1290            assert!(Quantity::of(i(1), &u).in_unit(&metre()).is_none(), "{} → m forbidden", u.symbol);
1291        }
1292    }
1293
1294    #[test]
1295    fn light_travel_time_is_exact_and_dimension_safe() {
1296        let secs = |q: Quantity| q.in_unit(&second()).unwrap();
1297        let ltt = |u: Unit| super::light_travel_time(&Quantity::of(i(1), &u)).unwrap();
1298        // Distances defined in terms of c invert to exact round times.
1299        assert_eq!(secs(ltt(light_second())), i(1));
1300        assert_eq!(secs(ltt(light_minute())), i(60));
1301        assert_eq!(secs(ltt(light_year())), i(31_557_600)); // one Julian year
1302        // Earth–Sun light time = AU / c: a Time quantity, exact rational ≈ 499 s.
1303        let sun = ltt(astronomical_unit());
1304        assert_eq!(sun.dimension(), Dimension::time());
1305        let sun_secs = secs(sun);
1306        assert_eq!(sun_secs, r(149_597_870_700, 299_792_458));
1307        assert!(sun_secs > i(498) && sun_secs < i(500));
1308        // A non-length distance is the forbidden case (dimensional guard).
1309        assert!(super::light_travel_time(&Quantity::of(i(5), &kilogram())).is_none());
1310    }
1311
1312    #[test]
1313    fn observed_arrival_adds_light_delay() {
1314        const NS: i64 = 1_000_000_000;
1315        // A Time quantity converts to nanoseconds (rounded).
1316        assert_eq!(super::time_quantity_to_nanos(&Quantity::of(i(1), &second())), Some(NS));
1317        assert_eq!(super::time_quantity_to_nanos(&Quantity::of(i(1), &millisecond())), Some(1_000_000));
1318        // A non-time quantity is rejected.
1319        assert_eq!(super::time_quantity_to_nanos(&Quantity::of(i(1), &metre())), None);
1320        // An event observed 1 light-minute away arrives exactly 60 s later.
1321        assert_eq!(
1322            super::observed_arrival_nanos(0, &Quantity::of(i(1), &light_minute())),
1323            Some(60 * NS)
1324        );
1325        // From a nonzero event time, the delay adds on.
1326        assert_eq!(
1327            super::observed_arrival_nanos(5 * NS, &Quantity::of(i(1), &light_second())),
1328            Some(6 * NS)
1329        );
1330        // Earth–Sun: ~499 s of delay (AU / c rounded to the nearest nanosecond).
1331        let sun = super::observed_arrival_nanos(0, &Quantity::of(i(1), &astronomical_unit())).unwrap();
1332        assert!(sun > 498 * NS && sun < 500 * NS);
1333        // It equals the rounded light-travel time exactly.
1334        let expected = super::time_quantity_to_nanos(&super::light_travel_time(&Quantity::of(i(1), &astronomical_unit())).unwrap());
1335        assert_eq!(Some(sun), expected);
1336        // A non-length distance is rejected.
1337        assert_eq!(super::observed_arrival_nanos(0, &Quantity::of(i(1), &kilogram())), None);
1338    }
1339
1340    #[test]
1341    fn spacetime_stamp_is_space_aware_like_a_timezone_for_position() {
1342        use super::{CausalRelation, Position, SpacetimeStamp};
1343        const C: f64 = 299_792_458.0; // metres per second; c·1s = 1 light-second of distance
1344        let pos = |x: f64| Position { x, y: 0.0, z: 0.0 };
1345        let at = |ns: i64, x: f64| SpacetimeStamp { instant_nanos: ns, position: pos(x) };
1346        let here = pos(0.0);
1347        let one_ls = C; // 1 light-second east
1348
1349        // Same place → ordered by time alone (zero light separation), like one timezone.
1350        assert_eq!(at(0, 0.0).causal_relation(at(1_000_000_000, 0.0)), CausalRelation::Before);
1351        assert_eq!(at(1_000_000_000, 0.0).causal_relation(at(0, 0.0)), CausalRelation::After);
1352        // 1 light-second apart in space takes ~1 s of light delay (symmetric) — the "offset".
1353        assert_eq!(at(0, 0.0).light_separation_nanos(at(0, one_ls)), 1_000_000_000);
1354        // 2 s apart but only 1 light-second away → inside the light cone → causally Before.
1355        assert_eq!(at(0, 0.0).causal_relation(at(2_000_000_000, one_ls)), CausalRelation::Before);
1356        // Exactly on the light cone (1 s apart, 1 light-second away) → still causally connected.
1357        assert_eq!(at(0, 0.0).causal_relation(at(1_000_000_000, one_ls)), CausalRelation::Before);
1358        // Only 0.5 s apart but 1 light-second away → SPACELIKE: genuinely concurrent (CRDT land).
1359        assert_eq!(at(0, 0.0).causal_relation(at(500_000_000, one_ls)), CausalRelation::Concurrent);
1360        // Observed-at: an event here is seen 1 s later from 1 light-second away (the relative read).
1361        assert_eq!(at(0, 0.0).observed_at_nanos(pos(one_ls)), 1_000_000_000);
1362        // Euclidean distance is honest 3-D (a 3-4-5 triangle → 5).
1363        assert!((Position { x: 3.0, y: 4.0, z: 0.0 }.distance_to(here) - 5.0).abs() < 1e-9);
1364    }
1365
1366    #[test]
1367    fn relativistic_time_dilation() {
1368        let approx = |a: f64, b: f64| (a - b).abs() < 1e-9;
1369        // No motion → no dilation.
1370        assert_eq!(super::lorentz_factor(0.0), Some(1.0));
1371        // Clean betas: √(1−0.36)=0.8 → γ=1.25; √(1−0.64)=0.6 → γ=5/3.
1372        assert!(approx(super::lorentz_factor(0.6).unwrap(), 1.25));
1373        assert!(approx(super::lorentz_factor(0.8).unwrap(), 5.0 / 3.0));
1374        // A clock at 0.6c for 10 coordinate-seconds ages only 8 proper seconds.
1375        assert!(approx(super::proper_time_seconds(10.0, 0.6).unwrap(), 8.0));
1376        // Light speed and beyond (and NaN) are unphysical → None.
1377        assert_eq!(super::lorentz_factor(1.0), None);
1378        assert_eq!(super::lorentz_factor(1.5), None);
1379        assert_eq!(super::lorentz_factor(f64::NAN), None);
1380        // From a speed Quantity: v = (3/5)·c → γ = 1.25.
1381        let c = super::constants::speed_of_light();
1382        let v = Quantity::si(c.magnitude_si().mul(&r(3, 5)), Dimension::speed());
1383        assert!(approx(super::time_dilation_factor(&v).unwrap(), 1.25));
1384        // A non-speed quantity → None.
1385        assert_eq!(super::time_dilation_factor(&Quantity::of(i(1), &metre())), None);
1386    }
1387
1388    #[test]
1389    fn physical_constants_carry_correct_dimensions_and_compose() {
1390        use super::constants::*;
1391        // c is a speed, exactly 299 792 458 m/s, and exact in km/h (× 18/5).
1392        let c = speed_of_light();
1393        assert_eq!(c.dimension(), Dimension::speed());
1394        assert_eq!(c.in_unit(&metre_per_second()).unwrap(), i(299_792_458));
1395        assert_eq!(c.in_unit(&kilometre_per_hour()).unwrap(), r(5_396_264_244, 5));
1396        // E = m·c² type-checks as energy, with exactly 2·c² joules for a 2 kg mass.
1397        let energy = Quantity::of(i(2), &kilogram()).mul(&c).mul(&c);
1398        assert_eq!(energy.dimension(), Dimension::energy());
1399        let c_sq = i(299_792_458).mul(&i(299_792_458));
1400        assert_eq!(energy.magnitude_si(), &i(2).mul(&c_sq));
1401        // The gas constant is exactly Avogadro × Boltzmann, dimension J/(mol·K).
1402        let r_gas = molar_gas_constant();
1403        assert_eq!(r_gas.dimension(), Dimension::energy().div(Dimension::amount()).div(Dimension::temperature()));
1404        assert_eq!(r_gas.magnitude_si(), avogadro_constant().mul(&boltzmann_constant()).magnitude_si());
1405        // Each constant's dimension is its defining one.
1406        assert_eq!(planck_constant().dimension(), Dimension::energy().mul(Dimension::time())); // action
1407        assert_eq!(elementary_charge().dimension(), Dimension::charge());
1408        assert_eq!(boltzmann_constant().dimension(), Dimension::energy().div(Dimension::temperature()));
1409        assert_eq!(avogadro_constant().dimension(), Dimension::amount().recip());
1410        assert_eq!(gravitational_constant().dimension(),
1411                   Dimension::volume().div(Dimension::mass()).div(Dimension::time().powi(2)));
1412        assert_eq!(standard_gravity().dimension(), Dimension::acceleration());
1413        // Exact defining values: atm = 101 325 Pa, g₀ = 9.806 65 m/s².
1414        assert_eq!(standard_atmosphere().in_unit(&pascal()).unwrap(), i(101_325));
1415        assert_eq!(standard_gravity().in_unit(&metre_per_second()).is_none(), true); // m/s ≠ m/s²
1416        // The ideal-gas law n·R·T/V has dimension pressure (cross-constant physics composes).
1417        let n = Quantity::of(i(1), &mole());
1418        let t = Quantity::of(i(300), &kelvin());
1419        let v = Quantity::of(i(1), &cubic_metre());
1420        let p = n.mul(&r_gas).mul(&t).div(&v).unwrap();
1421        assert_eq!(p.dimension(), Dimension::pressure());
1422    }
1423
1424    #[test]
1425    fn in_best_unit_auto_scales_to_the_most_human_unit() {
1426        let ladder = [millimetre(), centimetre(), metre(), kilometre()];
1427        // 1500 m → 1.5 km (smallest magnitude that is still ≥ 1).
1428        let (mag, u) = Quantity::of(i(1500), &metre()).in_best_unit(&ladder).unwrap();
1429        assert_eq!((u.symbol, mag), ("km", r(3, 2)));
1430        // 0.5 km → 500 m (km would be 0.5 < 1, so it drops to metres).
1431        let (mag, u) = Quantity::of(r(1, 2), &kilometre()).in_best_unit(&ladder).unwrap();
1432        assert_eq!((u.symbol, mag), ("m", i(500)));
1433        // 0.003 m → 3 mm.
1434        let (mag, u) = Quantity::of(r(3, 1000), &metre()).in_best_unit(&ladder).unwrap();
1435        assert_eq!((u.symbol, mag), ("mm", i(3)));
1436        // Exactly 1 km stays 1 km (magnitude == 1 qualifies).
1437        let (mag, u) = Quantity::of(i(1), &kilometre()).in_best_unit(&ladder).unwrap();
1438        assert_eq!((u.symbol, mag), ("km", i(1)));
1439        // Below the smallest unit → fall back to the smallest unit (largest magnitude).
1440        let (mag, u) = Quantity::of(r(1, 10), &millimetre()).in_best_unit(&ladder).unwrap();
1441        assert_eq!((u.symbol, mag), ("mm", r(1, 10)));
1442        // Negatives scale by absolute magnitude: −2000 m → −2 km.
1443        let (mag, u) = Quantity::of(i(-2000), &metre()).in_best_unit(&ladder).unwrap();
1444        assert_eq!((u.symbol, mag), ("km", i(-2)));
1445        // A ladder with no same-dimension unit yields None (the dimensions never match).
1446        assert!(Quantity::of(i(1), &metre()).in_best_unit(&[kilogram(), second()]).is_none());
1447        // The chosen (magnitude, unit) reconstructs the original SI quantity exactly.
1448        let q = Quantity::of(i(1500), &metre());
1449        let (mag, u) = q.in_best_unit(&ladder).unwrap();
1450        assert_eq!(Quantity::of(mag, u).magnitude_si(), q.magnitude_si());
1451        // Works across other dimensions too: 2_500_000 g → 2.5 t.
1452        let mass_ladder = [gram(), kilogram(), tonne()];
1453        let (mag, u) = Quantity::of(i(2_500_000), &gram()).in_best_unit(&mass_ladder).unwrap();
1454        assert_eq!((u.symbol, mag), ("t", r(5, 2)));
1455    }
1456
1457    #[test]
1458    fn by_name_resolves_units_from_english_names_plurals_and_symbols() {
1459        // The golden trio resolve — plurals, British spellings, and symbols all map to one unit.
1460        for alias in ["inch", "inches", "in", "  Inch  "] {
1461            assert_eq!(by_name(alias), Some(inch()), "{alias}");
1462        }
1463        for alias in ["centimeter", "centimetre", "cm"] {
1464            assert_eq!(by_name(alias), Some(centimetre()), "{alias}");
1465        }
1466        for alias in ["foot", "feet", "ft"] {
1467            assert_eq!(by_name(alias), Some(foot()), "{alias}");
1468        }
1469        // Case-insensitive across dimensions.
1470        assert_eq!(by_name("KM"), Some(kilometre()));
1471        assert_eq!(by_name("Kilogram"), Some(kilogram()));
1472        assert_eq!(by_name("celsius"), Some(celsius()));
1473        assert_eq!(by_name("tablespoon"), Some(tablespoon()));
1474        assert_eq!(by_name("MPH"), Some(mile_per_hour()));
1475        // Unknown / empty names are a clean None, never a panic.
1476        assert_eq!(by_name("zorgles"), None);
1477        assert_eq!(by_name(""), None);
1478        // The golden pipeline routed entirely through by_name: 2 inch + 5 cm in feet = 42/127.
1479        let a = Quantity::of(i(2), &by_name("inches").unwrap());
1480        let b = Quantity::of(i(5), &by_name("cm").unwrap());
1481        let sum = a.add(&b).unwrap();
1482        assert_eq!(sum.in_unit(&by_name("feet").unwrap()).unwrap(), r(42, 127));
1483    }
1484
1485    #[test]
1486    fn by_name_covers_radiation_photometry_em_dimensionless_and_prefixes() {
1487        // A representative member from each later-added family resolves.
1488        assert_eq!(by_name("gray"), Some(gray()));
1489        assert_eq!(by_name("sievert"), Some(sievert()));
1490        assert_eq!(by_name("becquerel"), Some(becquerel()));
1491        assert_eq!(by_name("lux"), Some(lux()));
1492        assert_eq!(by_name("lumen"), Some(lumen()));
1493        assert_eq!(by_name("tesla"), Some(tesla()));
1494        assert_eq!(by_name("farad"), Some(farad()));
1495        assert_eq!(by_name("henry"), Some(henry()));
1496        assert_eq!(by_name("poise"), Some(poise()));
1497        assert_eq!(by_name("molar"), Some(molar()));
1498        assert_eq!(by_name("Mbps"), Some(megabit_per_second()));
1499        assert_eq!(by_name("dozen"), Some(dozen()));
1500        assert_eq!(by_name("percent"), Some(percent()));
1501        assert_eq!(by_name("ppm"), Some(ppm()));
1502        assert_eq!(by_name("terabyte"), Some(terabyte()));
1503        assert_eq!(by_name("pinch"), Some(pinch()));
1504        assert_eq!(by_name("fathom"), Some(fathom()));
1505        assert_eq!(by_name("light second"), Some(light_second()));
1506        // Every name by_name returns is a well-formed unit that round-trips exactly.
1507        for name in ["gray", "tesla", "poise", "dozen", "percent", "terabyte", "pinch", "light second", "lux", "molar"] {
1508            let u = by_name(name).unwrap();
1509            let v = r(3, 1);
1510            assert_eq!(Quantity::of(v.clone(), &u).in_unit(&u).unwrap(), v, "{name}");
1511        }
1512    }
1513
1514    /// The whole catalog, grouped by dimension — the completeness harness iterates it.
1515    fn catalog() -> Vec<Vec<Unit>> {
1516        vec![
1517            vec![metre(), kilometre(), hectometre(), dekametre(), decimetre(), centimetre(), millimetre(),
1518                 micrometre(), nanometre(), picometre(), femtometre(), megametre(), gigametre(), angstrom(),
1519                 inch(), foot(), yard(), mile(), nautical_mile(),
1520                 fathom(), furlong(), chain(), rod(), hand(), point(), pica(), thou(), astronomical_unit(),
1521                 light_year(), cable(), league(), nautical_league(), light_second(), light_minute(),
1522                 light_hour(), light_day(), lunar_distance(), solar_radius(), earth_radius()],
1523            vec![kilogram(), gram(), milligram(), microgram(), nanogram(), tonne(), pound(), ounce(), stone(), short_ton(),
1524                 long_ton(), hundredweight(), grain(), dram(), carat(), troy_ounce(), troy_pound(),
1525                 solar_mass(), earth_mass(), jupiter_mass()],
1526            vec![second(), millisecond(), microsecond(), nanosecond(), picosecond(), minute(), hour(), day(), week(),
1527                 fortnight(), julian_year(), decade(), century()],
1528            vec![kelvin(), celsius(), fahrenheit(), rankine(), reaumur()],
1529            vec![cubic_metre(), litre(), millilitre(), centilitre(), decilitre(), cubic_centimetre(),
1530                 cubic_inch(), cubic_foot(), us_gallon(), us_quart(), us_pint(), us_cup(), us_gill(),
1531                 us_fluid_ounce(), tablespoon(), teaspoon(), oil_barrel(), imperial_gallon(), imperial_pint(),
1532                 imperial_fluid_ounce(), stick_of_butter(), dash(), pinch(), smidgen(), bushel(), peck(),
1533                 dry_gallon(), dry_quart(), dry_pint()],
1534            vec![square_metre(), square_kilometre(), square_centimetre(), square_inch(), square_foot(),
1535                 square_yard(), square_mile(), hectare(), are(), acre()],
1536            vec![metre_per_second(), kilometre_per_hour(), mile_per_hour(), foot_per_second(), knot()],
1537            vec![hertz(), kilohertz(), megahertz(), gigahertz(), rpm()],
1538            vec![newton(), kilonewton(), dyne(), kilogram_force(), pound_force()],
1539            vec![joule(), kilojoule(), megajoule(), gigajoule(), terajoule(), calorie(), kilocalorie(),
1540                 watt_hour(), kilowatt_hour(), erg(), electronvolt(), newton_metre(), pound_foot()],
1541            vec![watt(), kilowatt(), megawatt(), gigawatt(), terawatt(), horsepower()],
1542            vec![pascal(), kilopascal(), bar(), millibar(), atmosphere(), psi()],
1543            vec![bit(), byte(), kilobit(), kilobyte(), megabyte(), gigabyte(), terabit(), terabyte(), petabyte(),
1544                 kibibyte(), mebibyte(), gibibyte()],
1545            vec![turn(), degree(), gradian(), arcminute(), arcsecond()],
1546            vec![ampere(), milliampere()],
1547            vec![coulomb(), ampere_hour(), milliampere_hour()],
1548            vec![volt(), millivolt(), kilovolt()],
1549            vec![ohm()],
1550            vec![mole()],
1551            vec![candela()],
1552            // Radiation.
1553            vec![gray(), sievert(), rad_unit(), rem()], // absorbed/equivalent dose (J/kg)
1554            vec![becquerel(), curie()],                  // radioactivity (1/s)
1555            vec![roentgen()],                            // exposure (C/kg)
1556            vec![katal()],                               // catalytic activity (mol/s)
1557            // Photometry.
1558            vec![lumen()],                               // luminous flux (cd·sr)
1559            vec![lux(), phot(), foot_candle()],          // illuminance (lm/m²)
1560            vec![nit()],                                 // luminance (cd/m²)
1561            // Rates, flow, concentration, viscosity, fuel economy.
1562            vec![bit_per_second(), kilobit_per_second(), megabit_per_second(), gigabit_per_second(),
1563                 byte_per_second(), megabyte_per_second()],                                   // data rate (bit/s)
1564            vec![cubic_metre_per_second(), litre_per_second(), litre_per_minute(),
1565                 gallon_per_minute(), cubic_foot_per_minute()],                               // volumetric flow (m³/s)
1566            vec![molar(), millimolar(), micromolar()],                                        // molar concentration (mol/m³)
1567            vec![molal()],                                                                    // molality (mol/kg)
1568            vec![pascal_second(), poise(), centipoise()],                                     // dynamic viscosity (Pa·s)
1569            vec![square_metre_per_second(), stokes(), centistokes()],                         // kinematic viscosity (m²/s)
1570            vec![mile_per_gallon(), km_per_litre()],                                          // fuel economy (m⁻²)
1571            // Electromagnetism & surface tension.
1572            vec![siemens(), millisiemens()],                                                  // conductance (S = Ω⁻¹)
1573            vec![farad(), microfarad(), nanofarad(), picofarad()],                            // capacitance (F)
1574            vec![weber(), maxwell()],                                                         // magnetic flux (Wb)
1575            vec![henry(), millihenry(), microhenry()],                                        // inductance (H)
1576            vec![tesla(), millitesla(), gauss()],                                             // magnetic flux density (T)
1577            vec![newton_per_metre(), dyne_per_centimetre()],                                  // surface tension (N/m)
1578            // Counting & ratio (dimensionless).
1579            vec![each(), pair(), dozen(), baker_dozen(), score(), gross(), great_gross(), ream(),
1580                 percent(), permille(), ppm(), ppb(), basis_point()],
1581        ]
1582    }
1583
1584    /// COMPLETENESS HARNESS: every unit in the catalog must (1) round-trip `of(v,u).in_unit(u)==v`,
1585    /// (2) have a positive scale, (3) be inter-convertible with same-dimension units, and (4) refuse
1586    /// conversion to a different dimension (the forbidden cast). Adding a unit auto-extends coverage.
1587    #[test]
1588    fn every_unit_in_the_catalog_is_well_formed_and_dimension_safe() {
1589        let groups = catalog();
1590        let v = r(7, 3); // an arbitrary nonzero value
1591        for group in &groups {
1592            for u in group {
1593                // (1) round-trip identity.
1594                assert_eq!(Quantity::of(v.clone(), u).in_unit(u).unwrap(), v, "round-trip {}", u.symbol);
1595                // (2) positive scale.
1596                assert!(!u.scale.is_zero() && !u.scale.is_negative(), "scale > 0 for {}", u.symbol);
1597                // (3) inter-convertible within the dimension.
1598                for other in group {
1599                    assert!(
1600                        Quantity::of(v.clone(), u).in_unit(other).is_some(),
1601                        "{} convertible to {}",
1602                        u.symbol,
1603                        other.symbol
1604                    );
1605                    assert_eq!(u.dimension, other.dimension, "{} and {} share a dimension", u.symbol, other.symbol);
1606                }
1607            }
1608        }
1609        // (4) cross-dimension conversion is forbidden across every pair of distinct groups.
1610        for (gi, ga) in groups.iter().enumerate() {
1611            for (gj, gb) in groups.iter().enumerate() {
1612                if gi == gj || ga[0].dimension == gb[0].dimension {
1613                    continue;
1614                }
1615                assert!(
1616                    Quantity::of(v.clone(), &ga[0]).in_unit(&gb[0]).is_none(),
1617                    "{} → {} must be a forbidden cross-dimension cast",
1618                    ga[0].symbol,
1619                    gb[0].symbol
1620                );
1621            }
1622        }
1623    }
1624}