Skip to main content

qtty_core/units/length/
mod.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Length units.
5//!
6//! The canonical scaling unit for this dimension is [`Meter`] (`Meter::RATIO == 1.0`). All other
7//! length units are expressed as exact or best‑available ratios to metres.
8//!
9//! This module provides:
10//!
11//! - **SI ladder**: the full metric prefix family for metres from yocto‑ to yotta‑.
12//! - **Common defined units**: inch, foot, yard, (statute) mile, nautical mile, surveying units.
13//! - **Astronomy**: astronomical unit (au), light‑year (ly), parsec (pc) and its multiples.
14//! - **Geodesy and navigation**: Earth circumferences and related standards distances.
15//! - **Fundamental physics lengths**: Bohr radius, Planck length, and related constants.
16//! - **Nominal radii and distances**: available under the [`nominal`] submodule.
17//!
18//! Notes on definitions used here:
19//!
20//! - **Astronomical unit (au)** is **exactly** `149_597_870_700 m` (IAU 2012).
21//! - **Parsec (pc)** is defined from au via `pc = au * 648000 / π` (exact, given au).
22//! - **Light‑year (ly)** is derived from the exact speed of light `c = 299_792_458 m/s` and one
23//!   **Julian year** (`365.25 d`, `d = 86400 s`).
24//! - **Imperial and surveying units** follow the current international definitions (e.g. the
25//!   international inch is exactly `0.0254 m`).
26//! - **Nominal** astronomical/geodetic radii are grouped into [`nominal`] to avoid mixing them with
27//!   strictly defined units.
28//!
29//! This module aims to avoid avoidable precision loss by preferring rational expressions and exact
30//! relationships over rounded convenience factors wherever practical.
31//!
32//! ```rust
33//! use qtty_core::length::Kilometer;
34//!
35//! # #[cfg(feature = "astro")]
36//! # {
37//! use qtty_core::length::AstronomicalUnits;
38//! let au = AstronomicalUnits::new(1.0);
39//! let km = au.to::<Kilometer>();
40//! assert_eq!(km.value(), 149_597_870.7);
41//! # }
42//! ```
43//!
44//! ## All length units (default)
45//!
46//! ```rust
47//! use qtty_core::length::*;
48//!
49//! macro_rules! touch {
50//!     ($T:ty, $v:expr) => {{
51//!         let q = <$T>::new($v);
52//!         let _cloned = q;
53//!         assert!(q == q);
54//!     }};
55//! }
56//!
57//! // SI sub-meter
58//! touch!(Meters, 1.0); touch!(Decimeters, 1.0); touch!(Centimeters, 1.0);
59//! touch!(Millimeters, 1.0); touch!(Micrometers, 1.0); touch!(Nanometers, 1.0);
60//! touch!(Picometers, 1.0); touch!(Femtometers, 1.0); touch!(Attometers, 1.0);
61//! touch!(Zeptometers, 1.0); touch!(Yoctometers, 1.0);
62//! // SI super-meter
63//! touch!(Decameters, 1.0); touch!(Hectometers, 1.0); touch!(Kilometers, 1.0);
64//! touch!(Megameters, 1.0); touch!(Gigameters, 1.0); touch!(Terameters, 1.0);
65//! touch!(Petameters, 1.0); touch!(Exameters, 1.0); touch!(Zettameters, 1.0);
66//! touch!(Yottameters, 1.0);
67//! ```
68
69use crate::{Quantity, Unit};
70use qtty_derive::Unit;
71
72/// Re-export from the dimension module.
73pub use crate::dimension::Length;
74
75/// Marker trait for any [`Unit`] whose dimension is [`Length`].
76pub trait LengthUnit: Unit<Dim = Length> {}
77impl<T: Unit<Dim = Length>> LengthUnit for T {}
78
79#[cfg(feature = "astro")]
80mod astro;
81#[cfg(feature = "astro")]
82pub use astro::*;
83#[cfg(feature = "customary")]
84mod customary;
85#[cfg(feature = "customary")]
86pub use customary::*;
87#[cfg(feature = "navigation")]
88mod navigation;
89#[cfg(feature = "navigation")]
90pub use navigation::*;
91#[cfg(feature = "fundamental-physics")]
92mod fundamental_physics;
93#[cfg(feature = "fundamental-physics")]
94pub use fundamental_physics::*;
95
96// ─────────────────────────────────────────────────────────────────────────────
97// SI base unit and core helpers
98// ─────────────────────────────────────────────────────────────────────────────
99
100/// Metre (SI base unit).
101#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
102#[unit(symbol = "m", dimension = Length, ratio = 1.0)]
103pub struct Meter;
104/// A quantity measured in metres.
105pub type Meters = Quantity<Meter>;
106/// One metre.
107pub const M: Meters = Meters::new(1.0);
108
109/// Kilometre (`1000 m`).
110#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
111#[unit(symbol = "km", dimension = Length, ratio = 1_000.0)]
112pub struct Kilometer;
113/// Type alias shorthand for [`Kilometer`].
114pub type Km = Kilometer;
115/// A quantity measured in kilometres.
116pub type Kilometers = Quantity<Km>;
117/// One kilometre.
118pub const KM: Kilometers = Kilometers::new(1.0);
119
120/// Centimetre (`1e-2 m`).
121#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
122#[unit(symbol = "cm", dimension = Length, ratio = 1e-2)]
123pub struct Centimeter;
124/// Type alias shorthand for [`Centimeter`].
125pub type Cm = Centimeter;
126/// A quantity measured in centimetres.
127pub type Centimeters = Quantity<Cm>;
128/// One centimetre.
129pub const CM: Centimeters = Centimeters::new(1.0);
130
131/// Millimetre (`1e-3 m`).
132#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
133#[unit(symbol = "mm", dimension = Length, ratio = 1e-3)]
134pub struct Millimeter;
135/// Type alias shorthand for [`Millimeter`].
136pub type Mm = Millimeter;
137/// A quantity measured in millimetres.
138pub type Millimeters = Quantity<Mm>;
139/// One millimetre.
140pub const MM: Millimeters = Millimeters::new(1.0);
141
142/// Micrometre (`1e-6 m`).
143#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
144#[unit(symbol = "μm", dimension = Length, ratio = 1e-6)]
145pub struct Micrometer;
146/// Type alias shorthand for [`Micrometer`].
147pub type Um = Micrometer;
148/// A quantity measured in micrometres.
149pub type Micrometers = Quantity<Um>;
150/// One micrometre.
151pub const UM: Micrometers = Micrometers::new(1.0);
152
153/// Nanometre (`1e-9 m`).
154#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
155#[unit(symbol = "nm", dimension = Length, ratio = 1e-9)]
156pub struct Nanometer;
157/// Type alias shorthand for [`Nanometer`].
158pub type Nm = Nanometer;
159/// A quantity measured in nanometres.
160pub type Nanometers = Quantity<Nm>;
161/// One nanometre.
162pub const NM: Nanometers = Nanometers::new(1.0);
163
164/// Picometre (`1e-12 m`).
165#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
166#[unit(symbol = "pm", dimension = Length, ratio = 1e-12)]
167pub struct Picometer;
168/// A quantity measured in picometres.
169pub type Picometers = Quantity<Picometer>;
170/// One picometre.
171pub const PMETER: Picometers = Picometers::new(1.0);
172
173/// Femtometre (`1e-15 m`).
174#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
175#[unit(symbol = "fm", dimension = Length, ratio = 1e-15)]
176pub struct Femtometer;
177/// A quantity measured in femtometres.
178pub type Femtometers = Quantity<Femtometer>;
179/// One femtometre.
180pub const FM: Femtometers = Femtometers::new(1.0);
181
182/// Attometre (`1e-18 m`).
183#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
184#[unit(symbol = "am", dimension = Length, ratio = 1e-18)]
185pub struct Attometer;
186/// A quantity measured in attometres.
187pub type Attometers = Quantity<Attometer>;
188/// One attometre.
189pub const AM: Attometers = Attometers::new(1.0);
190
191/// Zeptometre (`1e-21 m`).
192#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
193#[unit(symbol = "zm", dimension = Length, ratio = 1e-21)]
194pub struct Zeptometer;
195/// A quantity measured in zeptometres.
196pub type Zeptometers = Quantity<Zeptometer>;
197/// One zeptometre.
198pub const ZMETER: Zeptometers = Zeptometers::new(1.0);
199
200/// Yoctometre (`1e-24 m`).
201#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
202#[unit(symbol = "ym", dimension = Length, ratio = 1e-24)]
203pub struct Yoctometer;
204/// A quantity measured in yoctometres.
205pub type Yoctometers = Quantity<Yoctometer>;
206/// One yoctometre.
207pub const YMETER: Yoctometers = Yoctometers::new(1.0);
208
209/// Megametre (`1e6 m`).
210#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
211#[unit(symbol = "Mm", dimension = Length, ratio = 1e6)]
212pub struct Megameter;
213/// Type alias shorthand for [`Megameter`].
214pub type MegaMeter = Megameter;
215/// A quantity measured in megametres.
216pub type Megameters = Quantity<Megameter>;
217/// One megametre.
218pub const MEGAMETER: Megameters = Megameters::new(1.0);
219
220/// Decimetre (`1e-1 m`).
221#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
222#[unit(symbol = "dm", dimension = Length, ratio = 1e-1)]
223pub struct Decimeter;
224/// A quantity measured in decimetres.
225pub type Decimeters = Quantity<Decimeter>;
226/// One decimetre.
227pub const DM: Decimeters = Decimeters::new(1.0);
228
229/// Decametre (`1e1 m`).
230#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
231#[unit(symbol = "dam", dimension = Length, ratio = 1e1)]
232pub struct Decameter;
233/// A quantity measured in decametres.
234pub type Decameters = Quantity<Decameter>;
235/// One decametre.
236pub const DAM: Decameters = Decameters::new(1.0);
237
238/// Hectometre (`1e2 m`).
239#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
240#[unit(symbol = "hm", dimension = Length, ratio = 1e2)]
241pub struct Hectometer;
242/// A quantity measured in hectometres.
243pub type Hectometers = Quantity<Hectometer>;
244/// One hectometre.
245pub const HM: Hectometers = Hectometers::new(1.0);
246
247/// Gigametre (`1e9 m`).
248#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
249#[unit(symbol = "Gm", dimension = Length, ratio = 1e9)]
250pub struct Gigameter;
251/// A quantity measured in gigametres.
252pub type Gigameters = Quantity<Gigameter>;
253/// One gigametre.
254pub const GM: Gigameters = Gigameters::new(1.0);
255
256/// Terametre (`1e12 m`).
257#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
258#[unit(symbol = "Tm", dimension = Length, ratio = 1e12)]
259pub struct Terameter;
260/// A quantity measured in terametres.
261pub type Terameters = Quantity<Terameter>;
262/// One terametre.
263pub const TM: Terameters = Terameters::new(1.0);
264
265/// Petametre (`1e15 m`).
266#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
267#[unit(symbol = "Pm", dimension = Length, ratio = 1e15)]
268pub struct Petameter;
269/// A quantity measured in petametres.
270pub type Petameters = Quantity<Petameter>;
271/// One petametre.
272pub const PM: Petameters = Petameters::new(1.0);
273
274/// Exametre (`1e18 m`).
275#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
276#[unit(symbol = "Em", dimension = Length, ratio = 1e18)]
277pub struct Exameter;
278/// A quantity measured in exametres.
279pub type Exameters = Quantity<Exameter>;
280/// One exametre.
281pub const EM: Exameters = Exameters::new(1.0);
282
283/// Zettametre (`1e21 m`).
284#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
285#[unit(symbol = "Zm", dimension = Length, ratio = 1e21)]
286pub struct Zettameter;
287/// A quantity measured in zettametres.
288pub type Zettameters = Quantity<Zettameter>;
289/// One zettametre.
290pub const ZM: Zettameters = Zettameters::new(1.0);
291
292/// Yottametre (`1e24 m`).
293#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
294#[unit(symbol = "Ym", dimension = Length, ratio = 1e24)]
295pub struct Yottameter;
296/// A quantity measured in yottametres.
297pub type Yottameters = Quantity<Yottameter>;
298/// One yottametre.
299pub const YM: Yottameters = Yottameters::new(1.0);
300
301// ─────────────────────────────────────────────────────────────────────────────
302// Imperial, US customary, and surveying units
303// ─────────────────────────────────────────────────────────────────────────────
304
305// ─────────────────────────────────────────────────────────────────────────────
306// From conversions: default (metric) units
307// ─────────────────────────────────────────────────────────────────────────────
308/// Canonical list of always-available (metric SI) length units.
309///
310/// Exported (`#[doc(hidden)]`) for use in `qtty`'s scalar alias generation and
311/// compile-time consistency checks.  Feature-gated units (astro, navigation,
312/// customary, fundamental-physics) are in their sub-modules.
313#[macro_export]
314#[doc(hidden)]
315macro_rules! length_units {
316    ($cb:path) => {
317        $cb!(
318            Meter, Decimeter, Centimeter, Millimeter, Micrometer, Nanometer, Picometer, Femtometer,
319            Attometer, Zeptometer, Yoctometer, Decameter, Hectometer, Kilometer, Megameter,
320            Gigameter, Terameter, Petameter, Exameter, Zettameter, Yottameter
321        );
322    };
323}
324
325// Generate bidirectional From impls between base metric SI length units.
326length_units!(crate::impl_unit_from_conversions);
327
328// ─────────────────────────────────────────────────────────────────────────────
329// Cross-unit ops: default (metric) units
330// ─────────────────────────────────────────────────────────────────────────────
331#[cfg(feature = "cross-unit-ops")]
332length_units!(crate::impl_unit_cross_unit_ops);
333
334// ── Per-family bridge helpers ─────────────────────────────────────────────────
335//
336// Each macro below holds ONE optional feature's length unit list and invokes
337// both the `From` and (when `cross-unit-ops` is enabled) the `PartialEq`/
338// `PartialOrd` generators.  The astro-length set is the largest family in this
339// module; defining it once here cuts its appearance count from once-per-pair
340// to one macro body.
341//
342// To add a new optional length family F:
343//   1. Define `__impl_length_bridges_with_F_as_base!` here with F's unit list.
344//   2. Add one call per existing family X (invoking whichever family's macro
345//      covers the larger set of length units).
346
347#[cfg(feature = "astro")]
348macro_rules! __impl_length_bridges_with_astro_as_base {
349    ($($extra:ty),+ $(,)?) => {
350        crate::__impl_from_each_extra_to_bases!(
351            {
352                AstronomicalUnit, LightYear, Parsec, Kiloparsec, Megaparsec, Gigaparsec,
353                nominal::SolarRadius, nominal::SolarDiameter, nominal::EarthRadius,
354                nominal::EarthEquatorialRadius, nominal::EarthPolarRadius,
355                nominal::JupiterRadius, nominal::LunarRadius, nominal::LunarDistance
356            }
357            $($extra),+
358        );
359        #[cfg(feature = "cross-unit-ops")]
360        crate::__impl_cross_ops_each_extra_to_bases!(
361            {
362                AstronomicalUnit, LightYear, Parsec, Kiloparsec, Megaparsec, Gigaparsec,
363                nominal::SolarRadius, nominal::SolarDiameter, nominal::EarthRadius,
364                nominal::EarthEquatorialRadius, nominal::EarthPolarRadius,
365                nominal::JupiterRadius, nominal::LunarRadius, nominal::LunarDistance
366            }
367            $($extra),+
368        );
369    };
370}
371
372#[cfg(feature = "navigation")]
373macro_rules! __impl_length_bridges_with_navigation_as_base {
374    ($($extra:ty),+ $(,)?) => {
375        crate::__impl_from_each_extra_to_bases!(
376            {NauticalMile, Chain, Rod, Link, Fathom, EarthMeridionalCircumference, EarthEquatorialCircumference}
377            $($extra),+
378        );
379        #[cfg(feature = "cross-unit-ops")]
380        crate::__impl_cross_ops_each_extra_to_bases!(
381            {NauticalMile, Chain, Rod, Link, Fathom, EarthMeridionalCircumference, EarthEquatorialCircumference}
382            $($extra),+
383        );
384    };
385}
386
387// ── Cross-feature From/PartialEq/PartialOrd: length families ─────────────────
388//
389// Each feature-gated submodule registers its extra units against the base metric
390// set.  The calls below close the gap so that units from *different* optional
391// features also get bidirectional `From` and cross-unit comparison impls.
392//
393// The macro of the *larger* length family is always invoked so the smaller
394// family's (shorter) unit list appears at the call site.
395
396// astro (14 length units) — largest optional length family; always the base.
397#[cfg(all(feature = "astro", feature = "customary"))]
398__impl_length_bridges_with_astro_as_base!(Inch, Foot, Yard, Mile);
399
400#[cfg(all(feature = "astro", feature = "navigation"))]
401__impl_length_bridges_with_astro_as_base!(
402    NauticalMile,
403    Chain,
404    Rod,
405    Link,
406    Fathom,
407    EarthMeridionalCircumference,
408    EarthEquatorialCircumference
409);
410
411#[cfg(all(feature = "astro", feature = "fundamental-physics"))]
412__impl_length_bridges_with_astro_as_base!(
413    BohrRadius,
414    ClassicalElectronRadius,
415    PlanckLength,
416    ElectronReducedComptonWavelength
417);
418
419// navigation (7 length units) — base for its pairs with customary (4) and fp (4).
420#[cfg(all(feature = "customary", feature = "navigation"))]
421__impl_length_bridges_with_navigation_as_base!(Inch, Foot, Yard, Mile);
422
423#[cfg(all(feature = "navigation", feature = "fundamental-physics"))]
424__impl_length_bridges_with_navigation_as_base!(
425    BohrRadius,
426    ClassicalElectronRadius,
427    PlanckLength,
428    ElectronReducedComptonWavelength
429);
430
431// customary (4) and fundamental-physics (4) are equal in size; customary is base by convention.
432#[cfg(all(feature = "customary", feature = "fundamental-physics"))]
433crate::__impl_from_each_extra_to_bases!(
434    {Inch, Foot, Yard, Mile}
435    BohrRadius, ClassicalElectronRadius, PlanckLength, ElectronReducedComptonWavelength
436);
437#[cfg(all(
438    feature = "customary",
439    feature = "fundamental-physics",
440    feature = "cross-unit-ops"
441))]
442crate::__impl_cross_ops_each_extra_to_bases!(
443    {Inch, Foot, Yard, Mile}
444    BohrRadius, ClassicalElectronRadius, PlanckLength, ElectronReducedComptonWavelength
445);
446
447// Compile-time check: every base length unit is registered as BuiltinUnit.
448#[cfg(test)]
449length_units!(crate::assert_units_are_builtin);
450
451#[cfg(all(test, feature = "std"))]
452mod tests {
453    use super::*;
454    #[cfg(feature = "astro")]
455    use crate::units::length::astro::{ARCSECONDS_PER_RADIAN, AU_IN_METERS};
456    use approx::{assert_abs_diff_eq, assert_relative_eq};
457    use proptest::prelude::*;
458
459    // ─────────────────────────────────────────────────────────────────────────────
460    // Basic conversions
461    // ─────────────────────────────────────────────────────────────────────────────
462
463    #[test]
464    fn kilometer_to_meter() {
465        let km = Kilometers::new(1.0);
466        let m = km.to::<Meter>();
467        assert_abs_diff_eq!(m.value(), 1000.0, epsilon = 1e-9);
468    }
469
470    #[test]
471    fn meter_to_kilometer() {
472        let m = Meters::new(1000.0);
473        let km = m.to::<Kilometer>();
474        assert_abs_diff_eq!(km.value(), 1.0, epsilon = 1e-12);
475    }
476
477    #[test]
478    #[cfg(feature = "astro")]
479    fn au_to_meters() {
480        let au = AstronomicalUnits::new(1.0);
481        let m = au.to::<Meter>();
482        // 1 AU = 149,597,870,700 meters (exact, IAU 2012).
483        assert_abs_diff_eq!(m.value(), AU_IN_METERS, epsilon = 1e-6);
484    }
485
486    #[test]
487    #[cfg(feature = "astro")]
488    fn au_to_kilometers() {
489        let au = AstronomicalUnits::new(1.0);
490        let km = au.to::<Kilometer>();
491        // 1 AU = 149,597,870,700 m => 149,597,870.7 km.
492        assert_relative_eq!(km.value(), 149_597_870.7, max_relative = 1e-12);
493    }
494
495    #[test]
496    #[cfg(feature = "astro")]
497    fn light_year_to_meters() {
498        let ly = LightYears::new(1.0);
499        let m = ly.to::<Meter>();
500        // 1 LY = c * 365.25 d, where d = 86400 s
501        assert_relative_eq!(m.value(), LightYear::RATIO, max_relative = 1e-12);
502    }
503
504    #[test]
505    #[cfg(feature = "astro")]
506    fn light_year_to_kilometers() {
507        let ly = LightYears::new(1.0);
508        let km = ly.to::<Kilometer>();
509        // 1 LY ≈ 9.461e12 km
510        assert_relative_eq!(km.value(), 9_460_730_472_580.000_8, max_relative = 1e-9);
511    }
512
513    // ─────────────────────────────────────────────────────────────────────────────
514    // AU <-> LY conversions
515    // ─────────────────────────────────────────────────────────────────────────────
516
517    #[test]
518    #[cfg(feature = "astro")]
519    fn au_to_light_year() {
520        let au = AstronomicalUnits::new(1.0);
521        let ly = au.to::<LightYear>();
522        // 1 AU ≈ 1.582e-5 LY
523        assert_relative_eq!(ly.value(), 1.582e-5, max_relative = 1e-3);
524    }
525
526    #[test]
527    #[cfg(feature = "astro")]
528    fn light_year_to_au() {
529        let ly = LightYears::new(1.0);
530        let au = ly.to::<AstronomicalUnit>();
531        // 1 LY ≈ 63,241 AU
532        assert_relative_eq!(au.value(), 63241.0, max_relative = 1e-3);
533    }
534
535    #[test]
536    #[cfg(feature = "astro")]
537    fn from_impl_au_to_ly() {
538        let au = 1.0 * AU;
539        let ly: LightYears = au.into();
540        assert_relative_eq!(ly.value(), 1.582e-5, max_relative = 1e-3);
541    }
542
543    #[test]
544    #[cfg(feature = "astro")]
545    fn from_impl_ly_to_au() {
546        let ly = 1.0 * LY;
547        let au: AstronomicalUnits = ly.into();
548        assert_relative_eq!(au.value(), 63241.0, max_relative = 1e-3);
549    }
550
551    // ─────────────────────────────────────────────────────────────────────────────
552    // Parsec conversions
553    // ─────────────────────────────────────────────────────────────────────────────
554
555    #[test]
556    #[cfg(feature = "astro")]
557    fn parsec_to_light_year() {
558        let pc = Parsecs::new(1.0);
559        let ly = pc.to::<LightYear>();
560        // 1 pc expressed in light-years, using the exact AU-based definition.
561        let expected = (AstronomicalUnit::RATIO * ARCSECONDS_PER_RADIAN) / LightYear::RATIO;
562        assert_relative_eq!(ly.value(), expected, max_relative = 1e-15);
563    }
564
565    #[test]
566    #[cfg(feature = "astro")]
567    fn parsec_to_au() {
568        let pc = Parsecs::new(1.0);
569        let au = pc.to::<AstronomicalUnit>();
570        // 1 pc ≈ 206,265 AU (using exact definition: 1 pc = 3.26 LY, 1 LY ≈ 63241 AU)
571        // So 1 pc ≈ 3.26 * 63241 ≈ 206,165 AU
572        assert_relative_eq!(au.value(), 3.26 * 63241.0, max_relative = 1e-2);
573    }
574
575    #[test]
576    #[cfg(feature = "astro")]
577    fn parsec_ratio_sanity() {
578        // Parsec is defined from AU: pc = au * 648000 / π
579        let lhs = Parsec::RATIO / AstronomicalUnit::RATIO;
580        let rhs = ARCSECONDS_PER_RADIAN;
581        assert_relative_eq!(lhs, rhs, max_relative = 1e-12);
582    }
583
584    // ─────────────────────────────────────────────────────────────────────────────
585    // Solar radius
586    // ─────────────────────────────────────────────────────────────────────────────
587
588    #[test]
589    #[cfg(feature = "astro")]
590    fn solar_radius_to_meters() {
591        let sr = nominal::SolarRadiuses::new(1.0);
592        let m = sr.to::<Meter>();
593        // 1 R☉ = 695,700 km = 695,700,000 m
594        assert_abs_diff_eq!(m.value(), 695_700_000.0, epsilon = 1e-3);
595    }
596
597    #[test]
598    #[cfg(feature = "astro")]
599    fn solar_radius_to_km() {
600        let sr = nominal::SolarRadiuses::new(1.0);
601        let km = sr.to::<Kilometer>();
602        // 1 R☉ = 695,700 km
603        assert_abs_diff_eq!(km.value(), 695_700.0, epsilon = 1e-6);
604    }
605
606    // ─────────────────────────────────────────────────────────────────────────────
607    // Roundtrip conversions
608    // ─────────────────────────────────────────────────────────────────────────────
609
610    #[test]
611    fn roundtrip_km_m() {
612        let original = Kilometers::new(42.5);
613        let converted = original.to::<Meter>();
614        let back = converted.to::<Kilometer>();
615        assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
616    }
617
618    #[test]
619    #[cfg(feature = "astro")]
620    fn roundtrip_au_ly() {
621        let original = AstronomicalUnits::new(10000.0);
622        let converted = original.to::<LightYear>();
623        let back = converted.to::<AstronomicalUnit>();
624        assert_relative_eq!(back.value(), original.value(), max_relative = 1e-12);
625    }
626
627    #[test]
628    #[cfg(feature = "astro")]
629    fn roundtrip_parsec_ly() {
630        let original = Parsecs::new(5.0);
631        let converted = original.to::<LightYear>();
632        let back = converted.to::<Parsec>();
633        assert_relative_eq!(back.value(), original.value(), max_relative = 1e-12);
634    }
635
636    // ─────────────────────────────────────────────────────────────────────────
637    // Exact relationship tests for new units
638    // ─────────────────────────────────────────────────────────────────────────
639
640    #[test]
641    #[cfg(feature = "customary")]
642    fn inch_to_meter_exact_ratio() {
643        let inch = Inches::new(1.0);
644        let m = inch.to::<Meter>();
645        // International inch: exactly 0.0254 m
646        assert_relative_eq!(m.value(), 0.0254, max_relative = 1e-16);
647    }
648
649    #[test]
650    #[cfg(feature = "navigation")]
651    fn nautical_mile_to_meter_exact_ratio() {
652        let nmi = NauticalMiles::new(1.0);
653        let m = nmi.to::<Meter>();
654        // International nautical mile: exactly 1852 m
655        assert_abs_diff_eq!(m.value(), 1852.0, epsilon = 1e-12);
656    }
657
658    // ─────────────────────────────────────────────────────────────────────────
659    // Roundtrip sanity for representative units
660    // ─────────────────────────────────────────────────────────────────────────
661
662    #[test]
663    #[cfg(feature = "customary")]
664    fn roundtrip_inch_meter() {
665        let original = Inches::new(123.456);
666        let converted = original.to::<Meter>();
667        let back = converted.to::<Inch>();
668        assert_relative_eq!(back.value(), original.value(), max_relative = 1e-12);
669    }
670
671    #[test]
672    #[cfg(feature = "navigation")]
673    fn roundtrip_nautical_mile_meter() {
674        let original = NauticalMiles::new(3.75);
675        let converted = original.to::<Meter>();
676        let back = converted.to::<NauticalMile>();
677        assert_relative_eq!(back.value(), original.value(), max_relative = 1e-12);
678    }
679
680    #[test]
681    #[cfg(feature = "astro")]
682    fn roundtrip_parsec_kpc() {
683        let original = Parsecs::new(12_345.0);
684        let converted = original.to::<Kiloparsec>();
685        let back = converted.to::<Parsec>();
686        assert_relative_eq!(back.value(), original.value(), max_relative = 1e-12);
687    }
688
689    // ─────────────────────────────────────────────────────────────────────────────
690    // Property-based tests
691    // ─────────────────────────────────────────────────────────────────────────────
692
693    proptest! {
694        #[test]
695        fn prop_roundtrip_km_m(k in -1e6..1e6f64) {
696            let original = Kilometers::new(k);
697            let converted = original.to::<Meter>();
698            let back = converted.to::<Kilometer>();
699            prop_assert!((back.value() - original.value()).abs() < 1e-9 * k.abs().max(1.0));
700        }
701
702        #[test]
703        fn prop_km_m_ratio(k in 1e-6..1e6f64) {
704            let km = Kilometers::new(k);
705            let m = km.to::<Meter>();
706            // 1 km = 1000 m
707            prop_assert!((m.value() / km.value() - 1000.0).abs() < 1e-9);
708        }
709
710        #[test]
711        #[cfg(feature = "astro")]
712        fn prop_roundtrip_au_km(a in 1e-6..1e6f64) {
713            let original = AstronomicalUnits::new(a);
714            let converted = original.to::<Kilometer>();
715            let back = converted.to::<AstronomicalUnit>();
716            prop_assert!((back.value() - original.value()).abs() / original.value() < 1e-12);
717        }
718
719        #[test]
720        #[cfg(feature = "customary")]
721        fn prop_roundtrip_inch_m(i in -1e6..1e6f64) {
722            let original = Inches::new(i);
723            let converted = original.to::<Meter>();
724            let back = converted.to::<Inch>();
725            let scale = i.abs().max(1.0);
726            prop_assert!((back.value() - original.value()).abs() < 1e-9 * scale);
727        }
728    }
729
730    // ─────────────────────────────────────────────────────────────────────────
731    // SI sub-meter ladder
732    // ─────────────────────────────────────────────────────────────────────────
733
734    #[test]
735    fn decimeter_to_meter() {
736        let q = Decimeters::new(10.0);
737        assert_relative_eq!(q.to::<Meter>().value(), 1.0, max_relative = 1e-15);
738    }
739
740    #[test]
741    fn centimeter_to_meter() {
742        let q = Centimeters::new(100.0);
743        assert_relative_eq!(q.to::<Meter>().value(), 1.0, max_relative = 1e-15);
744    }
745
746    #[test]
747    fn millimeter_to_centimeter() {
748        let q = Millimeters::new(10.0);
749        assert_relative_eq!(q.to::<Centimeter>().value(), 1.0, max_relative = 1e-15);
750    }
751
752    #[test]
753    fn micrometer_to_millimeter() {
754        let q = Micrometers::new(1_000.0);
755        assert_relative_eq!(q.to::<Millimeter>().value(), 1.0, max_relative = 1e-15);
756    }
757
758    #[test]
759    fn nanometer_to_micrometer() {
760        let q = Nanometers::new(1_000.0);
761        assert_relative_eq!(q.to::<Micrometer>().value(), 1.0, max_relative = 1e-15);
762    }
763
764    #[test]
765    fn picometer_to_nanometer() {
766        let q = Picometers::new(1_000.0);
767        assert_relative_eq!(q.to::<Nanometer>().value(), 1.0, max_relative = 1e-15);
768    }
769
770    #[test]
771    fn femtometer_to_picometer() {
772        let q = Femtometers::new(1_000.0);
773        assert_relative_eq!(q.to::<Picometer>().value(), 1.0, max_relative = 1e-15);
774    }
775
776    #[test]
777    fn attometer_to_femtometer() {
778        let q = Attometers::new(1_000.0);
779        assert_relative_eq!(q.to::<Femtometer>().value(), 1.0, max_relative = 1e-15);
780    }
781
782    #[test]
783    fn zeptometer_to_attometer() {
784        let q = Zeptometers::new(1_000.0);
785        assert_relative_eq!(q.to::<Attometer>().value(), 1.0, max_relative = 1e-15);
786    }
787
788    #[test]
789    fn yoctometer_to_zeptometer() {
790        let q = Yoctometers::new(1_000.0);
791        assert_relative_eq!(q.to::<Zeptometer>().value(), 1.0, max_relative = 1e-15);
792    }
793
794    // ─────────────────────────────────────────────────────────────────────────
795    // SI super-meter ladder
796    // ─────────────────────────────────────────────────────────────────────────
797
798    #[test]
799    fn decameter_to_meter() {
800        let q = Decameters::new(1.0);
801        assert_relative_eq!(q.to::<Meter>().value(), 10.0, max_relative = 1e-15);
802    }
803
804    #[test]
805    fn hectometer_to_meter() {
806        let q = Hectometers::new(1.0);
807        assert_relative_eq!(q.to::<Meter>().value(), 100.0, max_relative = 1e-15);
808    }
809
810    #[test]
811    fn megameter_to_kilometer() {
812        let q = Megameters::new(1.0);
813        assert_relative_eq!(q.to::<Kilometer>().value(), 1_000.0, max_relative = 1e-15);
814    }
815
816    #[test]
817    fn gigameter_to_megameter() {
818        let q = Gigameters::new(1.0);
819        assert_relative_eq!(q.to::<Megameter>().value(), 1_000.0, max_relative = 1e-15);
820    }
821
822    #[test]
823    fn terameter_to_gigameter() {
824        let q = Terameters::new(1.0);
825        assert_relative_eq!(q.to::<Gigameter>().value(), 1_000.0, max_relative = 1e-15);
826    }
827
828    #[test]
829    fn petameter_to_terameter() {
830        let q = Petameters::new(1.0);
831        assert_relative_eq!(q.to::<Terameter>().value(), 1_000.0, max_relative = 1e-15);
832    }
833
834    #[test]
835    fn exameter_to_petameter() {
836        let q = Exameters::new(1.0);
837        assert_relative_eq!(q.to::<Petameter>().value(), 1_000.0, max_relative = 1e-15);
838    }
839
840    #[test]
841    fn zettameter_to_exameter() {
842        let q = Zettameters::new(1.0);
843        assert_relative_eq!(q.to::<Exameter>().value(), 1_000.0, max_relative = 1e-15);
844    }
845
846    #[test]
847    fn yottameter_to_zettameter() {
848        let q = Yottameters::new(1.0);
849        assert_relative_eq!(q.to::<Zettameter>().value(), 1_000.0, max_relative = 1e-15);
850    }
851
852    // ─────────────────────────────────────────────────────────────────────────
853    // Imperial / surveying units
854    // ─────────────────────────────────────────────────────────────────────────
855
856    #[test]
857    #[cfg(feature = "customary")]
858    fn foot_to_meter() {
859        let q = Feet::new(1.0);
860        // 1 ft = 0.3048 m exactly
861        assert_relative_eq!(q.to::<Meter>().value(), 0.3048, max_relative = 1e-15);
862    }
863
864    #[test]
865    #[cfg(feature = "customary")]
866    fn yard_to_meter() {
867        let q = Yards::new(1.0);
868        // 1 yd = 0.9144 m exactly
869        assert_relative_eq!(q.to::<Meter>().value(), 0.9144, max_relative = 1e-15);
870    }
871
872    #[test]
873    #[cfg(feature = "customary")]
874    fn mile_to_kilometer() {
875        let q = Miles::new(1.0);
876        // 1 mi = 1609.344 m exactly
877        assert_relative_eq!(q.to::<Kilometer>().value(), 1.609_344, max_relative = 1e-15);
878    }
879
880    #[test]
881    #[cfg(all(feature = "navigation", feature = "customary"))]
882    fn fathom_to_foot() {
883        let q = Fathoms::new(1.0);
884        // 1 fathom = 6 ft
885        assert_relative_eq!(q.to::<Foot>().value(), 6.0, max_relative = 1e-14);
886    }
887
888    #[test]
889    #[cfg(all(feature = "navigation", feature = "customary"))]
890    fn chain_to_foot() {
891        let q = Chains::new(1.0);
892        // 1 chain = 66 ft
893        assert_relative_eq!(q.to::<Foot>().value(), 66.0, max_relative = 1e-14);
894    }
895
896    #[test]
897    #[cfg(all(feature = "navigation", feature = "customary"))]
898    fn rod_to_foot() {
899        let q = Rods::new(1.0);
900        // 1 rod = 16.5 ft
901        assert_relative_eq!(q.to::<Foot>().value(), 16.5, max_relative = 1e-14);
902    }
903
904    #[test]
905    #[cfg(all(feature = "navigation", feature = "customary"))]
906    fn link_to_foot() {
907        let q = Links::new(100.0);
908        // 100 links = 1 chain = 66 ft
909        assert_relative_eq!(q.to::<Foot>().value(), 66.0, max_relative = 1e-14);
910    }
911
912    // ─────────────────────────────────────────────────────────────────────────
913    // Larger astronomical parsec multiples
914    // ─────────────────────────────────────────────────────────────────────────
915
916    #[test]
917    #[cfg(feature = "astro")]
918    fn megaparsec_to_kiloparsec() {
919        let q = Megaparsecs::new(1.0);
920        assert_relative_eq!(q.to::<Kiloparsec>().value(), 1_000.0, max_relative = 1e-12);
921    }
922
923    #[test]
924    #[cfg(feature = "astro")]
925    fn gigaparsec_to_megaparsec() {
926        let q = Gigaparsecs::new(1.0);
927        assert_relative_eq!(q.to::<Megaparsec>().value(), 1_000.0, max_relative = 1e-12);
928    }
929
930    // ─────────────────────────────────────────────────────────────────────────
931    // Geodesy
932    // ─────────────────────────────────────────────────────────────────────────
933
934    #[test]
935    #[cfg(feature = "navigation")]
936    fn earth_meridional_circumference_to_km() {
937        let q = EarthMeridionalCircumferences::new(1.0);
938        // ≈ 40_007.863 km
939        assert_relative_eq!(q.to::<Kilometer>().value(), 40_007.863, max_relative = 1e-6);
940    }
941
942    #[test]
943    #[cfg(feature = "navigation")]
944    fn earth_equatorial_circumference_to_km() {
945        let q = EarthEquatorialCircumferences::new(1.0);
946        // ≈ 40_075.017 km
947        assert_relative_eq!(q.to::<Kilometer>().value(), 40_075.017, max_relative = 1e-6);
948    }
949
950    // ─────────────────────────────────────────────────────────────────────────
951    // Physics lengths
952    // ─────────────────────────────────────────────────────────────────────────
953
954    #[test]
955    #[cfg(feature = "fundamental-physics")]
956    fn bohr_radius_to_picometers() {
957        let q = BohrRadii::new(1.0);
958        // a0 ≈ 52.9177 pm
959        assert_relative_eq!(q.to::<Picometer>().value(), 52.917_72, max_relative = 1e-5);
960    }
961
962    #[test]
963    #[cfg(feature = "fundamental-physics")]
964    fn classical_electron_radius_to_femtometers() {
965        let q = ClassicalElectronRadii::new(1.0);
966        // re ≈ 2.81794 fm (CODATA 2022)
967        assert_relative_eq!(
968            q.to::<Femtometer>().value(),
969            2.817_940_320_5,
970            max_relative = 1e-9
971        );
972    }
973
974    #[test]
975    #[cfg(feature = "fundamental-physics")]
976    fn planck_length_ratio() {
977        // Just check ratio round-trips without numeric overflow
978        let q = PlanckLengths::new(1.0);
979        let back = q.to::<Meter>().to::<PlanckLength>();
980        assert_relative_eq!(back.value(), 1.0, max_relative = 1e-9);
981    }
982
983    #[test]
984    #[cfg(feature = "fundamental-physics")]
985    fn electron_compton_wavelength_to_femtometers() {
986        let q = ElectronReducedComptonWavelengths::new(1.0);
987        // λ̄_e ≈ 386.159 fm (CODATA 2022)
988        assert_relative_eq!(
989            q.to::<Femtometer>().value(),
990            386.159_267_44,
991            max_relative = 1e-7
992        );
993    }
994
995    // ─────────────────────────────────────────────────────────────────────────
996    // Nominal submodule
997    // ─────────────────────────────────────────────────────────────────────────
998
999    #[test]
1000    #[cfg(feature = "astro")]
1001    fn earth_radius_to_km() {
1002        let q = nominal::EarthRadii::new(1.0);
1003        assert_relative_eq!(q.to::<Kilometer>().value(), 6_371.0, max_relative = 1e-9);
1004    }
1005
1006    #[test]
1007    #[cfg(feature = "astro")]
1008    fn earth_equatorial_radius_to_km() {
1009        let q = nominal::EarthEquatorialRadii::new(1.0);
1010        assert_relative_eq!(q.to::<Kilometer>().value(), 6_378.137, max_relative = 1e-9);
1011    }
1012
1013    #[test]
1014    #[cfg(feature = "astro")]
1015    fn earth_polar_radius_to_km() {
1016        let q = nominal::EarthPolarRadii::new(1.0);
1017        assert_relative_eq!(
1018            q.to::<Kilometer>().value(),
1019            6_356.752_314_2,
1020            max_relative = 1e-9
1021        );
1022    }
1023
1024    #[test]
1025    #[cfg(feature = "astro")]
1026    fn lunar_radius_to_km() {
1027        let q = nominal::LunarRadii::new(1.0);
1028        assert_relative_eq!(q.to::<Kilometer>().value(), 1_737.4, max_relative = 1e-9);
1029    }
1030
1031    #[test]
1032    #[cfg(feature = "astro")]
1033    fn jupiter_radius_to_km() {
1034        let q = nominal::JupiterRadii::new(1.0);
1035        assert_relative_eq!(q.to::<Kilometer>().value(), 71_492.0, max_relative = 1e-9);
1036    }
1037
1038    #[test]
1039    #[cfg(feature = "astro")]
1040    fn lunar_distance_to_km() {
1041        let q = nominal::LunarDistances::new(1.0);
1042        assert_relative_eq!(q.to::<Kilometer>().value(), 384_400.0, max_relative = 1e-9);
1043    }
1044
1045    #[test]
1046    #[cfg(feature = "astro")]
1047    fn solar_diameter_to_solar_radius() {
1048        let diameters = nominal::SolarDiameters::new(1.0);
1049        let radii = diameters.to::<nominal::SolarRadius>();
1050        assert_relative_eq!(radii.value(), 2.0, max_relative = 1e-14);
1051    }
1052
1053    #[test]
1054    fn symbols_are_correct() {
1055        assert_eq!(Meter::SYMBOL, "m");
1056        assert_eq!(Kilometer::SYMBOL, "km");
1057        assert_eq!(Centimeter::SYMBOL, "cm");
1058        #[cfg(feature = "customary")]
1059        assert_eq!(Inch::SYMBOL, "in");
1060        #[cfg(feature = "astro")]
1061        assert_eq!(AstronomicalUnit::SYMBOL, "au");
1062        #[cfg(feature = "astro")]
1063        assert_eq!(Parsec::SYMBOL, "pc");
1064    }
1065}