Skip to main content

lox_core/
units.rs

1// SPDX-FileCopyrightText: 2025 Helge Eichhorn <git@helgeeichhorn.de>
2// SPDX-FileCopyrightText: 2013-2021 NumFOCUS Foundation
3//
4// SPDX-License-Identifier: MPL-2.0 AND LicenseRef-ERFA
5
6//! Newtype wrappers for unitful [`f64`] double precision values
7
8use core::{
9    f64::consts::{FRAC_PI_2, FRAC_PI_4, PI, TAU},
10    fmt::{Display, Formatter, Result},
11    ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign},
12};
13
14use glam::DMat3;
15use lox_test_utils::ApproxEq;
16
17use crate::f64::consts::SECONDS_PER_DAY;
18use crate::math::float::{
19    abs, acos, acosh, asin, asinh, atan, atan2, atanh, cos, cosh, log10, powf, round, sin, sin_cos,
20    sinh, tan, tanh, to_degrees, to_radians,
21};
22
23/// Degrees in full circle
24pub const DEGREES_IN_CIRCLE: f64 = 360.0;
25
26/// Arcseconds in full circle
27pub const ARCSECONDS_IN_CIRCLE: f64 = DEGREES_IN_CIRCLE * 60.0 * 60.0;
28
29/// Radians per arcsecond
30pub const RADIANS_IN_ARCSECOND: f64 = TAU / ARCSECONDS_IN_CIRCLE;
31
32/// Rounding granularity (arcseconds) used by the HMS/DMS decomposition
33/// methods to suppress floating-point undershoot at integer boundaries.
34/// See [`Angle::to_dms`] / [`Angle::to_hms`].
35const HMS_DMS_ROUNDING_ARCSEC: f64 = 1e-9;
36
37/// Sign of a signed component. Used for HMS/DMS decomposition.
38#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum Sign {
41    /// Positive sign (`+`).
42    Positive,
43    /// Negative sign (`-`).
44    Negative,
45}
46
47impl Sign {
48    /// Returns +1.0 for `Positive` and -1.0 for `Negative`.
49    ///
50    /// `const` so it can be called from `const fn` composition methods.
51    pub const fn as_f64(&self) -> f64 {
52        match self {
53            Sign::Positive => 1.0,
54            Sign::Negative => -1.0,
55        }
56    }
57}
58
59impl Display for Sign {
60    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
61        write!(
62            f,
63            "{}",
64            match self {
65                Sign::Positive => "+",
66                Sign::Negative => "-",
67            }
68        )
69    }
70}
71
72impl From<f64> for Sign {
73    fn from(x: f64) -> Self {
74        // `is_sign_negative()` reads the IEEE sign bit and correctly
75        // classifies -0.0 as `Negative`.
76        if x.is_sign_negative() {
77            Sign::Negative
78        } else {
79            Sign::Positive
80        }
81    }
82}
83
84impl From<Sign> for f64 {
85    fn from(s: Sign) -> f64 {
86        s.as_f64()
87    }
88}
89
90macro_rules! impl_sign_from_signed_int {
91    ($($t:ty),+ $(,)?) => {
92        $(
93            impl From<$t> for Sign {
94                fn from(x: $t) -> Self {
95                    if x < 0 { Sign::Negative } else { Sign::Positive }
96                }
97            }
98        )+
99    };
100}
101impl_sign_from_signed_int!(i8, i16, i32, i64, isize);
102
103type Radians = f64;
104
105/// Decomposes a signed arcsecond total into `(Sign, hours-or-degrees,
106/// minutes, seconds)`. Used by [`Angle::to_dms`] and [`Angle::to_hms`].
107///
108/// `unit_arcsec` is the number of arcseconds per major unit
109/// (3600.0 for degrees, 3600.0 × 15.0 = 54000.0 for hours).
110fn decompose_signed_arcseconds(total_arcsec: f64, unit_arcsec: f64) -> (Sign, u32, u8, f64) {
111    let sign = Sign::from(total_arcsec);
112    let abs_arcsec = abs(total_arcsec);
113    // Round to the nearest HMS_DMS_ROUNDING_ARCSEC to suppress floating-point
114    // undershoot at integer boundaries (e.g. 1799.999…98 → 1800.0).
115    let abs_arcsec = round(abs_arcsec / HMS_DMS_ROUNDING_ARCSEC) * HMS_DMS_ROUNDING_ARCSEC;
116    let major = (abs_arcsec / unit_arcsec) as u32;
117    let rem = abs_arcsec - (major as f64) * unit_arcsec;
118    // 60 arcseconds in an arcminute, 60 seconds in a minute.
119    let minutes = (rem / 60.0) as u8;
120    let seconds = rem - (minutes as f64) * 60.0;
121    (sign, major, minutes, seconds)
122}
123
124/// Angle in radians.
125#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127#[repr(transparent)]
128pub struct Angle(Radians);
129
130impl Angle {
131    /// An angle equal to zero.
132    pub const ZERO: Self = Self(0.0);
133    /// An angle equal to π.
134    pub const PI: Self = Self(PI);
135    /// An angle equal to τ = 2π.
136    pub const TAU: Self = Self(TAU);
137    /// An angle equal to π/2.
138    pub const FRAC_PI_2: Self = Self(FRAC_PI_2);
139    /// An angle equal to π/4.
140    pub const FRAC_PI_4: Self = Self(FRAC_PI_4);
141
142    /// Creates a new angle from an `f64` value in radians.
143    pub const fn new(rad: f64) -> Self {
144        Self(rad)
145    }
146
147    /// Creates a new angle from an `f64` value in radians.
148    pub const fn radians(rad: f64) -> Self {
149        Self(rad)
150    }
151
152    /// Creates a new angle from an `f64` value in radians and normalize the angle
153    /// to the interval [0.0, 2π).
154    pub const fn radians_normalized(rad: f64) -> Self {
155        Self(rad).mod_two_pi()
156    }
157
158    /// Creates a new angle from an `f64` value in radians and normalize the angle
159    /// to the interval (-2π, 2π).
160    pub const fn radians_normalized_signed(rad: f64) -> Self {
161        Self(rad).mod_two_pi_signed()
162    }
163
164    /// Creates a new angle from an `f64` value in degrees.
165    pub const fn degrees(deg: f64) -> Self {
166        Self(to_radians(deg))
167    }
168
169    /// Creates a new angle from sign, hours, minutes, and seconds (HMS notation).
170    ///
171    /// All magnitude components are non-negative; the sign is carried by the
172    /// `Sign` argument. This shape lets the function represent angles in
173    /// `(−1h, 0h)` (e.g. `−0h 30m 0s`) that an `i64 hours` argument cannot.
174    ///
175    /// Inverse of [`Angle::to_hms`]; round-trip is perfect.
176    ///
177    /// # References
178    ///
179    /// - ERFA [`tf2a`](https://github.com/liberfa/erfa/blob/master/src/tf2a.c)
180    pub const fn from_hms(sign: Sign, hours: u32, minutes: u8, seconds: f64) -> Self {
181        let mag = 15.0 * (hours as f64 + minutes as f64 / 60.0 + seconds / 3600.0);
182        Self::degrees(sign.as_f64() * mag)
183    }
184
185    /// Creates a new angle from sign, degrees, arcminutes, and arcseconds.
186    ///
187    /// All magnitude components are non-negative; the sign is carried by the
188    /// `Sign` argument. Inverse of [`Angle::to_dms`].
189    ///
190    /// # References
191    ///
192    /// - ERFA [`af2a`](https://github.com/liberfa/erfa/blob/master/src/af2a.c)
193    pub const fn from_dms(sign: Sign, degrees: u32, minutes: u8, seconds: f64) -> Self {
194        let mag = degrees as f64 + minutes as f64 / 60.0 + seconds / 3600.0;
195        Self::degrees(sign.as_f64() * mag)
196    }
197
198    /// Creates a new angle from an `f64` value in degrees and normalize the angle
199    /// to the interval [0.0, 2π).
200    pub const fn degrees_normalized(deg: f64) -> Self {
201        Self(to_radians(deg % DEGREES_IN_CIRCLE)).mod_two_pi()
202    }
203
204    /// Creates a new angle from an `f64` value in degrees and normalize the angle
205    /// to the interval (-2π, 2π).
206    pub const fn degrees_normalized_signed(deg: f64) -> Self {
207        Self(to_radians(deg % DEGREES_IN_CIRCLE))
208    }
209
210    /// Creates a new angle from an `f64` value in arcseconds.
211    pub const fn arcseconds(asec: f64) -> Self {
212        Self(asec * RADIANS_IN_ARCSECOND)
213    }
214
215    /// Creates a new angle from an `f64` value in arcseconds and normalize the angle
216    /// to the interval [0.0, 2π).
217    pub const fn arcseconds_normalized(asec: f64) -> Self {
218        Self((asec % ARCSECONDS_IN_CIRCLE) * RADIANS_IN_ARCSECOND).mod_two_pi()
219    }
220
221    /// Creates a new angle from an `f64` value in arcseconds and normalize the angle
222    /// to the interval (-2π, 2π).
223    pub const fn arcseconds_normalized_signed(asec: f64) -> Self {
224        Self((asec % ARCSECONDS_IN_CIRCLE) * RADIANS_IN_ARCSECOND)
225    }
226
227    /// Returns `true` if the angle is exactly zero.
228    pub fn is_zero(&self) -> bool {
229        self.0 == 0.0
230    }
231
232    /// Returns the absolute value of the angle.
233    pub fn abs(&self) -> Self {
234        Self(abs(self.0))
235    }
236
237    /// Creates an angle from the arcsine of a value.
238    pub fn from_asin(value: f64) -> Self {
239        Self(asin(value))
240    }
241
242    /// Creates an angle from the inverse hyperbolic sine of a value.
243    pub fn from_asinh(value: f64) -> Self {
244        Self(asinh(value))
245    }
246
247    /// Creates an angle from the arccosine of a value.
248    pub fn from_acos(value: f64) -> Self {
249        Self(acos(value))
250    }
251
252    /// Creates an angle from the inverse hyperbolic cosine of a value.
253    pub fn from_acosh(value: f64) -> Self {
254        Self(acosh(value))
255    }
256
257    /// Creates an angle from the arctangent of a value.
258    pub fn from_atan(value: f64) -> Self {
259        Self(atan(value))
260    }
261
262    /// Creates an angle from the inverse hyperbolic tangent of a value.
263    pub fn from_atanh(value: f64) -> Self {
264        Self(atanh(value))
265    }
266
267    /// Creates an angle from the two-argument arctangent of `y` and `x`.
268    pub fn from_atan2(y: f64, x: f64) -> Self {
269        Self(atan2(y, x))
270    }
271
272    /// Returns the cosine of the angle.
273    pub fn cos(&self) -> f64 {
274        cos(self.0)
275    }
276
277    /// Returns the hyperbolic cosine of the angle.
278    pub fn cosh(&self) -> f64 {
279        cosh(self.0)
280    }
281
282    /// Returns the sine of the angle.
283    pub fn sin(&self) -> f64 {
284        sin(self.0)
285    }
286
287    /// Returns the hyperbolic sine of the angle.
288    pub fn sinh(&self) -> f64 {
289        sinh(self.0)
290    }
291
292    /// Returns sine and cosine of the angle.
293    pub fn sin_cos(&self) -> (f64, f64) {
294        sin_cos(self.0)
295    }
296
297    /// Returns the tangent of the angle.
298    pub fn tan(&self) -> f64 {
299        tan(self.0)
300    }
301
302    /// Returns the hyperbolic tangent of the angle.
303    pub fn tanh(&self) -> f64 {
304        tanh(self.0)
305    }
306
307    /// Returns a new angle that is normalized to the interval [0.0, 2π).
308    pub const fn mod_two_pi(&self) -> Self {
309        let mut a = self.0 % TAU;
310        if a < 0.0 {
311            a += TAU
312        }
313        Self(a)
314    }
315
316    /// Returns a new angle that is normalized to the interval (-2π, 2π).
317    pub const fn mod_two_pi_signed(&self) -> Self {
318        Self(self.0 % TAU)
319    }
320
321    /// Returns a new angle that is normalized to a (-π, π) interval
322    /// centered around `center`.
323    pub const fn normalize_two_pi(&self, center: Self) -> Self {
324        // Inline const-compatible floor (f64::floor isn't const in no_std).
325        // The quotient is bounded by the angle range so the `as i64` cast is safe.
326        let q = (self.0 + PI - center.0) / TAU;
327        let i = q as i64 as f64;
328        let floor_q = if q < i { i - 1.0 } else { i };
329        Self(self.0 - TAU * floor_q)
330    }
331
332    /// Returns the value of the angle in radians.
333    pub const fn as_f64(&self) -> f64 {
334        self.0
335    }
336
337    /// Returns the value of the angle in radians.
338    pub const fn to_radians(&self) -> f64 {
339        self.0
340    }
341
342    /// Returns the value of the angle in degrees.
343    pub const fn to_degrees(&self) -> f64 {
344        to_degrees(self.0)
345    }
346
347    /// Returns the value of the angle in arcseconds.
348    pub const fn to_arcseconds(&self) -> f64 {
349        self.0 / RADIANS_IN_ARCSECOND
350    }
351
352    /// Returns the 3×3 rotation matrix for a rotation about the X axis.
353    pub fn rotation_x(&self) -> DMat3 {
354        DMat3::from_rotation_x(-self.to_radians())
355    }
356
357    /// Returns the 3×3 rotation matrix for a rotation about the Y axis.
358    pub fn rotation_y(&self) -> DMat3 {
359        DMat3::from_rotation_y(-self.to_radians())
360    }
361
362    /// Returns the 3×3 rotation matrix for a rotation about the Z axis.
363    pub fn rotation_z(&self) -> DMat3 {
364        DMat3::from_rotation_z(-self.to_radians())
365    }
366
367    /// Decomposes the angle into (sign, hours, minutes, seconds).
368    ///
369    /// Treats the angle as a clock-time hour angle (15° per hour). The
370    /// returned `hours` and `minutes` are non-negative; `seconds` is in
371    /// `[0.0, 60.0)`.
372    ///
373    /// Inverse of [`Angle::from_hms`]; round-trip is perfect.
374    ///
375    /// # References
376    ///
377    /// - ERFA [`a2tf`](https://github.com/liberfa/erfa/blob/master/src/a2tf.c)
378    pub fn to_hms(&self) -> (Sign, u32, u8, f64) {
379        // 1 hour-of-angle = 15° = 15 × 3600 arcseconds.
380        // Divide arcseconds by 15 so `decompose_signed_arcseconds` treats
381        // 3600 units-per-major as 3600 time-seconds-per-hour.
382        decompose_signed_arcseconds(self.to_arcseconds() / 15.0, 3600.0)
383    }
384
385    /// Decomposes the angle into (sign, degrees, arcminutes, arcseconds).
386    ///
387    /// The returned `degrees` and `arcminutes` are non-negative; `arcseconds`
388    /// is in `[0.0, 60.0)`. Arcseconds are rounded to the nearest 1e-9
389    /// arcsecond to suppress floating-point undershoot at integer boundaries.
390    ///
391    /// Inverse of [`Angle::from_dms`]; round-trip is perfect.
392    ///
393    /// # References
394    ///
395    /// - ERFA [`a2af`](https://github.com/liberfa/erfa/blob/master/src/a2af.c)
396    pub fn to_dms(&self) -> (Sign, u32, u8, f64) {
397        decompose_signed_arcseconds(self.to_arcseconds(), 3600.0)
398    }
399}
400
401impl Display for Angle {
402    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
403        to_degrees(self.0).fmt(f)?;
404        write!(f, " deg")
405    }
406}
407
408/// A trait for creating [`Angle`] instances from primitives.
409///
410/// By default it is implemented for [`f64`] and [`i64`].
411///
412/// # Examples
413///
414/// ```
415/// use lox_core::units::AngleUnits;
416///
417/// let angle = 360.deg();
418/// assert_eq!(angle.to_radians(), std::f64::consts::TAU);
419/// ```
420pub trait AngleUnits {
421    /// Creates an angle from a value in radians.
422    fn rad(&self) -> Angle;
423    /// Creates an angle from a value in degrees.
424    fn deg(&self) -> Angle;
425    /// Creates an angle from a value in arcseconds.
426    fn arcsec(&self) -> Angle;
427    /// Creates an angle from a value in milliarcseconds.
428    fn mas(&self) -> Angle;
429    /// Creates an angle from a value in microarcseconds.
430    fn uas(&self) -> Angle;
431}
432
433impl AngleUnits for f64 {
434    fn rad(&self) -> Angle {
435        Angle::radians(*self)
436    }
437
438    fn deg(&self) -> Angle {
439        Angle::degrees(*self)
440    }
441
442    fn arcsec(&self) -> Angle {
443        Angle::arcseconds(*self)
444    }
445
446    fn mas(&self) -> Angle {
447        Angle::arcseconds(self * 1e-3)
448    }
449
450    fn uas(&self) -> Angle {
451        Angle::arcseconds(self * 1e-6)
452    }
453}
454
455impl AngleUnits for i64 {
456    fn rad(&self) -> Angle {
457        Angle::radians(*self as f64)
458    }
459
460    fn deg(&self) -> Angle {
461        Angle::degrees(*self as f64)
462    }
463
464    fn arcsec(&self) -> Angle {
465        Angle::arcseconds(*self as f64)
466    }
467
468    fn mas(&self) -> Angle {
469        Angle::arcseconds(*self as f64 * 1e-3)
470    }
471
472    fn uas(&self) -> Angle {
473        Angle::arcseconds(*self as f64 * 1e-6)
474    }
475}
476
477/// The astronomical unit in meters.
478pub const ASTRONOMICAL_UNIT: f64 = 1.495978707e11;
479
480type Meters = f64;
481
482/// Distance in meters.
483#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
484#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
485#[repr(transparent)]
486pub struct Distance(Meters);
487
488impl Distance {
489    /// Create a new distance from an `f64` value in meters.
490    pub const fn new(m: f64) -> Self {
491        Self(m)
492    }
493
494    /// Create a new distance from an `f64` value in meters.
495    pub const fn meters(m: f64) -> Self {
496        Self(m)
497    }
498
499    /// Create a new distance from an `f64` value in kilometers.
500    pub const fn kilometers(m: f64) -> Self {
501        Self(m * 1e3)
502    }
503
504    /// Create a new distance from an `f64` value in astronomical units.
505    pub const fn astronomical_units(au: f64) -> Self {
506        Self(au * ASTRONOMICAL_UNIT)
507    }
508
509    /// Returns the value of the distance in meters as an `f64`.
510    pub const fn as_f64(&self) -> f64 {
511        self.0
512    }
513
514    /// Returns the value of the distance in meters.
515    pub const fn to_meters(&self) -> f64 {
516        self.0
517    }
518
519    /// Returns the value of the distance in kilometers.
520    pub const fn to_kilometers(&self) -> f64 {
521        self.0 * 1e-3
522    }
523
524    /// Returns the value of the distance in astronomical units.
525    pub const fn to_astronomical_units(&self) -> f64 {
526        self.0 / ASTRONOMICAL_UNIT
527    }
528}
529
530impl Display for Distance {
531    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
532        (1e-3 * self.0).fmt(f)?;
533        write!(f, " km")
534    }
535}
536
537/// A trait for creating [`Distance`] instances from primitives.
538///
539/// By default it is implemented for [`f64`] and [`i64`].
540///
541/// # Examples
542///
543/// ```
544/// use lox_core::units::DistanceUnits;
545///
546/// let d = 1.km();
547/// assert_eq!(d.to_meters(), 1e3);
548/// ```
549pub trait DistanceUnits {
550    /// Creates a distance from a value in meters.
551    fn m(&self) -> Distance;
552    /// Creates a distance from a value in kilometers.
553    fn km(&self) -> Distance;
554    /// Creates a distance from a value in astronomical units.
555    fn au(&self) -> Distance;
556}
557
558impl DistanceUnits for f64 {
559    fn m(&self) -> Distance {
560        Distance::meters(*self)
561    }
562
563    fn km(&self) -> Distance {
564        Distance::kilometers(*self)
565    }
566
567    fn au(&self) -> Distance {
568        Distance::astronomical_units(*self)
569    }
570}
571
572impl DistanceUnits for i64 {
573    fn m(&self) -> Distance {
574        Distance::meters(*self as f64)
575    }
576
577    fn km(&self) -> Distance {
578        Distance::kilometers(*self as f64)
579    }
580
581    fn au(&self) -> Distance {
582        Distance::astronomical_units(*self as f64)
583    }
584}
585
586type MetersPerSecond = f64;
587
588/// Velocity in meters per second.
589#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
590#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
591#[repr(transparent)]
592pub struct Velocity(MetersPerSecond);
593
594impl Velocity {
595    /// Creates a new velocity from an `f64` value in m/s.
596    pub const fn new(mps: f64) -> Self {
597        Self(mps)
598    }
599
600    /// Creates a new velocity from an `f64` value in m/s.
601    pub const fn meters_per_second(mps: f64) -> Self {
602        Self(mps)
603    }
604
605    /// Creates a new velocity from an `f64` value in km/s.
606    pub const fn kilometers_per_second(mps: f64) -> Self {
607        Self(mps * 1e3)
608    }
609
610    /// Creates a new velocity from an `f64` value in au/d.
611    pub const fn astronomical_units_per_day(aud: f64) -> Self {
612        Self(aud * ASTRONOMICAL_UNIT / SECONDS_PER_DAY)
613    }
614
615    /// Creates a new velocity from an `f64` value in 1/c.
616    pub const fn fraction_of_speed_of_light(c: f64) -> Self {
617        Self(c * SPEED_OF_LIGHT)
618    }
619
620    /// Returns the value of the velocity in m/s as an `f64`.
621    pub const fn as_f64(&self) -> f64 {
622        self.0
623    }
624
625    /// Returns the value of the velocity in m/s.
626    pub const fn to_meters_per_second(&self) -> f64 {
627        self.0
628    }
629
630    /// Returns the value of the velocity in km/s.
631    pub const fn to_kilometers_per_second(&self) -> f64 {
632        self.0 * 1e-3
633    }
634
635    /// Returns the value of the velocity in au/d.
636    pub const fn to_astronomical_units_per_day(&self) -> f64 {
637        self.0 * SECONDS_PER_DAY / ASTRONOMICAL_UNIT
638    }
639
640    /// Returns the value of the velocity in 1/c.
641    pub const fn to_fraction_of_speed_of_light(&self) -> f64 {
642        self.0 / SPEED_OF_LIGHT
643    }
644}
645
646impl Display for Velocity {
647    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
648        (1e-3 * self.0).fmt(f)?;
649        write!(f, " km/s")
650    }
651}
652
653/// A trait for creating [`Velocity`] instances from primitives.
654///
655/// By default it is implemented for [`f64`] and [`i64`].
656///
657/// # Examples
658///
659/// ```
660/// use lox_core::units::VelocityUnits;
661///
662/// let v = 1.kps();
663/// assert_eq!(v.to_meters_per_second(), 1e3);
664/// ```
665pub trait VelocityUnits {
666    /// Creates a velocity from a value in m/s.
667    fn mps(&self) -> Velocity;
668    /// Creates a velocity from a value in km/s.
669    fn kps(&self) -> Velocity;
670    /// Creates a velocity from a value in au/d.
671    fn aud(&self) -> Velocity;
672    /// Crates a velocity from a value in 1/c (fraction of the speed of light).
673    fn c(&self) -> Velocity;
674}
675
676impl VelocityUnits for f64 {
677    fn mps(&self) -> Velocity {
678        Velocity::meters_per_second(*self)
679    }
680
681    fn kps(&self) -> Velocity {
682        Velocity::kilometers_per_second(*self)
683    }
684
685    fn aud(&self) -> Velocity {
686        Velocity::astronomical_units_per_day(*self)
687    }
688
689    fn c(&self) -> Velocity {
690        Velocity::fraction_of_speed_of_light(*self)
691    }
692}
693
694impl VelocityUnits for i64 {
695    fn mps(&self) -> Velocity {
696        Velocity::meters_per_second(*self as f64)
697    }
698
699    fn kps(&self) -> Velocity {
700        Velocity::kilometers_per_second(*self as f64)
701    }
702
703    fn aud(&self) -> Velocity {
704        Velocity::astronomical_units_per_day(*self as f64)
705    }
706
707    fn c(&self) -> Velocity {
708        Velocity::fraction_of_speed_of_light(*self as f64)
709    }
710}
711
712/// The speed of light in vacuum in m/s.
713pub const SPEED_OF_LIGHT: f64 = 299792458.0;
714
715/// IEEE letter codes for frequency bands commonly used for satellite communications.
716#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
717#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
718pub enum FrequencyBand {
719    /// HF (High Frequency) – 3 to 30 MHz
720    HF,
721    /// VHF (Very High Frequency) – 30 to 300 MHz
722    VHF,
723    /// UHF (Ultra-High Frequency) – 0.3 to 1 GHz
724    UHF,
725    /// L – 1 to 2 GHz
726    L,
727    /// S – 2 to 4 GHz
728    S,
729    /// C – 4 to 8 GHz
730    C,
731    /// X – 8 to 12 GHz
732    X,
733    /// Kᵤ – 12 to 18 GHz
734    Ku,
735    /// K – 18 to 27 GHz
736    K,
737    /// Kₐ – 27 to 40 GHz
738    Ka,
739    /// V – 40 to 75 GHz
740    V,
741    /// W – 75 to 110 GHz
742    W,
743    /// G – 110 to 300 GHz
744    G,
745}
746
747type Hertz = f64;
748
749/// Frequency in Hertz
750#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
751#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
752#[repr(transparent)]
753pub struct Frequency(Hertz);
754
755impl Frequency {
756    /// Creates a new frequency from an `f64` value in Hz.
757    pub const fn new(hz: Hertz) -> Self {
758        Self(hz)
759    }
760
761    /// Creates a new frequency from an `f64` value in Hz.
762    pub const fn hertz(hz: Hertz) -> Self {
763        Self(hz)
764    }
765
766    /// Creates a new frequency from an `f64` value in KHz.
767    pub const fn kilohertz(hz: Hertz) -> Self {
768        Self(hz * 1e3)
769    }
770
771    /// Creates a new frequency from an `f64` value in MHz.
772    pub const fn megahertz(hz: Hertz) -> Self {
773        Self(hz * 1e6)
774    }
775
776    /// Creates a new frequency from an `f64` value in GHz.
777    pub const fn gigahertz(hz: Hertz) -> Self {
778        Self(hz * 1e9)
779    }
780
781    /// Creates a new frequency from an `f64` value in THz.
782    pub const fn terahertz(hz: Hertz) -> Self {
783        Self(hz * 1e12)
784    }
785
786    /// Returns the value of the frequency in Hz.
787    pub const fn to_hertz(&self) -> f64 {
788        self.0
789    }
790
791    /// Returns the value of the frequency in KHz.
792    pub const fn to_kilohertz(&self) -> f64 {
793        self.0 * 1e-3
794    }
795
796    /// Returns the value of the frequency in MHz.
797    pub const fn to_megahertz(&self) -> f64 {
798        self.0 * 1e-6
799    }
800
801    /// Returns the value of the frequency in GHz.
802    pub const fn to_gigahertz(&self) -> f64 {
803        self.0 * 1e-9
804    }
805
806    /// Returns the value of the frequency in THz.
807    pub const fn to_terahertz(&self) -> f64 {
808        self.0 * 1e-12
809    }
810
811    /// Returns the wavelength.
812    pub fn wavelength(&self) -> Distance {
813        Distance(SPEED_OF_LIGHT / self.0)
814    }
815
816    /// Returns the IEEE letter code if the frequency matches one of the bands.
817    pub fn band(&self) -> Option<FrequencyBand> {
818        match self.0 {
819            f if f < 3e6 => None,
820            f if f < 30e6 => Some(FrequencyBand::HF),
821            f if f < 300e6 => Some(FrequencyBand::VHF),
822            f if f < 1e9 => Some(FrequencyBand::UHF),
823            f if f < 2e9 => Some(FrequencyBand::L),
824            f if f < 4e9 => Some(FrequencyBand::S),
825            f if f < 8e9 => Some(FrequencyBand::C),
826            f if f < 12e9 => Some(FrequencyBand::X),
827            f if f < 18e9 => Some(FrequencyBand::Ku),
828            f if f < 27e9 => Some(FrequencyBand::K),
829            f if f < 40e9 => Some(FrequencyBand::Ka),
830            f if f < 75e9 => Some(FrequencyBand::V),
831            f if f < 110e9 => Some(FrequencyBand::W),
832            f if f < 300e9 => Some(FrequencyBand::G),
833            _ => None,
834        }
835    }
836}
837
838impl Display for Frequency {
839    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
840        (1e-9 * self.0).fmt(f)?;
841        write!(f, " GHz")
842    }
843}
844
845/// A trait for creating [`Frequency`] instances from primitives.
846///
847/// By default it is implemented for [`f64`] and [`i64`].
848///
849/// # Examples
850///
851/// ```
852/// use lox_core::units::FrequencyUnits;
853///
854/// let f = 1.ghz();
855/// assert_eq!(f.to_hertz(), 1e9);
856/// ```
857pub trait FrequencyUnits {
858    /// Creates a frequency from a value in Hz.
859    fn hz(&self) -> Frequency;
860    /// Creates a frequency from a value in KHz.
861    fn khz(&self) -> Frequency;
862    /// Creates a frequency from a value in MHz.
863    fn mhz(&self) -> Frequency;
864    /// Creates a frequency from a value in GHz.
865    fn ghz(&self) -> Frequency;
866    /// Creates a frequency from a value in THz.
867    fn thz(&self) -> Frequency;
868}
869
870impl FrequencyUnits for f64 {
871    fn hz(&self) -> Frequency {
872        Frequency::hertz(*self)
873    }
874
875    fn khz(&self) -> Frequency {
876        Frequency::kilohertz(*self)
877    }
878
879    fn mhz(&self) -> Frequency {
880        Frequency::megahertz(*self)
881    }
882
883    fn ghz(&self) -> Frequency {
884        Frequency::gigahertz(*self)
885    }
886
887    fn thz(&self) -> Frequency {
888        Frequency::terahertz(*self)
889    }
890}
891
892impl FrequencyUnits for i64 {
893    fn hz(&self) -> Frequency {
894        Frequency::hertz(*self as f64)
895    }
896
897    fn khz(&self) -> Frequency {
898        Frequency::kilohertz(*self as f64)
899    }
900
901    fn mhz(&self) -> Frequency {
902        Frequency::megahertz(*self as f64)
903    }
904
905    fn ghz(&self) -> Frequency {
906        Frequency::gigahertz(*self as f64)
907    }
908
909    fn thz(&self) -> Frequency {
910        Frequency::terahertz(*self as f64)
911    }
912}
913
914type Kilograms = f64;
915
916/// Mass in kilograms.
917#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
918#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
919#[repr(transparent)]
920pub struct Mass(Kilograms);
921
922impl Mass {
923    /// Creates a new mass from an `f64` value in kilograms.
924    pub const fn new(kg: f64) -> Self {
925        Self(kg)
926    }
927
928    /// Creates a new mass from an `f64` value in kilograms.
929    pub const fn kilograms(kg: f64) -> Self {
930        Self(kg)
931    }
932
933    /// Creates a new mass from an `f64` value in grams.
934    pub const fn grams(g: f64) -> Self {
935        Self(g * 1e-3)
936    }
937
938    /// Creates a new mass from an `f64` value in metric tons (1000 kg).
939    pub const fn metric_tons(t: f64) -> Self {
940        Self(t * 1e3)
941    }
942
943    /// Returns the value of the mass in kilograms as an `f64`.
944    pub const fn as_f64(&self) -> f64 {
945        self.0
946    }
947
948    /// Returns the value of the mass in kilograms.
949    pub const fn to_kilograms(&self) -> f64 {
950        self.0
951    }
952
953    /// Returns the value of the mass in grams.
954    pub const fn to_grams(&self) -> f64 {
955        self.0 * 1e3
956    }
957
958    /// Returns the value of the mass in metric tons.
959    pub const fn to_metric_tons(&self) -> f64 {
960        self.0 * 1e-3
961    }
962}
963
964impl Display for Mass {
965    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
966        self.0.fmt(f)?;
967        write!(f, " kg")
968    }
969}
970
971/// A trait for creating [`Mass`] instances from primitives.
972///
973/// By default it is implemented for [`f64`] and [`i64`].
974///
975/// # Examples
976///
977/// ```
978/// use lox_core::units::MassUnits;
979///
980/// let m = 1.kg();
981/// assert_eq!(m.to_kilograms(), 1.0);
982/// ```
983pub trait MassUnits {
984    /// Creates a mass from a value in kilograms.
985    fn kg(&self) -> Mass;
986    /// Creates a mass from a value in grams.
987    fn g(&self) -> Mass;
988    /// Creates a mass from a value in metric tons.
989    fn t(&self) -> Mass;
990}
991
992impl MassUnits for f64 {
993    fn kg(&self) -> Mass {
994        Mass::kilograms(*self)
995    }
996
997    fn g(&self) -> Mass {
998        Mass::grams(*self)
999    }
1000
1001    fn t(&self) -> Mass {
1002        Mass::metric_tons(*self)
1003    }
1004}
1005
1006impl MassUnits for i64 {
1007    fn kg(&self) -> Mass {
1008        Mass::kilograms(*self as f64)
1009    }
1010
1011    fn g(&self) -> Mass {
1012        Mass::grams(*self as f64)
1013    }
1014
1015    fn t(&self) -> Mass {
1016        Mass::metric_tons(*self as f64)
1017    }
1018}
1019
1020type SquareMeters = f64;
1021
1022/// Area in square meters.
1023#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
1024#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1025#[repr(transparent)]
1026pub struct Area(SquareMeters);
1027
1028impl Area {
1029    /// Creates a new area from an `f64` value in square meters.
1030    pub const fn new(m2: f64) -> Self {
1031        Self(m2)
1032    }
1033
1034    /// Creates a new area from an `f64` value in square meters.
1035    pub const fn square_meters(m2: f64) -> Self {
1036        Self(m2)
1037    }
1038
1039    /// Creates a new area from an `f64` value in square kilometers.
1040    pub const fn square_kilometers(km2: f64) -> Self {
1041        Self(km2 * 1e6)
1042    }
1043
1044    /// Returns the value of the area in square meters as an `f64`.
1045    pub const fn as_f64(&self) -> f64 {
1046        self.0
1047    }
1048
1049    /// Returns the value of the area in square meters.
1050    pub const fn to_square_meters(&self) -> f64 {
1051        self.0
1052    }
1053
1054    /// Returns the value of the area in square kilometers.
1055    pub const fn to_square_kilometers(&self) -> f64 {
1056        self.0 * 1e-6
1057    }
1058}
1059
1060impl Display for Area {
1061    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
1062        self.0.fmt(f)?;
1063        write!(f, " m²")
1064    }
1065}
1066
1067/// A trait for creating [`Area`] instances from primitives.
1068///
1069/// By default it is implemented for [`f64`] and [`i64`].
1070///
1071/// # Examples
1072///
1073/// ```
1074/// use lox_core::units::AreaUnits;
1075///
1076/// let a = 4.m2();
1077/// assert_eq!(a.to_square_meters(), 4.0);
1078/// ```
1079pub trait AreaUnits {
1080    /// Creates an area from a value in square meters.
1081    fn m2(&self) -> Area;
1082    /// Creates an area from a value in square kilometers.
1083    fn km2(&self) -> Area;
1084}
1085
1086impl AreaUnits for f64 {
1087    fn m2(&self) -> Area {
1088        Area::square_meters(*self)
1089    }
1090
1091    fn km2(&self) -> Area {
1092        Area::square_kilometers(*self)
1093    }
1094}
1095
1096impl AreaUnits for i64 {
1097    fn m2(&self) -> Area {
1098        Area::square_meters(*self as f64)
1099    }
1100
1101    fn km2(&self) -> Area {
1102        Area::square_kilometers(*self as f64)
1103    }
1104}
1105
1106type SquareMetersPerKilogram = f64;
1107
1108/// Area-to-mass ratio in m²/kg.
1109///
1110/// Used for ballistic and radiation-pressure coefficients (e.g. the OMM
1111/// `BTERM` and `AGOM` fields in CCSDS 502.0-B-3).
1112#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
1113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1114#[repr(transparent)]
1115pub struct AreaToMass(SquareMetersPerKilogram);
1116
1117impl AreaToMass {
1118    /// Creates a new area-to-mass ratio from an `f64` value in m²/kg.
1119    pub const fn new(m2_per_kg: f64) -> Self {
1120        Self(m2_per_kg)
1121    }
1122
1123    /// Creates a new area-to-mass ratio from an `f64` value in m²/kg.
1124    pub const fn square_meters_per_kilogram(m2_per_kg: f64) -> Self {
1125        Self(m2_per_kg)
1126    }
1127
1128    /// Returns the value in m²/kg as an `f64`.
1129    pub const fn as_f64(&self) -> f64 {
1130        self.0
1131    }
1132
1133    /// Returns the value in m²/kg.
1134    pub const fn to_square_meters_per_kilogram(&self) -> f64 {
1135        self.0
1136    }
1137}
1138
1139impl Display for AreaToMass {
1140    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
1141        self.0.fmt(f)?;
1142        write!(f, " m²/kg")
1143    }
1144}
1145
1146/// A trait for creating [`AreaToMass`] instances from primitives.
1147///
1148/// By default it is implemented for [`f64`] and [`i64`].
1149///
1150/// # Examples
1151///
1152/// ```
1153/// use lox_core::units::AreaToMassUnits;
1154///
1155/// let r = 0.05.m2_per_kg();
1156/// assert_eq!(r.to_square_meters_per_kilogram(), 0.05);
1157/// ```
1158pub trait AreaToMassUnits {
1159    /// Creates an area-to-mass ratio from a value in m²/kg.
1160    fn m2_per_kg(&self) -> AreaToMass;
1161}
1162
1163impl AreaToMassUnits for f64 {
1164    fn m2_per_kg(&self) -> AreaToMass {
1165        AreaToMass::square_meters_per_kilogram(*self)
1166    }
1167}
1168
1169impl AreaToMassUnits for i64 {
1170    fn m2_per_kg(&self) -> AreaToMass {
1171        AreaToMass::square_meters_per_kilogram(*self as f64)
1172    }
1173}
1174
1175/// Temperature in Kelvin (deprecated type alias, use [`Temperature`] instead).
1176pub type Kelvin = f64;
1177
1178type KelvinValue = f64;
1179
1180/// Temperature in Kelvin.
1181#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
1182#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1183#[repr(transparent)]
1184pub struct Temperature(KelvinValue);
1185
1186impl Temperature {
1187    /// Creates a new temperature from an `f64` value in Kelvin.
1188    pub const fn new(k: f64) -> Self {
1189        Self(k)
1190    }
1191
1192    /// Creates a new temperature from an `f64` value in Kelvin.
1193    pub const fn kelvin(k: f64) -> Self {
1194        Self(k)
1195    }
1196
1197    /// Returns the value in Kelvin as an `f64`.
1198    pub const fn as_f64(&self) -> f64 {
1199        self.0
1200    }
1201
1202    /// Returns the value in Kelvin.
1203    pub const fn to_kelvin(&self) -> f64 {
1204        self.0
1205    }
1206}
1207
1208impl Display for Temperature {
1209    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
1210        self.0.fmt(f)?;
1211        write!(f, " K")
1212    }
1213}
1214
1215type Pascals = f64;
1216
1217/// Pressure in pascals.
1218#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
1219#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1220#[repr(transparent)]
1221pub struct Pressure(Pascals);
1222
1223impl Pressure {
1224    /// Creates a new pressure from an `f64` value in pascals.
1225    pub const fn new(pa: f64) -> Self {
1226        Self(pa)
1227    }
1228
1229    /// Creates a new pressure from an `f64` value in pascals.
1230    pub const fn pa(pa: f64) -> Self {
1231        Self(pa)
1232    }
1233
1234    /// Creates a new pressure from an `f64` value in hectopascals.
1235    pub const fn hpa(hpa: f64) -> Self {
1236        Self(hpa * 100.0)
1237    }
1238
1239    /// Returns the value in pascals as an `f64`.
1240    pub const fn as_f64(&self) -> f64 {
1241        self.0
1242    }
1243
1244    /// Returns the value in pascals.
1245    pub const fn to_pa(&self) -> f64 {
1246        self.0
1247    }
1248
1249    /// Returns the value in hectopascals.
1250    pub const fn to_hpa(&self) -> f64 {
1251        self.0 * 0.01
1252    }
1253}
1254
1255impl Display for Pressure {
1256    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
1257        self.0.fmt(f)?;
1258        write!(f, " Pa")
1259    }
1260}
1261
1262type Watts = f64;
1263
1264/// Power in Watts.
1265#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
1266#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1267#[repr(transparent)]
1268pub struct Power(Watts);
1269
1270impl Power {
1271    /// Creates a new power from an `f64` value in Watts.
1272    pub const fn new(w: f64) -> Self {
1273        Self(w)
1274    }
1275
1276    /// Creates a new power from an `f64` value in Watts.
1277    pub const fn watts(w: f64) -> Self {
1278        Self(w)
1279    }
1280
1281    /// Creates a new power from an `f64` value in kilowatts.
1282    pub const fn kilowatts(kw: f64) -> Self {
1283        Self(kw * 1e3)
1284    }
1285
1286    /// Returns the value in Watts as an `f64`.
1287    pub const fn as_f64(&self) -> f64 {
1288        self.0
1289    }
1290
1291    /// Returns the value in Watts.
1292    pub const fn to_watts(&self) -> f64 {
1293        self.0
1294    }
1295
1296    /// Returns the value in kilowatts.
1297    pub const fn to_kilowatts(&self) -> f64 {
1298        self.0 * 1e-3
1299    }
1300
1301    /// Returns the value in dBW.
1302    pub fn to_dbw(&self) -> f64 {
1303        10.0 * log10(self.0)
1304    }
1305}
1306
1307impl Display for Power {
1308    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
1309        self.0.fmt(f)?;
1310        write!(f, " W")
1311    }
1312}
1313
1314type RadiansPerSecond = f64;
1315
1316/// Angular rate in radians per second.
1317#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
1318#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1319#[repr(transparent)]
1320pub struct AngularRate(RadiansPerSecond);
1321
1322impl AngularRate {
1323    /// Creates a new angular rate from an `f64` value in rad/s.
1324    pub const fn new(rps: f64) -> Self {
1325        Self(rps)
1326    }
1327
1328    /// Creates a new angular rate from an `f64` value in rad/s.
1329    pub const fn radians_per_second(rps: f64) -> Self {
1330        Self(rps)
1331    }
1332
1333    /// Creates a new angular rate from an `f64` value in deg/s.
1334    pub const fn degrees_per_second(dps: f64) -> Self {
1335        Self(to_radians(dps))
1336    }
1337
1338    /// Returns the value in rad/s as an `f64`.
1339    pub const fn as_f64(&self) -> f64 {
1340        self.0
1341    }
1342
1343    /// Returns the value in rad/s.
1344    pub const fn to_radians_per_second(&self) -> f64 {
1345        self.0
1346    }
1347
1348    /// Returns the value in deg/s.
1349    pub const fn to_degrees_per_second(&self) -> f64 {
1350        to_degrees(self.0)
1351    }
1352}
1353
1354impl Display for AngularRate {
1355    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
1356        to_degrees(self.0).fmt(f)?;
1357        write!(f, " deg/s")
1358    }
1359}
1360
1361type DecibelValue = f64;
1362
1363/// A value in decibels.
1364#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
1365#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1366#[repr(transparent)]
1367pub struct Decibel(DecibelValue);
1368
1369impl Decibel {
1370    /// Creates a new `Decibel` from a value already in dB.
1371    pub const fn new(db: f64) -> Self {
1372        Self(db)
1373    }
1374
1375    /// Converts a linear power-ratio value to decibels.
1376    pub fn from_linear(val: f64) -> Self {
1377        Self(10.0 * log10(val))
1378    }
1379
1380    /// Converts this decibel value to a linear power-ratio.
1381    pub fn to_linear(self) -> f64 {
1382        powf(10.0_f64, self.0 / 10.0)
1383    }
1384
1385    /// Returns the raw `f64` value in dB.
1386    pub const fn as_f64(self) -> f64 {
1387        self.0
1388    }
1389}
1390
1391impl Display for Decibel {
1392    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
1393        self.0.fmt(f)?;
1394        write!(f, " dB")
1395    }
1396}
1397
1398/// A trait for creating [`Decibel`] instances from primitives.
1399///
1400/// By default it is implemented for [`f64`] and [`i64`].
1401///
1402/// # Examples
1403///
1404/// ```
1405/// use lox_core::units::DecibelUnits;
1406///
1407/// let d = 3.0.db();
1408/// assert_eq!(d.as_f64(), 3.0);
1409/// ```
1410pub trait DecibelUnits {
1411    /// Creates a decibel value.
1412    fn db(&self) -> Decibel;
1413}
1414
1415impl DecibelUnits for f64 {
1416    fn db(&self) -> Decibel {
1417        Decibel::new(*self)
1418    }
1419}
1420
1421impl DecibelUnits for i64 {
1422    fn db(&self) -> Decibel {
1423        Decibel::new(*self as f64)
1424    }
1425}
1426
1427macro_rules! trait_impls {
1428    ($($unit:ident),*) => {
1429        $(
1430            impl Neg for $unit {
1431                type Output = Self;
1432
1433                fn neg(self) -> Self::Output {
1434                    Self(-self.0)
1435                }
1436            }
1437
1438            impl Add for $unit {
1439                type Output = Self;
1440
1441                fn add(self, rhs: Self) -> Self::Output {
1442                    Self(self.0 + rhs.0)
1443                }
1444            }
1445
1446            impl AddAssign for $unit {
1447                fn add_assign(&mut self, rhs: Self) {
1448                    self.0 = self.0 + rhs.0;
1449                }
1450            }
1451
1452            impl Sub for $unit {
1453                type Output = Self;
1454
1455                fn sub(self, rhs: Self) -> Self::Output {
1456                    Self(self.0 - rhs.0)
1457                }
1458            }
1459
1460            impl SubAssign for $unit {
1461                fn sub_assign(&mut self, rhs: Self) {
1462                    self.0 = self.0 - rhs.0
1463                }
1464            }
1465
1466            impl Mul<$unit> for f64 {
1467                type Output = $unit;
1468
1469                fn mul(self, rhs: $unit) -> Self::Output {
1470                    $unit(self * rhs.0)
1471                }
1472            }
1473
1474            impl From<$unit> for f64 {
1475                fn from(val: $unit) -> Self {
1476                    val.0
1477                }
1478            }
1479        )*
1480    };
1481}
1482
1483trait_impls!(
1484    Angle,
1485    AngularRate,
1486    Area,
1487    AreaToMass,
1488    Decibel,
1489    Distance,
1490    Frequency,
1491    Mass,
1492    Power,
1493    Pressure,
1494    Temperature,
1495    Velocity
1496);
1497
1498#[cfg(test)]
1499mod tests {
1500    use alloc::format;
1501    use core::f64::consts::{FRAC_PI_2, PI};
1502
1503    use lox_test_utils::assert_approx_eq;
1504    use rstest::rstest;
1505
1506    extern crate alloc;
1507
1508    use super::*;
1509
1510    #[test]
1511    fn test_angle_deg() {
1512        let angle = 90.0.deg();
1513        assert_approx_eq!(angle.0, FRAC_PI_2, rtol <= 1e-10);
1514    }
1515
1516    #[test]
1517    fn test_angle_rad() {
1518        let angle = PI.rad();
1519        assert_approx_eq!(angle.0, PI, rtol <= 1e-10);
1520    }
1521
1522    #[test]
1523    fn test_angle_conversions() {
1524        let angle_deg = 180.0.deg();
1525        let angle_rad = PI.rad();
1526        assert_approx_eq!(angle_deg.0, angle_rad.0, rtol <= 1e-10);
1527    }
1528
1529    #[test]
1530    fn test_angle_display() {
1531        let angle = 90.123456.deg();
1532        assert_eq!(format!("{:.2}", angle), "90.12 deg")
1533    }
1534
1535    #[test]
1536    fn test_angle_neg() {
1537        assert_eq!(Angle(-1.0), -1.0.rad())
1538    }
1539
1540    const TOLERANCE: f64 = f64::EPSILON;
1541
1542    #[rstest]
1543    // Center 0.0 – expected range [-π, π).
1544    #[case(Angle::ZERO, Angle::ZERO, 0.0)]
1545    #[case(Angle::PI, Angle::ZERO, -PI)]
1546    #[case(-Angle::PI, Angle::ZERO, -PI)]
1547    #[case(Angle::TAU, Angle::ZERO, 0.0)]
1548    #[case(Angle::FRAC_PI_2, Angle::ZERO, FRAC_PI_2)]
1549    #[case(-Angle::FRAC_PI_2, Angle::ZERO, -FRAC_PI_2)]
1550    // Center π – expected range [0, 2π).
1551    #[case(Angle::ZERO, Angle::PI, 0.0)]
1552    #[case(Angle::PI, Angle::PI, PI)]
1553    #[case(-Angle::PI, Angle::PI, PI)]
1554    #[case(Angle::TAU, Angle::PI, 0.0)]
1555    #[case(Angle::FRAC_PI_2, Angle::PI, FRAC_PI_2)]
1556    #[case(-Angle::FRAC_PI_2, Angle::PI, 3.0 * PI / 2.0)]
1557    // Center -π – expected range [-2π, 0).
1558    #[case(Angle::ZERO, -Angle::PI, -TAU)]
1559    #[case(Angle::PI, -Angle::PI, -PI)]
1560    #[case(-Angle::PI, -Angle::PI, -PI)]
1561    #[case(Angle::TAU, -Angle::PI, -TAU)]
1562    #[case(Angle::FRAC_PI_2, -Angle::PI, -3.0 * PI / 2.0)]
1563    #[case(-Angle::FRAC_PI_2, -Angle::PI, -FRAC_PI_2)]
1564    fn test_angle_normalize_two_pi(#[case] angle: Angle, #[case] center: Angle, #[case] exp: f64) {
1565        // atol is preferred to rtol for floating-point comparisons with 0.0. See
1566        // https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/#inferna
1567        if exp == 0.0 {
1568            assert_approx_eq!(angle.normalize_two_pi(center).0, exp, atol <= TOLERANCE);
1569        } else {
1570            assert_approx_eq!(angle.normalize_two_pi(center).0, exp, rtol <= TOLERANCE);
1571        }
1572    }
1573
1574    #[test]
1575    fn test_distance_m() {
1576        let distance = 1000.0.m();
1577        assert_eq!(distance.0, 1000.0);
1578    }
1579
1580    #[test]
1581    fn test_distance_km() {
1582        let distance = 1.0.km();
1583        assert_eq!(distance.0, 1000.0);
1584    }
1585
1586    #[test]
1587    fn test_distance_au() {
1588        let distance = 1.0.au();
1589        assert_eq!(distance.0, ASTRONOMICAL_UNIT);
1590    }
1591
1592    #[test]
1593    fn test_distance_conversions() {
1594        let d1 = 1.5e11.m();
1595        let d2 = (1.5e11 / ASTRONOMICAL_UNIT).au();
1596        assert_approx_eq!(d1.0, d2.0, rtol <= 1e-9);
1597    }
1598
1599    #[test]
1600    fn test_distance_display() {
1601        let distance = 9.123456.km();
1602        assert_eq!(format!("{:.2}", distance), "9.12 km")
1603    }
1604
1605    #[test]
1606    fn test_distance_neg() {
1607        assert_eq!(Distance(-1.0), -1.0.m())
1608    }
1609
1610    #[test]
1611    fn test_velocity_mps() {
1612        let velocity = 1000.0.mps();
1613        assert_eq!(velocity.0, 1000.0);
1614    }
1615
1616    #[test]
1617    fn test_velocity_kps() {
1618        let velocity = 1.0.kps();
1619        assert_eq!(velocity.0, 1000.0);
1620    }
1621
1622    #[test]
1623    fn test_velocity_conversions() {
1624        let v1 = 7500.0.mps();
1625        let v2 = 7.5.kps();
1626        assert_eq!(v1.0, v2.0);
1627    }
1628
1629    #[test]
1630    fn test_velocity_display() {
1631        let velocity = 9.123456.kps();
1632        assert_eq!(format!("{:.2}", velocity), "9.12 km/s")
1633    }
1634
1635    #[test]
1636    fn test_velocity_neg() {
1637        assert_eq!(Velocity(-1.0), -1.0.mps())
1638    }
1639
1640    #[test]
1641    fn test_frequency_hz() {
1642        let frequency = 1000.0.hz();
1643        assert_eq!(frequency.0, 1000.0);
1644    }
1645
1646    #[test]
1647    fn test_frequency_khz() {
1648        let frequency = 1.0.khz();
1649        assert_eq!(frequency.0, 1000.0);
1650    }
1651
1652    #[test]
1653    fn test_frequency_mhz() {
1654        let frequency = 1.0.mhz();
1655        assert_eq!(frequency.0, 1_000_000.0);
1656    }
1657
1658    #[test]
1659    fn test_frequency_ghz() {
1660        let frequency = 1.0.ghz();
1661        assert_eq!(frequency.0, 1_000_000_000.0);
1662    }
1663
1664    #[test]
1665    fn test_frequency_thz() {
1666        let frequency = 1.0.thz();
1667        assert_eq!(frequency.0, 1_000_000_000_000.0);
1668    }
1669
1670    #[test]
1671    fn test_frequency_conversions() {
1672        let f1 = 2.4.ghz();
1673        let f2 = 2400.0.mhz();
1674        assert_eq!(f1.0, f2.0);
1675    }
1676
1677    #[test]
1678    fn test_frequency_wavelength() {
1679        let f = 1.0.ghz();
1680        let wavelength = f.wavelength();
1681        assert_approx_eq!(wavelength.0, 0.299792458, rtol <= 1e-9);
1682    }
1683
1684    #[test]
1685    fn test_frequency_wavelength_speed_of_light() {
1686        let f = 299792458.0.hz(); // 1 Hz at speed of light
1687        let wavelength = f.wavelength();
1688        assert_approx_eq!(wavelength.0, 1.0, rtol <= 1e-10);
1689    }
1690
1691    #[test]
1692    fn test_frequency_display() {
1693        let frequency = 2.4123456.ghz();
1694        assert_eq!(format!("{:.2}", frequency), "2.41 GHz");
1695    }
1696
1697    #[rstest]
1698    #[case(0.0.hz(), None)]
1699    #[case(3.0.mhz(), Some(FrequencyBand::HF))]
1700    #[case(30.0.mhz(), Some(FrequencyBand::VHF))]
1701    #[case(300.0.mhz(), Some(FrequencyBand::UHF))]
1702    #[case(1.0.ghz(), Some(FrequencyBand::L))]
1703    #[case(2.0.ghz(), Some(FrequencyBand::S))]
1704    #[case(4.0.ghz(), Some(FrequencyBand::C))]
1705    #[case(8.0.ghz(), Some(FrequencyBand::X))]
1706    #[case(12.0.ghz(), Some(FrequencyBand::Ku))]
1707    #[case(18.0.ghz(), Some(FrequencyBand::K))]
1708    #[case(27.0.ghz(), Some(FrequencyBand::Ka))]
1709    #[case(40.0.ghz(), Some(FrequencyBand::V))]
1710    #[case(75.0.ghz(), Some(FrequencyBand::W))]
1711    #[case(110.0.ghz(), Some(FrequencyBand::G))]
1712    #[case(1.0.thz(), None)]
1713    fn test_frequency_band(#[case] f: Frequency, #[case] exp: Option<FrequencyBand>) {
1714        assert_eq!(f.band(), exp)
1715    }
1716
1717    #[test]
1718    fn test_decibel_db() {
1719        let d = 3.0.db();
1720        assert_eq!(d.as_f64(), 3.0);
1721    }
1722
1723    #[test]
1724    fn test_decibel_from_linear() {
1725        let d = Decibel::from_linear(100.0);
1726        assert_approx_eq!(d.0, 20.0, rtol <= 1e-10);
1727    }
1728
1729    #[test]
1730    fn test_decibel_to_linear() {
1731        let d = Decibel::new(20.0);
1732        assert_approx_eq!(d.to_linear(), 100.0, rtol <= 1e-10);
1733    }
1734
1735    #[test]
1736    fn test_decibel_roundtrip() {
1737        let val = 42.5;
1738        let d = Decibel::new(val);
1739        let roundtripped = Decibel::from_linear(d.to_linear());
1740        assert_approx_eq!(roundtripped.0, val, rtol <= 1e-10);
1741    }
1742
1743    #[test]
1744    fn test_decibel_add() {
1745        let sum = 3.0.db() + 3.0.db();
1746        assert_approx_eq!(sum.0, 6.0, rtol <= 1e-10);
1747    }
1748
1749    #[test]
1750    fn test_decibel_sub() {
1751        let diff = 6.0.db() - 3.0.db();
1752        assert_approx_eq!(diff.0, 3.0, rtol <= 1e-10);
1753    }
1754
1755    #[test]
1756    fn test_decibel_neg() {
1757        assert_eq!(-3.0.db(), Decibel::new(-3.0));
1758    }
1759
1760    #[test]
1761    fn test_decibel_display() {
1762        let d = 3.0.db();
1763        assert_eq!(format!("{:.1}", d), "3.0 dB");
1764    }
1765
1766    // --- Temperature ---
1767
1768    #[test]
1769    fn test_temperature_new() {
1770        let t = Temperature::new(290.0);
1771        assert_eq!(t.as_f64(), 290.0);
1772    }
1773
1774    #[test]
1775    fn test_temperature_kelvin() {
1776        let t = Temperature::kelvin(300.0);
1777        assert_eq!(t.to_kelvin(), 300.0);
1778    }
1779
1780    #[test]
1781    fn test_temperature_display() {
1782        let t = Temperature::new(290.0);
1783        assert_eq!(format!("{}", t), "290 K");
1784    }
1785
1786    #[test]
1787    fn test_temperature_arithmetic() {
1788        let a = Temperature::new(100.0);
1789        let b = Temperature::new(200.0);
1790        assert_eq!((a + b).as_f64(), 300.0);
1791        assert_eq!((b - a).as_f64(), 100.0);
1792        assert_eq!((-a).as_f64(), -100.0);
1793        assert_eq!((2.0 * a).as_f64(), 200.0);
1794    }
1795
1796    // --- Power ---
1797
1798    #[test]
1799    fn test_power_watts() {
1800        let p = Power::watts(100.0);
1801        assert_eq!(p.to_watts(), 100.0);
1802    }
1803
1804    #[test]
1805    fn test_power_kilowatts() {
1806        let p = Power::kilowatts(1.0);
1807        assert_eq!(p.to_watts(), 1000.0);
1808        assert_eq!(p.to_kilowatts(), 1.0);
1809    }
1810
1811    #[test]
1812    fn test_power_dbw() {
1813        let p = Power::watts(100.0);
1814        assert_approx_eq!(p.to_dbw(), 20.0, rtol <= 1e-10);
1815    }
1816
1817    #[test]
1818    fn test_power_display() {
1819        let p = Power::watts(100.0);
1820        assert_eq!(format!("{}", p), "100 W");
1821    }
1822
1823    #[test]
1824    fn test_power_arithmetic() {
1825        let a = Power::watts(50.0);
1826        let b = Power::watts(150.0);
1827        assert_eq!((a + b).as_f64(), 200.0);
1828        assert_eq!((b - a).as_f64(), 100.0);
1829        assert_eq!((-a).as_f64(), -50.0);
1830    }
1831
1832    // --- AngularRate ---
1833
1834    #[test]
1835    fn test_angular_rate_rps() {
1836        let ar = AngularRate::radians_per_second(1.0);
1837        assert_eq!(ar.to_radians_per_second(), 1.0);
1838        assert_approx_eq!(ar.to_degrees_per_second(), 57.29577951308232, rtol <= 1e-10);
1839    }
1840
1841    #[test]
1842    fn test_angular_rate_dps() {
1843        let ar = AngularRate::degrees_per_second(180.0);
1844        assert_approx_eq!(
1845            ar.to_radians_per_second(),
1846            core::f64::consts::PI,
1847            rtol <= 1e-10
1848        );
1849    }
1850
1851    #[test]
1852    fn test_angular_rate_display() {
1853        let ar = AngularRate::radians_per_second(1.0);
1854        let s = format!("{}", ar);
1855        assert!(s.contains("deg/s"));
1856    }
1857
1858    #[test]
1859    fn test_angular_rate_arithmetic() {
1860        let a = AngularRate::new(1.0);
1861        let b = AngularRate::new(2.0);
1862        assert_eq!((a + b).as_f64(), 3.0);
1863        assert_eq!((b - a).as_f64(), 1.0);
1864        assert_eq!((-a).as_f64(), -1.0);
1865        assert_eq!((3.0 * a).as_f64(), 3.0);
1866    }
1867
1868    // --- Pressure ---
1869
1870    #[test]
1871    fn test_pressure_hpa() {
1872        let p = Pressure::hpa(1013.25);
1873        assert_eq!(p.to_hpa(), 1013.25);
1874        assert_approx_eq!(p.to_pa(), 101325.0, rtol <= 1e-10);
1875    }
1876
1877    #[test]
1878    fn test_pressure_pa() {
1879        let p = Pressure::pa(101325.0);
1880        assert_approx_eq!(p.to_hpa(), 1013.25, rtol <= 1e-10);
1881    }
1882
1883    #[test]
1884    fn test_pressure_display() {
1885        let p = Pressure::pa(101325.0);
1886        let s = format!("{}", p);
1887        assert!(s.contains("Pa"));
1888    }
1889
1890    #[test]
1891    fn test_mass_kilograms() {
1892        let m = Mass::kilograms(1.5);
1893        assert_eq!(m.to_kilograms(), 1.5);
1894    }
1895
1896    #[test]
1897    fn test_mass_grams() {
1898        let m = Mass::grams(2500.0);
1899        assert_approx_eq!(m.to_kilograms(), 2.5, rtol <= 1e-12);
1900    }
1901
1902    #[test]
1903    fn test_mass_metric_tons() {
1904        let m = Mass::metric_tons(0.5);
1905        assert_approx_eq!(m.to_kilograms(), 500.0, rtol <= 1e-12);
1906    }
1907
1908    #[test]
1909    fn test_mass_units_kg() {
1910        let m = 1.5.kg();
1911        assert_eq!(m.to_kilograms(), 1.5);
1912    }
1913
1914    #[test]
1915    fn test_mass_units_g() {
1916        let m = 500.0.g();
1917        assert_approx_eq!(m.to_kilograms(), 0.5, rtol <= 1e-12);
1918    }
1919
1920    #[test]
1921    fn test_mass_display() {
1922        let m = 12.5.kg();
1923        assert_eq!(format!("{:.2}", m), "12.50 kg");
1924    }
1925
1926    #[test]
1927    fn test_mass_neg() {
1928        assert_eq!(Mass(-1.0), -1.0.kg())
1929    }
1930
1931    #[test]
1932    fn test_area_square_meters() {
1933        let a = Area::square_meters(2.5);
1934        assert_eq!(a.to_square_meters(), 2.5);
1935    }
1936
1937    #[test]
1938    fn test_area_square_kilometers() {
1939        let a = Area::square_kilometers(1.0);
1940        assert_approx_eq!(a.to_square_meters(), 1e6, rtol <= 1e-12);
1941    }
1942
1943    #[test]
1944    fn test_area_units_m2() {
1945        let a = 9.0.m2();
1946        assert_eq!(a.to_square_meters(), 9.0);
1947    }
1948
1949    #[test]
1950    fn test_area_units_km2() {
1951        let a = 2.0.km2();
1952        assert_approx_eq!(a.to_square_meters(), 2e6, rtol <= 1e-12);
1953    }
1954
1955    #[test]
1956    fn test_area_display() {
1957        let a = 4.5.m2();
1958        assert_eq!(format!("{:.2}", a), "4.50 m²");
1959    }
1960
1961    #[test]
1962    fn test_area_neg() {
1963        assert_eq!(Area(-1.0), -1.0.m2())
1964    }
1965
1966    #[test]
1967    fn test_area_to_mass_square_meters_per_kilogram() {
1968        let r = AreaToMass::square_meters_per_kilogram(0.025);
1969        assert_eq!(r.to_square_meters_per_kilogram(), 0.025);
1970    }
1971
1972    #[test]
1973    fn test_area_to_mass_units_shorthand() {
1974        let r = 0.05.m2_per_kg();
1975        assert_eq!(r.to_square_meters_per_kilogram(), 0.05);
1976    }
1977
1978    #[test]
1979    fn test_area_to_mass_display() {
1980        let r = 0.05.m2_per_kg();
1981        assert_eq!(format!("{:.2}", r), "0.05 m²/kg");
1982    }
1983
1984    #[test]
1985    fn test_area_to_mass_neg() {
1986        assert_eq!(AreaToMass(-1.0), -1.0.m2_per_kg())
1987    }
1988
1989    // -----------------------------------------------------------------------
1990    // Angle — additional constructors, accessors, and trig functions
1991    // -----------------------------------------------------------------------
1992
1993    #[test]
1994    fn test_angle_from_hms_erfa_tf2a() {
1995        // ERFA t_erfa_c.c::t_tf2a: tf2a('+', 4, 58, 20.2) = 1.301739278189537429 rad
1996        let a = Angle::from_hms(Sign::Positive, 4, 58, 20.2);
1997        assert_approx_eq!(a.to_radians(), 1.301_739_278_189_537_4, atol <= 1e-12);
1998    }
1999
2000    #[test]
2001    fn test_angle_from_hms_negative_within_one_hour() {
2002        // -0h 30m 0s must be representable as a negative angle.
2003        let a = Angle::from_hms(Sign::Negative, 0, 30, 0.0);
2004        assert!(a.to_radians() < 0.0);
2005        assert_approx_eq!(a.to_degrees(), -7.5, atol <= 1e-12);
2006    }
2007
2008    #[test]
2009    fn test_angle_arcseconds_roundtrip() {
2010        let a = Angle::arcseconds(3600.0); // = 1 degree
2011        assert_approx_eq!(a.to_degrees(), 1.0, rtol <= 1e-10);
2012        assert_approx_eq!(a.to_arcseconds(), 3600.0, rtol <= 1e-10);
2013    }
2014
2015    #[test]
2016    fn test_angle_arcseconds_normalized() {
2017        // ARCSECONDS_IN_CIRCLE + 3600 should normalize to 3600 arcsec = 1 deg
2018        let a = Angle::arcseconds_normalized(ARCSECONDS_IN_CIRCLE + 3600.0);
2019        assert_approx_eq!(a.to_arcseconds(), 3600.0, rtol <= 1e-10);
2020    }
2021
2022    #[test]
2023    fn test_angle_arcseconds_normalized_signed() {
2024        let a = Angle::arcseconds_normalized_signed(-ARCSECONDS_IN_CIRCLE - 3600.0);
2025        // Signed normalisation: just removes full circles, sign preserved
2026        let deg = a.to_degrees();
2027        assert!(deg < 0.0);
2028    }
2029
2030    #[test]
2031    fn test_angle_degrees_normalized() {
2032        let a = Angle::degrees_normalized(370.0); // 370 mod 360 = 10, then to [0, 2π)
2033        assert_approx_eq!(a.to_degrees(), 10.0, rtol <= 1e-10);
2034    }
2035
2036    #[test]
2037    fn test_angle_degrees_normalized_signed() {
2038        let a = Angle::degrees_normalized_signed(-10.0);
2039        assert_approx_eq!(a.to_degrees(), -10.0, rtol <= 1e-10);
2040    }
2041
2042    #[test]
2043    fn test_angle_radians_normalized() {
2044        use core::f64::consts::TAU;
2045        let a = Angle::radians_normalized(TAU + 0.5);
2046        assert_approx_eq!(a.as_f64(), 0.5, rtol <= 1e-10);
2047    }
2048
2049    #[test]
2050    fn test_angle_radians_normalized_signed() {
2051        use core::f64::consts::TAU;
2052        let a = Angle::radians_normalized_signed(-TAU - 0.5);
2053        assert!(a.as_f64() < 0.0);
2054    }
2055
2056    #[test]
2057    fn test_angle_is_zero() {
2058        assert!(Angle::ZERO.is_zero());
2059        assert!(!Angle::PI.is_zero());
2060    }
2061
2062    #[test]
2063    fn test_angle_abs() {
2064        let a = Angle::radians(-1.5);
2065        assert_approx_eq!(a.abs().as_f64(), 1.5, rtol <= 1e-10);
2066    }
2067
2068    #[test]
2069    fn test_angle_from_asin() {
2070        let a = Angle::from_asin(1.0);
2071        assert_approx_eq!(a.to_degrees(), 90.0, rtol <= 1e-10);
2072    }
2073
2074    #[test]
2075    fn test_angle_from_acos() {
2076        let a = Angle::from_acos(1.0);
2077        assert_approx_eq!(a.to_degrees(), 0.0, atol <= 1e-10);
2078    }
2079
2080    #[test]
2081    fn test_angle_from_atan() {
2082        let a = Angle::from_atan(1.0);
2083        assert_approx_eq!(a.to_degrees(), 45.0, rtol <= 1e-10);
2084    }
2085
2086    #[test]
2087    fn test_angle_from_atan2() {
2088        let a = Angle::from_atan2(1.0, 1.0); // 45 deg
2089        assert_approx_eq!(a.to_degrees(), 45.0, rtol <= 1e-10);
2090    }
2091
2092    #[test]
2093    fn test_angle_from_asinh() {
2094        let a = Angle::from_asinh(0.0);
2095        assert_approx_eq!(a.as_f64(), 0.0, atol <= 1e-10);
2096    }
2097
2098    #[test]
2099    fn test_angle_from_acosh() {
2100        let a = Angle::from_acosh(1.0);
2101        assert_approx_eq!(a.as_f64(), 0.0, atol <= 1e-10);
2102    }
2103
2104    #[test]
2105    fn test_angle_from_atanh() {
2106        let a = Angle::from_atanh(0.0);
2107        assert_approx_eq!(a.as_f64(), 0.0, atol <= 1e-10);
2108    }
2109
2110    #[test]
2111    fn test_angle_trig_functions() {
2112        let a = Angle::FRAC_PI_2;
2113        assert_approx_eq!(a.sin(), 1.0, rtol <= 1e-10);
2114        assert_approx_eq!(a.cos(), 0.0, atol <= 1e-10);
2115        let (s, c) = a.sin_cos();
2116        assert_approx_eq!(s, 1.0, rtol <= 1e-10);
2117        assert_approx_eq!(c, 0.0, atol <= 1e-10);
2118    }
2119
2120    #[test]
2121    fn test_angle_tan() {
2122        let a = Angle::degrees(45.0);
2123        assert_approx_eq!(a.tan(), 1.0, rtol <= 1e-10);
2124    }
2125
2126    #[test]
2127    fn test_angle_hyperbolic_trig() {
2128        let a = Angle::radians(1.0);
2129        assert_approx_eq!(a.sinh(), sinh(1.0_f64), rtol <= 1e-10);
2130        assert_approx_eq!(a.cosh(), cosh(1.0_f64), rtol <= 1e-10);
2131        assert_approx_eq!(a.tanh(), tanh(1.0_f64), rtol <= 1e-10);
2132    }
2133
2134    #[test]
2135    fn test_angle_mod_two_pi() {
2136        use core::f64::consts::TAU;
2137        let a = Angle::radians(TAU + 1.0).mod_two_pi();
2138        assert_approx_eq!(a.as_f64(), 1.0, rtol <= 1e-10);
2139    }
2140
2141    #[test]
2142    fn test_angle_mod_two_pi_signed() {
2143        use core::f64::consts::TAU;
2144        let a = Angle::radians(TAU + 1.0).mod_two_pi_signed();
2145        assert_approx_eq!(a.as_f64(), 1.0, rtol <= 1e-10);
2146    }
2147
2148    #[test]
2149    fn test_angle_rotation_matrices() {
2150        let a = Angle::ZERO;
2151        // Rotation by zero should give identity
2152        let rx = a.rotation_x();
2153        let ry = a.rotation_y();
2154        let rz = a.rotation_z();
2155        for i in 0..3 {
2156            for j in 0..3 {
2157                let expected = if i == j { 1.0 } else { 0.0 };
2158                assert_approx_eq!(rx.col(i)[j], expected, atol <= 1e-10);
2159                assert_approx_eq!(ry.col(i)[j], expected, atol <= 1e-10);
2160                assert_approx_eq!(rz.col(i)[j], expected, atol <= 1e-10);
2161            }
2162        }
2163    }
2164
2165    #[test]
2166    fn test_angle_arithmetic() {
2167        let a = Angle::degrees(30.0);
2168        let b = Angle::degrees(60.0);
2169        assert_approx_eq!((a + b).to_degrees(), 90.0, rtol <= 1e-10);
2170        assert_approx_eq!((b - a).to_degrees(), 30.0, rtol <= 1e-10);
2171        let mut c = a;
2172        c += b;
2173        assert_approx_eq!(c.to_degrees(), 90.0, rtol <= 1e-10);
2174        let mut d = b;
2175        d -= a;
2176        assert_approx_eq!(d.to_degrees(), 30.0, rtol <= 1e-10);
2177        let scaled = 2.0 * a;
2178        assert_approx_eq!(scaled.to_degrees(), 60.0, rtol <= 1e-10);
2179        let f: f64 = a.into();
2180        assert_approx_eq!(f, a.as_f64(), rtol <= 1e-10);
2181    }
2182
2183    #[test]
2184    fn test_angle_i64_units() {
2185        let a = 90_i64.deg();
2186        assert_approx_eq!(a.to_degrees(), 90.0, rtol <= 1e-10);
2187        let b = 1_i64.rad();
2188        assert_approx_eq!(b.as_f64(), 1.0, rtol <= 1e-10);
2189        let c = 3600_i64.arcsec();
2190        assert_approx_eq!(c.to_degrees(), 1.0, rtol <= 1e-10);
2191        let d = 1000_i64.mas();
2192        assert_approx_eq!(d.to_degrees(), 1.0 / 3600.0, rtol <= 1e-8);
2193        let e = 1_000_000_i64.uas();
2194        assert_approx_eq!(e.to_degrees(), 1.0 / 3600.0, rtol <= 1e-8);
2195    }
2196
2197    #[test]
2198    fn test_angle_f64_mas_uas() {
2199        let a = 1000.0_f64.mas();
2200        assert_approx_eq!(a.to_degrees(), 1.0 / 3600.0, rtol <= 1e-8);
2201        let b = 1_000_000.0_f64.uas();
2202        assert_approx_eq!(b.to_degrees(), 1.0 / 3600.0, rtol <= 1e-8);
2203    }
2204
2205    // -----------------------------------------------------------------------
2206    // Distance — additional accessors and units
2207    // -----------------------------------------------------------------------
2208
2209    #[test]
2210    fn test_distance_to_astronomical_units() {
2211        let d = Distance::astronomical_units(1.0);
2212        assert_approx_eq!(d.to_astronomical_units(), 1.0, rtol <= 1e-10);
2213    }
2214
2215    #[test]
2216    fn test_distance_as_f64() {
2217        let d = Distance::meters(5000.0);
2218        assert_eq!(d.as_f64(), 5000.0);
2219    }
2220
2221    #[test]
2222    fn test_distance_new() {
2223        let d = Distance::new(1234.0);
2224        assert_eq!(d.to_meters(), 1234.0);
2225    }
2226
2227    #[test]
2228    fn test_distance_to_meters() {
2229        let d = Distance::kilometers(1.0);
2230        assert_eq!(d.to_meters(), 1000.0);
2231    }
2232
2233    #[test]
2234    fn test_distance_i64_units() {
2235        let d = 7_i64.km();
2236        assert_eq!(d.to_meters(), 7000.0);
2237        let e = 1000_i64.m();
2238        assert_eq!(e.to_meters(), 1000.0);
2239        let f = 1_i64.au();
2240        assert_approx_eq!(f.to_meters(), ASTRONOMICAL_UNIT, rtol <= 1e-10);
2241    }
2242
2243    #[test]
2244    fn test_distance_arithmetic() {
2245        let a = Distance::meters(100.0);
2246        let b = Distance::meters(50.0);
2247        assert_eq!((a + b).to_meters(), 150.0);
2248        assert_eq!((a - b).to_meters(), 50.0);
2249        let mut c = a;
2250        c += b;
2251        assert_eq!(c.to_meters(), 150.0);
2252        let mut d = a;
2253        d -= b;
2254        assert_eq!(d.to_meters(), 50.0);
2255        let scaled = 2.0 * a;
2256        assert_eq!(scaled.to_meters(), 200.0);
2257        let f: f64 = a.into();
2258        assert_eq!(f, 100.0);
2259    }
2260
2261    // -----------------------------------------------------------------------
2262    // Velocity — additional constructors/accessors
2263    // -----------------------------------------------------------------------
2264
2265    #[test]
2266    fn test_velocity_astronomical_units_per_day() {
2267        let v = Velocity::astronomical_units_per_day(1.0);
2268        let expected = ASTRONOMICAL_UNIT / SECONDS_PER_DAY;
2269        assert_approx_eq!(v.to_meters_per_second(), expected, rtol <= 1e-10);
2270        assert_approx_eq!(v.to_astronomical_units_per_day(), 1.0, rtol <= 1e-10);
2271    }
2272
2273    #[test]
2274    fn test_velocity_fraction_of_speed_of_light() {
2275        let v = Velocity::fraction_of_speed_of_light(1.0);
2276        assert_approx_eq!(v.to_meters_per_second(), SPEED_OF_LIGHT, rtol <= 1e-10);
2277        assert_approx_eq!(v.to_fraction_of_speed_of_light(), 1.0, rtol <= 1e-10);
2278    }
2279
2280    #[test]
2281    fn test_velocity_new() {
2282        let v = Velocity::new(300.0);
2283        assert_eq!(v.as_f64(), 300.0);
2284    }
2285
2286    #[test]
2287    fn test_velocity_to_km_per_second() {
2288        let v = Velocity::meters_per_second(3000.0);
2289        assert_approx_eq!(v.to_kilometers_per_second(), 3.0, rtol <= 1e-10);
2290    }
2291
2292    #[test]
2293    fn test_velocity_i64_units() {
2294        let a = 7_i64.kps();
2295        assert_eq!(a.to_meters_per_second(), 7000.0);
2296        let b = 1_i64.mps();
2297        assert_eq!(b.to_meters_per_second(), 1.0);
2298        let c = 1_i64.aud();
2299        assert_approx_eq!(
2300            c.to_meters_per_second(),
2301            ASTRONOMICAL_UNIT / SECONDS_PER_DAY,
2302            rtol <= 1e-10
2303        );
2304        let d = 1_i64.c();
2305        assert_approx_eq!(d.to_meters_per_second(), SPEED_OF_LIGHT, rtol <= 1e-10);
2306    }
2307
2308    #[test]
2309    fn test_velocity_arithmetic() {
2310        let a = Velocity::meters_per_second(500.0);
2311        let b = Velocity::meters_per_second(250.0);
2312        assert_eq!((a + b).to_meters_per_second(), 750.0);
2313        assert_eq!((a - b).to_meters_per_second(), 250.0);
2314        let scaled = 3.0 * b;
2315        assert_eq!(scaled.to_meters_per_second(), 750.0);
2316        let f: f64 = a.into();
2317        assert_eq!(f, 500.0);
2318    }
2319
2320    // -----------------------------------------------------------------------
2321    // Mass — additional constructors and i64 metric_tons
2322    // -----------------------------------------------------------------------
2323
2324    #[test]
2325    fn test_mass_new() {
2326        let m = Mass::new(1.5);
2327        assert_eq!(m.as_f64(), 1.5);
2328    }
2329
2330    #[test]
2331    fn test_mass_to_grams() {
2332        let m = Mass::kilograms(2.0);
2333        assert_approx_eq!(m.to_grams(), 2000.0, rtol <= 1e-12);
2334    }
2335
2336    #[test]
2337    fn test_mass_to_metric_tons() {
2338        let m = Mass::kilograms(1000.0);
2339        assert_approx_eq!(m.to_metric_tons(), 1.0, rtol <= 1e-12);
2340    }
2341
2342    #[test]
2343    fn test_mass_i64_metric_tons() {
2344        let m = 1_i64.t();
2345        assert_approx_eq!(m.to_kilograms(), 1000.0, rtol <= 1e-12);
2346    }
2347
2348    #[test]
2349    fn test_mass_arithmetic() {
2350        let a = Mass::kilograms(100.0);
2351        let b = Mass::kilograms(50.0);
2352        assert_eq!((a + b).to_kilograms(), 150.0);
2353        assert_eq!((a - b).to_kilograms(), 50.0);
2354        let mut c = a;
2355        c += b;
2356        assert_eq!(c.to_kilograms(), 150.0);
2357        let scaled = 2.0 * b;
2358        assert_eq!(scaled.to_kilograms(), 100.0);
2359        let f: f64 = a.into();
2360        assert_eq!(f, 100.0);
2361    }
2362
2363    // -----------------------------------------------------------------------
2364    // Area — new/as_f64
2365    // -----------------------------------------------------------------------
2366
2367    #[test]
2368    fn test_area_new() {
2369        let a = Area::new(5.0);
2370        assert_eq!(a.as_f64(), 5.0);
2371    }
2372
2373    #[test]
2374    fn test_area_to_square_kilometers() {
2375        let a = Area::square_meters(1_000_000.0);
2376        assert_approx_eq!(a.to_square_kilometers(), 1.0, rtol <= 1e-12);
2377    }
2378
2379    #[test]
2380    fn test_area_i64_units() {
2381        let a = 4_i64.m2();
2382        assert_eq!(a.to_square_meters(), 4.0);
2383        let b = 2_i64.km2();
2384        assert_approx_eq!(b.to_square_meters(), 2e6, rtol <= 1e-12);
2385    }
2386
2387    #[test]
2388    fn test_area_arithmetic() {
2389        let a = Area::square_meters(4.0);
2390        let b = Area::square_meters(2.0);
2391        assert_eq!((a + b).to_square_meters(), 6.0);
2392        assert_eq!((a - b).to_square_meters(), 2.0);
2393        let scaled = 3.0 * b;
2394        assert_eq!(scaled.to_square_meters(), 6.0);
2395        let f: f64 = a.into();
2396        assert_eq!(f, 4.0);
2397    }
2398
2399    // -----------------------------------------------------------------------
2400    // AreaToMass — new/as_f64/i64 units/arithmetic
2401    // -----------------------------------------------------------------------
2402
2403    #[test]
2404    fn test_area_to_mass_new() {
2405        let r = AreaToMass::new(0.1);
2406        assert_eq!(r.as_f64(), 0.1);
2407    }
2408
2409    #[test]
2410    fn test_area_to_mass_i64_units() {
2411        let r = 1_i64.m2_per_kg();
2412        assert_eq!(r.to_square_meters_per_kilogram(), 1.0);
2413    }
2414
2415    #[test]
2416    fn test_area_to_mass_arithmetic() {
2417        let a = AreaToMass::square_meters_per_kilogram(0.1);
2418        let b = AreaToMass::square_meters_per_kilogram(0.05);
2419        assert_approx_eq!((a + b).as_f64(), 0.15, rtol <= 1e-10);
2420        assert_approx_eq!((a - b).as_f64(), 0.05, rtol <= 1e-10);
2421        let scaled = 2.0 * b;
2422        assert_approx_eq!(scaled.as_f64(), 0.1, rtol <= 1e-10);
2423        let f: f64 = a.into();
2424        assert_approx_eq!(f, 0.1, rtol <= 1e-10);
2425    }
2426
2427    // -----------------------------------------------------------------------
2428    // Frequency — additional
2429    // -----------------------------------------------------------------------
2430
2431    #[test]
2432    fn test_frequency_new() {
2433        let f = Frequency::new(1e9);
2434        assert_eq!(f.to_hertz(), 1e9);
2435    }
2436
2437    #[test]
2438    fn test_frequency_to_kilohertz() {
2439        let f = Frequency::megahertz(1.0);
2440        assert_approx_eq!(f.to_kilohertz(), 1000.0, rtol <= 1e-10);
2441    }
2442
2443    #[test]
2444    fn test_frequency_to_terahertz() {
2445        let f = Frequency::terahertz(1.0);
2446        assert_approx_eq!(f.to_terahertz(), 1.0, rtol <= 1e-10);
2447    }
2448
2449    #[test]
2450    fn test_frequency_arithmetic() {
2451        let a = Frequency::gigahertz(1.0);
2452        let b = Frequency::gigahertz(0.5);
2453        assert_approx_eq!((a + b).to_gigahertz(), 1.5, rtol <= 1e-10);
2454        assert_approx_eq!((a - b).to_gigahertz(), 0.5, rtol <= 1e-10);
2455        let scaled = 2.0 * b;
2456        assert_approx_eq!(scaled.to_gigahertz(), 1.0, rtol <= 1e-10);
2457        let f: f64 = a.into();
2458        assert_approx_eq!(f, 1e9, rtol <= 1e-10);
2459    }
2460
2461    // -----------------------------------------------------------------------
2462    // Temperature — arithmetic
2463    // -----------------------------------------------------------------------
2464
2465    #[test]
2466    fn test_temperature_neg() {
2467        let t = Temperature::kelvin(100.0);
2468        assert_eq!((-t).as_f64(), -100.0);
2469    }
2470
2471    #[test]
2472    fn test_temperature_add_assign_sub_assign() {
2473        let mut a = Temperature::kelvin(200.0);
2474        a += Temperature::kelvin(50.0);
2475        assert_eq!(a.as_f64(), 250.0);
2476        a -= Temperature::kelvin(100.0);
2477        assert_eq!(a.as_f64(), 150.0);
2478    }
2479
2480    // -----------------------------------------------------------------------
2481    // Pressure — new/arithmetic
2482    // -----------------------------------------------------------------------
2483
2484    #[test]
2485    fn test_pressure_new() {
2486        let p = Pressure::new(101325.0);
2487        assert_eq!(p.as_f64(), 101325.0);
2488    }
2489
2490    #[test]
2491    fn test_pressure_arithmetic() {
2492        let a = Pressure::pa(1000.0);
2493        let b = Pressure::pa(500.0);
2494        assert_eq!((a + b).to_pa(), 1500.0);
2495        assert_eq!((a - b).to_pa(), 500.0);
2496        assert_eq!((-b).to_pa(), -500.0);
2497        let scaled = 2.0 * b;
2498        assert_eq!(scaled.to_pa(), 1000.0);
2499        let f: f64 = a.into();
2500        assert_eq!(f, 1000.0);
2501    }
2502
2503    // -----------------------------------------------------------------------
2504    // Power — new/add_assign/sub_assign
2505    // -----------------------------------------------------------------------
2506
2507    #[test]
2508    fn test_power_new() {
2509        let p = Power::new(500.0);
2510        assert_eq!(p.as_f64(), 500.0);
2511    }
2512
2513    #[test]
2514    fn test_power_add_assign_sub_assign() {
2515        let mut p = Power::watts(200.0);
2516        p += Power::watts(100.0);
2517        assert_eq!(p.to_watts(), 300.0);
2518        p -= Power::watts(50.0);
2519        assert_eq!(p.to_watts(), 250.0);
2520    }
2521
2522    // -----------------------------------------------------------------------
2523    // AngularRate — new/as_f64/arithmetic
2524    // -----------------------------------------------------------------------
2525
2526    #[test]
2527    fn test_angular_rate_new() {
2528        let ar = AngularRate::new(2.0);
2529        assert_eq!(ar.as_f64(), 2.0);
2530    }
2531
2532    #[test]
2533    fn test_angular_rate_add_assign_sub_assign() {
2534        let mut ar = AngularRate::radians_per_second(1.0);
2535        ar += AngularRate::radians_per_second(0.5);
2536        assert_approx_eq!(ar.as_f64(), 1.5, rtol <= 1e-10);
2537        ar -= AngularRate::radians_per_second(0.5);
2538        assert_approx_eq!(ar.as_f64(), 1.0, rtol <= 1e-10);
2539        let f: f64 = ar.into();
2540        assert_approx_eq!(f, 1.0, rtol <= 1e-10);
2541    }
2542
2543    // -----------------------------------------------------------------------
2544    // Decibel — add_assign/sub_assign/neg/i64
2545    // -----------------------------------------------------------------------
2546
2547    #[test]
2548    fn test_decibel_add_assign_sub_assign() {
2549        let mut d = Decibel::new(10.0);
2550        d += Decibel::new(3.0);
2551        assert_approx_eq!(d.as_f64(), 13.0, rtol <= 1e-10);
2552        d -= Decibel::new(3.0);
2553        assert_approx_eq!(d.as_f64(), 10.0, rtol <= 1e-10);
2554    }
2555
2556    #[test]
2557    fn test_decibel_i64_units() {
2558        let d = 10_i64.db();
2559        assert_eq!(d.as_f64(), 10.0);
2560    }
2561
2562    #[test]
2563    fn test_decibel_mul_and_into() {
2564        let d = Decibel::new(5.0);
2565        let scaled = 2.0 * d;
2566        assert_approx_eq!(scaled.as_f64(), 10.0, rtol <= 1e-10);
2567        let f: f64 = d.into();
2568        assert_eq!(f, 5.0);
2569    }
2570
2571    // -----------------------------------------------------------------------
2572    // Sign
2573    // -----------------------------------------------------------------------
2574
2575    #[rstest]
2576    #[case(1.0_f64, Sign::Positive)]
2577    #[case(-1.0_f64, Sign::Negative)]
2578    #[case(0.0_f64, Sign::Positive)]
2579    #[case(-0.0_f64, Sign::Negative)] // IEEE sign bit
2580    #[case(f64::INFINITY, Sign::Positive)]
2581    #[case(f64::NEG_INFINITY, Sign::Negative)]
2582    fn test_sign_from_f64(#[case] input: f64, #[case] expected: Sign) {
2583        assert_eq!(Sign::from(input), expected);
2584    }
2585
2586    #[rstest]
2587    #[case(1_i32, Sign::Positive)]
2588    #[case(-1_i32, Sign::Negative)]
2589    #[case(0_i32, Sign::Positive)]
2590    fn test_sign_from_i32(#[case] input: i32, #[case] expected: Sign) {
2591        assert_eq!(Sign::from(input), expected);
2592    }
2593
2594    #[test]
2595    fn test_sign_from_signed_integer_widths() {
2596        assert_eq!(Sign::from(-1_i8), Sign::Negative);
2597        assert_eq!(Sign::from(-1_i16), Sign::Negative);
2598        assert_eq!(Sign::from(-1_i64), Sign::Negative);
2599        assert_eq!(Sign::from(-1_isize), Sign::Negative);
2600    }
2601
2602    #[test]
2603    fn test_sign_to_f64() {
2604        assert_eq!(f64::from(Sign::Positive), 1.0);
2605        assert_eq!(f64::from(Sign::Negative), -1.0);
2606        assert_eq!(Sign::Positive.as_f64(), 1.0);
2607        assert_eq!(Sign::Negative.as_f64(), -1.0);
2608    }
2609
2610    #[test]
2611    fn test_sign_display() {
2612        assert_eq!(format!("{}", Sign::Positive), "+");
2613        assert_eq!(format!("{}", Sign::Negative), "-");
2614    }
2615
2616    // -----------------------------------------------------------------------
2617    // Angle::from_dms (ERFA af2a)
2618    // -----------------------------------------------------------------------
2619
2620    #[test]
2621    fn test_angle_from_dms_erfa_af2a() {
2622        // ERFA t_erfa_c.c::t_af2a: af2a('-', 45, 13, 27.2) = -0.7893115794313644842 rad
2623        let a = Angle::from_dms(Sign::Negative, 45, 13, 27.2);
2624        assert_approx_eq!(a.to_radians(), -0.789_311_579_431_364_4, atol <= 1e-12);
2625    }
2626
2627    #[test]
2628    fn test_angle_from_dms_negative_within_one_degree() {
2629        // -0° 30' 0" must be representable.
2630        let a = Angle::from_dms(Sign::Negative, 0, 30, 0.0);
2631        assert!(a.to_radians() < 0.0);
2632        assert_approx_eq!(a.to_degrees(), -0.5, atol <= 1e-12);
2633    }
2634
2635    // -----------------------------------------------------------------------
2636    // Angle::to_hms (ERFA a2tf)
2637    // -----------------------------------------------------------------------
2638
2639    #[test]
2640    fn test_angle_to_hms_erfa_a2tf() {
2641        // ERFA t_erfa_c.c::t_a2tf: a2tf(4, -3.01234) -> -11h 30m 22.6484s
2642        let (sign, hours, min, sec) = Angle::radians(-3.01234).to_hms();
2643        assert_eq!(sign, Sign::Negative);
2644        assert_eq!(hours, 11);
2645        assert_eq!(min, 30);
2646        assert_approx_eq!(sec, 22.6484, atol <= 1e-4);
2647    }
2648
2649    #[test]
2650    fn test_angle_to_hms_negative_within_one_hour() {
2651        // -7.5° = -0h 30m 0s. Tests that the `-0h` case is representable.
2652        let (sign, hours, min, sec) = Angle::degrees(-7.5).to_hms();
2653        assert_eq!(sign, Sign::Negative);
2654        assert_eq!(hours, 0);
2655        assert_eq!(min, 30);
2656        assert_approx_eq!(sec, 0.0, atol <= 1e-10);
2657    }
2658
2659    // -----------------------------------------------------------------------
2660    // Angle::to_dms (ERFA a2af)
2661    // -----------------------------------------------------------------------
2662
2663    #[test]
2664    fn test_angle_to_dms_erfa_a2af() {
2665        // ERFA t_erfa_c.c::t_a2af: a2af(4, 2.345) -> +134° 21' 30.9706"
2666        let (sign, deg, min, sec) = Angle::radians(2.345).to_dms();
2667        assert_eq!(sign, Sign::Positive);
2668        assert_eq!(deg, 134);
2669        assert_eq!(min, 21);
2670        assert_approx_eq!(sec, 30.9706, atol <= 1e-4);
2671    }
2672
2673    #[test]
2674    fn test_angle_to_dms_negative() {
2675        // -0.5° should round-trip via to_dms exactly.
2676        let (sign, deg, min, sec) = Angle::degrees(-0.5).to_dms();
2677        assert_eq!(sign, Sign::Negative);
2678        assert_eq!(deg, 0);
2679        assert_eq!(min, 30);
2680        assert_approx_eq!(sec, 0.0, atol <= 1e-10);
2681    }
2682
2683    #[test]
2684    fn test_angle_to_dms_zero() {
2685        let (sign, deg, min, sec) = Angle::ZERO.to_dms();
2686        assert_eq!(sign, Sign::Positive);
2687        assert_eq!(deg, 0);
2688        assert_eq!(min, 0);
2689        assert_eq!(sec, 0.0);
2690    }
2691
2692    #[rstest]
2693    #[case(0.0)]
2694    #[case(0.5)]
2695    #[case(-0.5)]
2696    #[case(2.345)]
2697    #[case(-3.01234)]
2698    #[case(core::f64::consts::PI)]
2699    #[case(-core::f64::consts::PI)]
2700    fn test_angle_dms_roundtrip(#[case] radians: f64) {
2701        let a = Angle::radians(radians);
2702        let (sign, deg, min, sec) = a.to_dms();
2703        let b = Angle::from_dms(sign, deg, min, sec);
2704        assert_approx_eq!(a.to_radians(), b.to_radians(), atol <= 1e-12);
2705    }
2706
2707    #[rstest]
2708    #[case(0.0)]
2709    #[case(0.5)]
2710    #[case(-0.5)]
2711    #[case(2.345)]
2712    #[case(-3.01234)]
2713    #[case(core::f64::consts::PI)]
2714    #[case(-core::f64::consts::PI)]
2715    fn test_angle_hms_roundtrip(#[case] radians: f64) {
2716        let a = Angle::radians(radians);
2717        let (sign, hours, min, sec) = a.to_hms();
2718        let b = Angle::from_hms(sign, hours, min, sec);
2719        assert_approx_eq!(a.to_radians(), b.to_radians(), atol <= 1e-12);
2720    }
2721}