Skip to main content

qtty_core/units/
angular.rs

1//! Angular quantities and utilities.
2//!
3//! This module defines the **`Angular` dimension**, a blanket [`AngularUnit`] trait that extends
4//! [`Unit`] for all angular units, common angular units (degrees, radians, arcseconds, etc.), and a set of
5//! convenience methods on [`Quantity<U>`] where `U: AngularUnit`.
6//!
7//! # Design overview
8//!
9//! * **Canonical unit:** Degrees are taken as the canonical *scaling* unit for this dimension. That is,
10//!   `Degree::RATIO == 1.0`, and all other angular units express how many *degrees* correspond to one of that unit.
11//!   For example, `Radian::RATIO == 180.0 / PI` because 1 radian = 180/π degrees.
12//! * **Associated constants:** The `AngularUnit` trait exposes precomputed constants (`FULL_TURN`, `HALF_TURN`,
13//!   `QUARTER_TURN`) expressed *in the receiving unit* for ergonomic range‑wrapping. These are derived from `τ`
14//!   radians and then converted to the target unit to avoid cumulative error from chained conversions.
15//! * **Trigonometry:** `sin`, `cos`, `tan`, and `sin_cos` methods are provided on angular quantities; they convert to
16//!   radians internally and then call the corresponding `f64` intrinsic.
17//! * **Wrapping helpers:** Utility methods to wrap any angle into common ranges — `[0, 360)` (or unit equivalent),
18//!   `(-180, 180]`, and the latitude‑style quarter fold `[-90, 90]`.
19//!
20//! ## Edge cases
21//!
22//! Wrapping and trig operations follow IEEE‑754 semantics from `f64`: if the underlying numeric is `NaN` or
23//! `±∞`, results will generally be `NaN`.
24//!
25//! ## Unit symbols
26//!
27//! Unit `SYMBOL`s are used for display (e.g., `format!("{}", angle)`) and follow conventional unit symbols.
28//! Unicode symbols are used where standard notation requires them (e.g., `°`, `′`, `″`, `μas`).
29//!
30//! ## Examples
31//!
32//! Convert between degrees and radians and evaluate a trig function:
33//!
34//! ```rust
35//! use qtty_core::angular::{Degrees, Radians};
36//!
37//! let angle: Degrees = Degrees::new(90.0);
38//! let r: Radians = angle.to();
39//! assert!((r.value() - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
40//! assert!((angle.sin() - 1.0).abs() < 1e-12);
41//! ```
42//!
43//! Wrap into the conventional signed range:
44//!
45//! ```rust
46//! use qtty_core::angular::Degrees;
47//! let a = Degrees::new(370.0).wrap_signed();
48//! assert_eq!(a.value(), 10.0);
49//! ```
50
51use crate::scalar::Transcendental;
52use crate::{Quantity, Unit};
53use core::f64::consts::TAU;
54use qtty_derive::Unit;
55
56#[inline]
57fn rem_euclid(x: f64, modulus: f64) -> f64 {
58    #[cfg(feature = "std")]
59    {
60        x.rem_euclid(modulus)
61    }
62    #[cfg(not(feature = "std"))]
63    {
64        let r = crate::libm::fmod(x, modulus);
65        if r < 0.0 {
66            r + modulus
67        } else {
68            r
69        }
70    }
71}
72
73/// Re-export from the dimension module.
74pub use crate::dimension::Angular;
75
76/// Blanket extension trait for any [`Unit`] whose dimension is [`Angular`].
77///
78/// These associated constants provide the size of key turn fractions *expressed in the implementing unit*.
79/// They are computed via a compile-time conversion from `TAU` radians (i.e., a full revolution) and then scaled.
80/// This keeps all fractions derived from the same base value.
81///
82pub trait AngularUnit: Unit<Dim = Angular> {
83    /// One full revolution (τ radians / 360°) expressed in this unit.
84    const FULL_TURN: f64;
85    /// Half a revolution (π radians / 180°) expressed in this unit.
86    const HALF_TURN: f64;
87    /// A quarter revolution (π/2 radians / 90°) expressed in this unit.
88    const QUARTER_TURN: f64;
89}
90impl<T: Unit<Dim = Angular>> AngularUnit for T {
91    /// One full revolution (360°) expressed in T unit.
92    const FULL_TURN: f64 = Radians::new(TAU).to_const::<T>().value();
93    /// Half a revolution (180°) expressed in T unit.
94    const HALF_TURN: f64 = Radians::new(TAU).to_const::<T>().value() * 0.5;
95    /// Quarter revolution (90°) expressed in T unit.
96    const QUARTER_TURN: f64 = Radians::new(TAU).to_const::<T>().value() * 0.25;
97}
98
99impl<U: AngularUnit + Copy> Quantity<U> {
100    /// Constant representing τ radians (2π rad == 360°).
101    ///
102    /// For angular quantities, `TAU` and [`Self::FULL_TURN`] are identical by construction.
103    pub const TAU: Quantity<U> = Quantity::<U>::new(U::FULL_TURN);
104    /// One full revolution (360°) expressed as `Quantity<U>`.
105    pub const FULL_TURN: Quantity<U> = Quantity::<U>::new(U::FULL_TURN);
106    /// Half a revolution (180°) expressed as `Quantity<U>`.
107    pub const HALF_TURN: Quantity<U> = Quantity::<U>::new(U::HALF_TURN);
108    /// Quarter revolution (90°) expressed as `Quantity<U>`.
109    pub const QUARTER_TURN: Quantity<U> = Quantity::<U>::new(U::QUARTER_TURN);
110
111    /// Sign of the *raw numeric* in this unit (same semantics as `f64::signum()`).
112    #[inline]
113    pub const fn signum_const(self) -> f64 {
114        self.value().signum()
115    }
116
117    /// Normalize into the canonical positive range `[0, FULL_TURN)`.
118    ///
119    /// Shorthand for [`Self::wrap_pos`].
120    #[inline]
121    pub fn normalize(self) -> Self {
122        self.wrap_pos()
123    }
124
125    /// Wrap into the positive range `[0, FULL_TURN)` using Euclidean remainder.
126    ///
127    /// IEEE‑754 note: `NaN`/`±∞` inputs generally produce `NaN`.
128    #[inline]
129    pub fn wrap_pos(self) -> Self {
130        Self::new(rem_euclid(self.value(), U::FULL_TURN))
131    }
132
133    /// Wrap into the signed range `(-HALF_TURN, HALF_TURN]`.
134    ///
135    /// *Upper bound is inclusive*; lower bound is exclusive. Useful for computing minimal signed angular differences.
136    ///
137    /// IEEE‑754 note: `NaN`/`±∞` inputs generally produce `NaN`.
138    #[inline]
139    pub fn wrap_signed(self) -> Self {
140        let full = U::FULL_TURN;
141        let half = 0.5 * full;
142        let x = self.value();
143        let y = rem_euclid(x + half, full) - half;
144        let norm = if y <= -half { y + full } else { y };
145        Self::new(norm)
146    }
147
148    /// Wrap into the alternate signed range `[-HALF_TURN, HALF_TURN)`.
149    ///
150    /// Lower bound inclusive; upper bound exclusive. Equivalent to `self.wrap_signed()` with the boundary flipped.
151    ///
152    /// IEEE‑754 note: `NaN`/`±∞` inputs generally produce `NaN`.
153    #[inline]
154    pub fn wrap_signed_lo(self) -> Self {
155        let mut y = self.wrap_signed().value(); // now in (-half, half]
156        let half = 0.5 * U::FULL_TURN;
157        if y >= half {
158            // move +half to -half
159            y -= U::FULL_TURN;
160        }
161        Self::new(y)
162    }
163
164    /// "Latitude fold": map into `[-QUARTER_TURN, +QUARTER_TURN]`.
165    ///
166    /// Useful for folding polar coordinates (e.g., converting declination‑like angles to a limited range).
167    ///
168    /// IEEE‑754 note: `NaN`/`±∞` inputs generally produce `NaN`.
169    #[inline]
170    pub fn wrap_quarter_fold(self) -> Self {
171        let full = U::FULL_TURN;
172        let half = 0.5 * full;
173        let quarter = 0.25 * full;
174        let y = rem_euclid(self.value() + quarter, full);
175        // quarter - |y - half| yields [-quarter, quarter]
176        Self::new(quarter - (y - half).abs())
177    }
178
179    /// Signed smallest angular separation in `(-HALF_TURN, HALF_TURN]`.
180    #[inline]
181    pub fn signed_separation(self, other: Self) -> Self {
182        (self - other).wrap_signed()
183    }
184
185    /// Absolute smallest angular separation (magnitude only).
186    #[inline]
187    pub fn abs_separation(self, other: Self) -> Self {
188        let sep = self.signed_separation(other);
189        Self::new(sep.value().abs())
190    }
191}
192
193// ─────────────────────────────────────────────────────────────────────────────
194// Generic trigonometric implementations for any Transcendental scalar type
195// ─────────────────────────────────────────────────────────────────────────────
196
197impl<U: AngularUnit + Copy, S: Transcendental> Quantity<U, S> {
198    /// Sine of the angle.
199    ///
200    /// Converts the angle to radians and computes the sine.
201    /// Works with any scalar type that implements [`Transcendental`] (e.g., `f32`, `f64`).
202    #[inline]
203    pub fn sin(self) -> S {
204        let x_rad = self.to::<Radian>().value();
205        x_rad.sin()
206    }
207
208    /// Cosine of the angle.
209    ///
210    /// Converts the angle to radians and computes the cosine.
211    /// Works with any scalar type that implements [`Transcendental`] (e.g., `f32`, `f64`).
212    #[inline]
213    pub fn cos(self) -> S {
214        let x_rad = self.to::<Radian>().value();
215        x_rad.cos()
216    }
217
218    /// Tangent of the angle.
219    ///
220    /// Converts the angle to radians and computes the tangent.
221    /// Works with any scalar type that implements [`Transcendental`] (e.g., `f32`, `f64`).
222    #[inline]
223    pub fn tan(self) -> S {
224        let x_rad = self.to::<Radian>().value();
225        x_rad.tan()
226    }
227
228    /// Simultaneously compute sine and cosine.
229    ///
230    /// Converts the angle to radians and computes both sine and cosine.
231    /// Works with any scalar type that implements [`Transcendental`] (e.g., `f32`, `f64`).
232    #[inline]
233    pub fn sin_cos(self) -> (S, S) {
234        let x_rad = self.to::<Radian>().value();
235        x_rad.sin_cos()
236    }
237}
238
239/// Degree.
240#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
241#[unit(symbol = "°", dimension = Angular, ratio = 1.0)]
242pub struct Degree;
243/// Type alias shorthand for [`Degree`].
244pub type Deg = Degree;
245/// Convenience alias for a degree quantity.
246pub type Degrees = Quantity<Deg>;
247/// One degree.
248pub const DEG: Degrees = Degrees::new(1.0);
249
250/// Radian.
251#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
252#[unit(symbol = "rad", dimension = Angular, ratio = 180.0 / core::f64::consts::PI)]
253pub struct Radian;
254/// Type alias shorthand for [`Radian`].
255pub type Rad = Radian;
256/// Convenience alias for a radian quantity.
257pub type Radians = Quantity<Rad>;
258/// One radian.
259pub const RAD: Radians = Radians::new(1.0);
260
261/// Milliradian (`1/1000` radian).
262#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
263#[unit(symbol = "mrad", dimension = Angular, ratio = (180.0 / core::f64::consts::PI) / 1_000.0)]
264pub struct Milliradian;
265/// Type alias shorthand for [`Milliradian`].
266pub type Mrad = Milliradian;
267/// Convenience alias for a milliradian quantity.
268pub type Milliradians = Quantity<Mrad>;
269/// One milliradian.
270pub const MRAD: Milliradians = Milliradians::new(1.0);
271
272/// Arcminute (`1/60` degree).
273#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
274#[unit(symbol = "′", dimension = Angular, ratio = 1.0 / 60.0)]
275pub struct Arcminute;
276/// Alias for [`Arcminute`] (minute of angle, MOA).
277pub type MOA = Arcminute;
278/// Type alias shorthand for [`Arcminute`].
279pub type Arcm = Arcminute;
280/// Convenience alias for an arcminute quantity.
281pub type Arcminutes = Quantity<Arcm>;
282/// One arcminute.
283pub const ARCM: Arcminutes = Arcminutes::new(1.0);
284
285/// Arcsecond (`1/3600` degree).
286#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
287#[unit(symbol = "″", dimension = Angular, ratio = 1.0 / 3600.0)]
288pub struct Arcsecond;
289/// Type alias shorthand for [`Arcsecond`].
290pub type Arcs = Arcsecond;
291/// Convenience alias for an arcsecond quantity.
292pub type Arcseconds = Quantity<Arcs>;
293/// One arcsecond.
294pub const ARCS: Arcseconds = Arcseconds::new(1.0);
295
296/// Milliarcsecond (`1/3_600_000` degree).
297#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
298#[unit(symbol = "mas", dimension = Angular, ratio = 1.0 / 3_600_000.0)]
299pub struct MilliArcsecond;
300/// Type alias shorthand for [`MilliArcsecond`].
301pub type Mas = MilliArcsecond;
302/// Convenience alias for a milliarcsecond quantity.
303pub type MilliArcseconds = Quantity<Mas>;
304/// One milliarcsecond.
305pub const MAS: MilliArcseconds = MilliArcseconds::new(1.0);
306
307/// Microarcsecond (`1/3_600_000_000` degree).
308#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
309#[unit(symbol = "μas", dimension = Angular, ratio = 1.0 / 3_600_000_000.0)]
310pub struct MicroArcsecond;
311/// Type alias shorthand for [`MicroArcsecond`].
312pub type Uas = MicroArcsecond;
313/// Convenience alias for a microarcsecond quantity.
314pub type MicroArcseconds = Quantity<Uas>;
315/// One microarcsecond.
316pub const UAS: MicroArcseconds = MicroArcseconds::new(1.0);
317
318/// Gradian (also called gon; `1/400` of a full turn = `0.9` degree).
319#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
320#[unit(symbol = "gon", dimension = Angular, ratio = 0.9)]
321pub struct Gradian;
322/// Type alias shorthand for [`Gradian`].
323pub type Gon = Gradian;
324/// Convenience alias for a gradian quantity.
325pub type Gradians = Quantity<Gon>;
326/// One gradian.
327pub const GON: Gradians = Gradians::new(1.0);
328
329/// Turn (full revolution; `360` degrees).
330#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
331#[unit(symbol = "turn", dimension = Angular, ratio = 360.0)]
332pub struct Turn;
333/// Convenience alias for a turn quantity.
334pub type Turns = Quantity<Turn>;
335/// One turn.
336pub const TURN: Turns = Turns::new(1.0);
337
338/// Hour angle hour (`15` degrees).
339#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
340#[unit(symbol = "h", dimension = Angular, ratio = 15.0)]
341pub struct HourAngle;
342/// Type alias shorthand for [`HourAngle`].
343pub type Hms = HourAngle;
344/// Convenience alias for an hour-angle quantity.
345pub type HourAngles = Quantity<Hms>;
346/// One hour angle hour (==15°).
347pub const HOUR_ANGLE: HourAngles = HourAngles::new(1.0);
348
349impl HourAngles {
350    /// Construct from **HMS** components (`hours`, `minutes`, `seconds`).
351    ///
352    /// Sign is taken from `hours`; the `minutes` and `seconds` parameters are treated as magnitudes.
353    ///
354    /// ```rust
355    /// use qtty_core::angular::HourAngles;
356    /// let ra = HourAngles::from_hms(5, 30, 0.0); // 5h30m == 5.5h
357    /// assert_eq!(ra.value(), 5.5);
358    /// ```
359    pub const fn from_hms(hours: i32, minutes: u32, seconds: f64) -> Self {
360        let sign = if hours < 0 { -1.0 } else { 1.0 };
361        let h_abs = if hours < 0 { -hours } else { hours } as f64;
362        let m = minutes as f64 / 60.0;
363        let s = seconds / 3600.0;
364        let total_hours = sign * (h_abs + m + s);
365        Self::new(total_hours)
366    }
367}
368
369impl Degrees {
370    /// Construct from **DMS** components (`deg`, `min`, `sec`).
371    ///
372    /// Sign is taken from `deg`; the magnitude of `min` and `sec` is always added.
373    /// No range checking is performed. Use one of the wrapping helpers if you need a canonical range.
374    ///
375    /// ```rust
376    /// use qtty_core::angular::Degrees;
377    /// let lat = Degrees::from_dms(-33, 52, 0.0); // −33°52′00″
378    /// assert!(lat.value() < 0.0);
379    /// ```
380    pub const fn from_dms(deg: i32, min: u32, sec: f64) -> Self {
381        let sign = if deg < 0 { -1.0 } else { 1.0 };
382        let d_abs = if deg < 0 { -deg } else { deg } as f64;
383        let m = min as f64 / 60.0;
384        let s = sec / 3600.0;
385        let total = sign * (d_abs + m + s);
386        Self::new(total)
387    }
388
389    /// Construct from explicit sign and magnitude components.
390    ///
391    /// `sign` should be −1, 0, or +1 (0 treated as +1 unless all components are zero).
392    pub const fn from_dms_sign(sign: i8, deg: u32, min: u32, sec: f64) -> Self {
393        let s = if sign < 0 { -1.0 } else { 1.0 };
394        let total = (deg as f64) + (min as f64) / 60.0 + (sec / 3600.0);
395        Self::new(s * total)
396    }
397}
398
399// Generate all bidirectional From implementations between angular units.
400crate::impl_unit_from_conversions!(
401    Degree,
402    Radian,
403    Milliradian,
404    Arcminute,
405    Arcsecond,
406    MilliArcsecond,
407    MicroArcsecond,
408    Gradian,
409    Turn,
410    HourAngle
411);
412
413// Optional cross-unit operator support (`==`, `<`, etc.).
414#[cfg(feature = "cross-unit-ops")]
415crate::impl_unit_cross_unit_ops!(
416    Degree,
417    Radian,
418    Milliradian,
419    Arcminute,
420    Arcsecond,
421    MilliArcsecond,
422    MicroArcsecond,
423    Gradian,
424    Turn,
425    HourAngle
426);
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431    use approx::{assert_abs_diff_eq, assert_relative_eq};
432    use proptest::prelude::*;
433    use std::f64::consts::{PI, TAU};
434
435    // ─────────────────────────────────────────────────────────────────────────────
436    // Angular unit constants
437    // ─────────────────────────────────────────────────────────────────────────────
438
439    #[test]
440    fn test_full_turn() {
441        assert_abs_diff_eq!(Radian::FULL_TURN, TAU, epsilon = 1e-12);
442        assert_eq!(Degree::FULL_TURN, 360.0);
443        assert_eq!(Arcsecond::FULL_TURN, 1_296_000.0);
444    }
445
446    #[test]
447    fn test_half_turn() {
448        assert_abs_diff_eq!(Radian::HALF_TURN, PI, epsilon = 1e-12);
449        assert_eq!(Degree::HALF_TURN, 180.0);
450        assert_eq!(Arcsecond::HALF_TURN, 648_000.0);
451    }
452
453    #[test]
454    fn test_quarter_turn() {
455        assert_abs_diff_eq!(Radian::QUARTER_TURN, PI / 2.0, epsilon = 1e-12);
456        assert_eq!(Degree::QUARTER_TURN, 90.0);
457        assert_eq!(Arcsecond::QUARTER_TURN, 324_000.0);
458    }
459
460    #[test]
461    fn test_quantity_constants() {
462        assert_eq!(Degrees::FULL_TURN.value(), 360.0);
463        assert_eq!(Degrees::HALF_TURN.value(), 180.0);
464        assert_eq!(Degrees::QUARTER_TURN.value(), 90.0);
465        assert_eq!(Degrees::TAU.value(), 360.0);
466    }
467
468    // ─────────────────────────────────────────────────────────────────────────────
469    // Conversions
470    // ─────────────────────────────────────────────────────────────────────────────
471
472    #[test]
473    fn conversion_degrees_to_radians() {
474        let deg = Degrees::new(180.0);
475        let rad = deg.to::<Radian>();
476        assert_abs_diff_eq!(rad.value(), PI, epsilon = 1e-12);
477    }
478
479    #[test]
480    fn conversion_radians_to_degrees() {
481        let rad = Radians::new(PI);
482        let deg = rad.to::<Degree>();
483        assert_abs_diff_eq!(deg.value(), 180.0, epsilon = 1e-12);
484    }
485
486    #[test]
487    fn conversion_degrees_to_arcseconds() {
488        let deg = Degrees::new(1.0);
489        let arcs = deg.to::<Arcsecond>();
490        assert_abs_diff_eq!(arcs.value(), 3600.0, epsilon = 1e-9);
491    }
492
493    #[test]
494    fn conversion_arcseconds_to_degrees() {
495        let arcs = Arcseconds::new(3600.0);
496        let deg = arcs.to::<Degree>();
497        assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-12);
498    }
499
500    #[test]
501    fn conversion_degrees_to_milliarcseconds() {
502        let deg = Degrees::new(1.0);
503        let mas = deg.to::<MilliArcsecond>();
504        assert_abs_diff_eq!(mas.value(), 3_600_000.0, epsilon = 1e-6);
505    }
506
507    #[test]
508    fn conversion_hour_angles_to_degrees() {
509        let ha = HourAngles::new(1.0);
510        let deg = ha.to::<Degree>();
511        assert_abs_diff_eq!(deg.value(), 15.0, epsilon = 1e-12);
512    }
513
514    #[test]
515    fn conversion_roundtrip() {
516        let original = Degrees::new(123.456);
517        let rad = original.to::<Radian>();
518        let back = rad.to::<Degree>();
519        assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
520    }
521
522    #[test]
523    fn from_impl_degrees_radians() {
524        let deg = Degrees::new(90.0);
525        let rad: Radians = deg.into();
526        assert_abs_diff_eq!(rad.value(), PI / 2.0, epsilon = 1e-12);
527
528        let rad2 = Radians::new(PI);
529        let deg2: Degrees = rad2.into();
530        assert_abs_diff_eq!(deg2.value(), 180.0, epsilon = 1e-12);
531    }
532
533    // ─────────────────────────────────────────────────────────────────────────────
534    // Trig functions
535    // ─────────────────────────────────────────────────────────────────────────────
536
537    #[test]
538    fn test_trig() {
539        let a = Degrees::new(90.0);
540        assert!((a.sin() - 1.0).abs() < 1e-12);
541        assert!(a.cos().abs() < 1e-12);
542    }
543
544    #[test]
545    fn trig_sin_known_values() {
546        assert_abs_diff_eq!(Degrees::new(0.0).sin(), 0.0, epsilon = 1e-12);
547        assert_abs_diff_eq!(Degrees::new(30.0).sin(), 0.5, epsilon = 1e-12);
548        assert_abs_diff_eq!(Degrees::new(90.0).sin(), 1.0, epsilon = 1e-12);
549        assert_abs_diff_eq!(Degrees::new(180.0).sin(), 0.0, epsilon = 1e-12);
550        assert_abs_diff_eq!(Degrees::new(270.0).sin(), -1.0, epsilon = 1e-12);
551    }
552
553    #[test]
554    fn trig_cos_known_values() {
555        assert_abs_diff_eq!(Degrees::new(0.0).cos(), 1.0, epsilon = 1e-12);
556        assert_abs_diff_eq!(Degrees::new(60.0).cos(), 0.5, epsilon = 1e-12);
557        assert_abs_diff_eq!(Degrees::new(90.0).cos(), 0.0, epsilon = 1e-12);
558        assert_abs_diff_eq!(Degrees::new(180.0).cos(), -1.0, epsilon = 1e-12);
559    }
560
561    #[test]
562    fn trig_tan_known_values() {
563        assert_abs_diff_eq!(Degrees::new(0.0).tan(), 0.0, epsilon = 1e-12);
564        assert_abs_diff_eq!(Degrees::new(45.0).tan(), 1.0, epsilon = 1e-12);
565        assert_abs_diff_eq!(Degrees::new(180.0).tan(), 0.0, epsilon = 1e-12);
566    }
567
568    #[test]
569    fn trig_sin_cos_consistency() {
570        let angle = Degrees::new(37.5);
571        let (sin, cos) = angle.sin_cos();
572        assert_abs_diff_eq!(sin, angle.sin(), epsilon = 1e-15);
573        assert_abs_diff_eq!(cos, angle.cos(), epsilon = 1e-15);
574    }
575
576    #[test]
577    fn trig_pythagorean_identity() {
578        let angle = Degrees::new(123.456);
579        let sin = angle.sin();
580        let cos = angle.cos();
581        assert_abs_diff_eq!(sin * sin + cos * cos, 1.0, epsilon = 1e-12);
582    }
583
584    #[test]
585    fn trig_radians() {
586        assert_abs_diff_eq!(Radians::new(0.0).sin(), 0.0, epsilon = 1e-12);
587        assert_abs_diff_eq!(Radians::new(PI / 2.0).sin(), 1.0, epsilon = 1e-12);
588        assert_abs_diff_eq!(Radians::new(PI).cos(), -1.0, epsilon = 1e-12);
589    }
590
591    // ─────────────────────────────────────────────────────────────────────────────
592    // signum
593    // ─────────────────────────────────────────────────────────────────────────────
594
595    #[test]
596    fn signum_positive() {
597        assert_eq!(Degrees::new(45.0).signum(), 1.0);
598    }
599
600    #[test]
601    fn signum_negative() {
602        assert_eq!(Degrees::new(-45.0).signum(), -1.0);
603    }
604
605    #[test]
606    fn signum_zero() {
607        assert_eq!(Degrees::new(0.0).signum(), 1.0);
608    }
609
610    // ─────────────────────────────────────────────────────────────────────────────
611    // wrap_pos (normalize)
612    // ─────────────────────────────────────────────────────────────────────────────
613
614    #[test]
615    fn wrap_pos_basic() {
616        assert_abs_diff_eq!(
617            Degrees::new(370.0).wrap_pos().value(),
618            10.0,
619            epsilon = 1e-12
620        );
621        assert_abs_diff_eq!(Degrees::new(720.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
622        assert_abs_diff_eq!(Degrees::new(0.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
623    }
624
625    #[test]
626    fn wrap_pos_negative() {
627        assert_abs_diff_eq!(
628            Degrees::new(-10.0).wrap_pos().value(),
629            350.0,
630            epsilon = 1e-12
631        );
632        assert_abs_diff_eq!(
633            Degrees::new(-370.0).wrap_pos().value(),
634            350.0,
635            epsilon = 1e-12
636        );
637        assert_abs_diff_eq!(
638            Degrees::new(-720.0).wrap_pos().value(),
639            0.0,
640            epsilon = 1e-12
641        );
642    }
643
644    #[test]
645    fn wrap_pos_boundary() {
646        assert_abs_diff_eq!(Degrees::new(360.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
647        assert_abs_diff_eq!(
648            Degrees::new(-360.0).wrap_pos().value(),
649            0.0,
650            epsilon = 1e-12
651        );
652    }
653
654    #[test]
655    fn normalize_is_wrap_pos() {
656        let angle = Degrees::new(450.0);
657        assert_eq!(angle.normalize().value(), angle.wrap_pos().value());
658    }
659
660    // ─────────────────────────────────────────────────────────────────────────────
661    // wrap_signed: (-180, 180]
662    // ─────────────────────────────────────────────────────────────────────────────
663
664    #[test]
665    fn test_wrap_signed() {
666        let a = Degrees::new(370.0).wrap_signed();
667        assert_eq!(a.value(), 10.0);
668        let b = Degrees::new(-190.0).wrap_signed();
669        assert_eq!(b.value(), 170.0);
670    }
671
672    #[test]
673    fn wrap_signed_basic() {
674        assert_abs_diff_eq!(
675            Degrees::new(10.0).wrap_signed().value(),
676            10.0,
677            epsilon = 1e-12
678        );
679        assert_abs_diff_eq!(
680            Degrees::new(-10.0).wrap_signed().value(),
681            -10.0,
682            epsilon = 1e-12
683        );
684    }
685
686    #[test]
687    fn wrap_signed_over_180() {
688        assert_abs_diff_eq!(
689            Degrees::new(190.0).wrap_signed().value(),
690            -170.0,
691            epsilon = 1e-12
692        );
693        assert_abs_diff_eq!(
694            Degrees::new(270.0).wrap_signed().value(),
695            -90.0,
696            epsilon = 1e-12
697        );
698    }
699
700    #[test]
701    fn wrap_signed_boundary_180() {
702        assert_abs_diff_eq!(
703            Degrees::new(180.0).wrap_signed().value(),
704            180.0,
705            epsilon = 1e-12
706        );
707        assert_abs_diff_eq!(
708            Degrees::new(-180.0).wrap_signed().value(),
709            180.0,
710            epsilon = 1e-12
711        );
712    }
713
714    #[test]
715    fn wrap_signed_large_values() {
716        assert_abs_diff_eq!(
717            Degrees::new(540.0).wrap_signed().value(),
718            180.0,
719            epsilon = 1e-12
720        );
721        assert_abs_diff_eq!(
722            Degrees::new(-540.0).wrap_signed().value(),
723            180.0,
724            epsilon = 1e-12
725        );
726    }
727
728    // ─────────────────────────────────────────────────────────────────────────────
729    // wrap_quarter_fold: [-90, 90]
730    // ─────────────────────────────────────────────────────────────────────────────
731
732    #[test]
733    fn wrap_quarter_fold_basic() {
734        assert_abs_diff_eq!(
735            Degrees::new(0.0).wrap_quarter_fold().value(),
736            0.0,
737            epsilon = 1e-12
738        );
739        assert_abs_diff_eq!(
740            Degrees::new(45.0).wrap_quarter_fold().value(),
741            45.0,
742            epsilon = 1e-12
743        );
744        assert_abs_diff_eq!(
745            Degrees::new(-45.0).wrap_quarter_fold().value(),
746            -45.0,
747            epsilon = 1e-12
748        );
749    }
750
751    #[test]
752    fn wrap_quarter_fold_boundary() {
753        assert_abs_diff_eq!(
754            Degrees::new(90.0).wrap_quarter_fold().value(),
755            90.0,
756            epsilon = 1e-12
757        );
758        assert_abs_diff_eq!(
759            Degrees::new(-90.0).wrap_quarter_fold().value(),
760            -90.0,
761            epsilon = 1e-12
762        );
763    }
764
765    #[test]
766    fn wrap_quarter_fold_over_90() {
767        assert_abs_diff_eq!(
768            Degrees::new(100.0).wrap_quarter_fold().value(),
769            80.0,
770            epsilon = 1e-12
771        );
772        assert_abs_diff_eq!(
773            Degrees::new(135.0).wrap_quarter_fold().value(),
774            45.0,
775            epsilon = 1e-12
776        );
777        assert_abs_diff_eq!(
778            Degrees::new(180.0).wrap_quarter_fold().value(),
779            0.0,
780            epsilon = 1e-12
781        );
782    }
783
784    // ─────────────────────────────────────────────────────────────────────────────
785    // Separation helpers
786    // ─────────────────────────────────────────────────────────────────────────────
787
788    #[test]
789    fn signed_separation_basic() {
790        let a = Degrees::new(30.0);
791        let b = Degrees::new(50.0);
792        assert_abs_diff_eq!(a.signed_separation(b).value(), -20.0, epsilon = 1e-12);
793        assert_abs_diff_eq!(b.signed_separation(a).value(), 20.0, epsilon = 1e-12);
794    }
795
796    #[test]
797    fn signed_separation_wrap() {
798        let a = Degrees::new(10.0);
799        let b = Degrees::new(350.0);
800        assert_abs_diff_eq!(a.signed_separation(b).value(), 20.0, epsilon = 1e-12);
801        assert_abs_diff_eq!(b.signed_separation(a).value(), -20.0, epsilon = 1e-12);
802    }
803
804    #[test]
805    fn abs_separation() {
806        let a = Degrees::new(30.0);
807        let b = Degrees::new(50.0);
808        assert_abs_diff_eq!(a.abs_separation(b).value(), 20.0, epsilon = 1e-12);
809        assert_abs_diff_eq!(b.abs_separation(a).value(), 20.0, epsilon = 1e-12);
810    }
811
812    // ─────────────────────────────────────────────────────────────────────────────
813    // DMS / HMS construction
814    // ─────────────────────────────────────────────────────────────────────────────
815
816    #[test]
817    fn degrees_from_dms_positive() {
818        let d = Degrees::from_dms(12, 30, 0.0);
819        assert_abs_diff_eq!(d.value(), 12.5, epsilon = 1e-12);
820    }
821
822    #[test]
823    fn degrees_from_dms_negative() {
824        let d = Degrees::from_dms(-33, 52, 0.0);
825        assert!(d.value() < 0.0);
826        assert_abs_diff_eq!(d.value(), -(33.0 + 52.0 / 60.0), epsilon = 1e-12);
827    }
828
829    #[test]
830    fn degrees_from_dms_with_seconds() {
831        let d = Degrees::from_dms(10, 20, 30.0);
832        assert_abs_diff_eq!(
833            d.value(),
834            10.0 + 20.0 / 60.0 + 30.0 / 3600.0,
835            epsilon = 1e-12
836        );
837    }
838
839    #[test]
840    fn degrees_from_dms_sign() {
841        let pos = Degrees::from_dms_sign(1, 45, 30, 0.0);
842        let neg = Degrees::from_dms_sign(-1, 45, 30, 0.0);
843        assert_abs_diff_eq!(pos.value(), 45.5, epsilon = 1e-12);
844        assert_abs_diff_eq!(neg.value(), -45.5, epsilon = 1e-12);
845    }
846
847    #[test]
848    fn hour_angles_from_hms() {
849        let ha = HourAngles::from_hms(5, 30, 0.0);
850        assert_abs_diff_eq!(ha.value(), 5.5, epsilon = 1e-12);
851    }
852
853    #[test]
854    fn hour_angles_from_hms_negative() {
855        let ha = HourAngles::from_hms(-3, 15, 0.0);
856        assert_abs_diff_eq!(ha.value(), -3.25, epsilon = 1e-12);
857    }
858
859    #[test]
860    fn hour_angles_to_degrees() {
861        let ha = HourAngles::new(6.0);
862        let deg = ha.to::<Degree>();
863        assert_abs_diff_eq!(deg.value(), 90.0, epsilon = 1e-12);
864    }
865
866    // ─────────────────────────────────────────────────────────────────────────────
867    // Display formatting
868    // ─────────────────────────────────────────────────────────────────────────────
869
870    #[test]
871    fn display_degrees() {
872        let d = Degrees::new(45.5);
873        assert_eq!(format!("{}", d), "45.5 °");
874    }
875
876    #[test]
877    fn display_radians() {
878        let r = Radians::new(1.0);
879        assert_eq!(format!("{}", r), "1 rad");
880    }
881
882    // ─────────────────────────────────────────────────────────────────────────────
883    // Unit constants
884    // ─────────────────────────────────────────────────────────────────────────────
885
886    #[test]
887    fn unit_constants() {
888        assert_eq!(DEG.value(), 1.0);
889        assert_eq!(RAD.value(), 1.0);
890        assert_eq!(MRAD.value(), 1.0);
891        assert_eq!(ARCM.value(), 1.0);
892        assert_eq!(ARCS.value(), 1.0);
893        assert_eq!(MAS.value(), 1.0);
894        assert_eq!(UAS.value(), 1.0);
895        assert_eq!(GON.value(), 1.0);
896        assert_eq!(TURN.value(), 1.0);
897        assert_eq!(HOUR_ANGLE.value(), 1.0);
898    }
899
900    // ─────────────────────────────────────────────────────────────────────────────
901    // wrap_signed_lo: [-180, 180)
902    // ─────────────────────────────────────────────────────────────────────────────
903
904    #[test]
905    fn wrap_signed_lo_boundary_half_turn() {
906        // +half turn should map to -half turn to make the upper bound exclusive.
907        assert_abs_diff_eq!(
908            Degrees::new(180.0).wrap_signed_lo().value(),
909            -180.0,
910            epsilon = 1e-12
911        );
912        assert_abs_diff_eq!(
913            Degrees::new(-180.0).wrap_signed_lo().value(),
914            -180.0,
915            epsilon = 1e-12
916        );
917    }
918
919    // ─────────────────────────────────────────────────────────────────────────────
920    // New unit conversions and tests
921    // ─────────────────────────────────────────────────────────────────────────────
922
923    #[test]
924    fn conversion_degrees_to_arcminutes() {
925        let deg = Degrees::new(1.0);
926        let arcm = deg.to::<Arcminute>();
927        assert_abs_diff_eq!(arcm.value(), 60.0, epsilon = 1e-12);
928    }
929
930    #[test]
931    fn conversion_arcminutes_to_degrees() {
932        let arcm = Arcminutes::new(60.0);
933        let deg = arcm.to::<Degree>();
934        assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-12);
935    }
936
937    #[test]
938    fn conversion_arcminutes_to_arcseconds() {
939        let arcm = Arcminutes::new(1.0);
940        let arcs = arcm.to::<Arcsecond>();
941        assert_abs_diff_eq!(arcs.value(), 60.0, epsilon = 1e-12);
942    }
943
944    #[test]
945    fn conversion_arcseconds_to_microarcseconds() {
946        let arcs = Arcseconds::new(1.0);
947        let uas = arcs.to::<MicroArcsecond>();
948        assert_abs_diff_eq!(uas.value(), 1_000_000.0, epsilon = 1e-6);
949    }
950
951    #[test]
952    fn conversion_microarcseconds_to_degrees() {
953        let uas = MicroArcseconds::new(3_600_000_000.0);
954        let deg = uas.to::<Degree>();
955        assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-9);
956    }
957
958    #[test]
959    fn conversion_degrees_to_gradians() {
960        let deg = Degrees::new(90.0);
961        let gon = deg.to::<Gradian>();
962        assert_abs_diff_eq!(gon.value(), 100.0, epsilon = 1e-12);
963    }
964
965    #[test]
966    fn conversion_gradians_to_degrees() {
967        let gon = Gradians::new(400.0);
968        let deg = gon.to::<Degree>();
969        assert_abs_diff_eq!(deg.value(), 360.0, epsilon = 1e-12);
970    }
971
972    #[test]
973    fn conversion_gradians_to_radians() {
974        let gon = Gradians::new(200.0);
975        let rad = gon.to::<Radian>();
976        assert_abs_diff_eq!(rad.value(), PI, epsilon = 1e-12);
977    }
978
979    #[test]
980    fn conversion_degrees_to_turns() {
981        let deg = Degrees::new(360.0);
982        let turn = deg.to::<Turn>();
983        assert_abs_diff_eq!(turn.value(), 1.0, epsilon = 1e-12);
984    }
985
986    #[test]
987    fn conversion_milliradians_to_radians() {
988        let mrad = Milliradians::new(1_000.0);
989        let rad = mrad.to::<Radian>();
990        assert_abs_diff_eq!(rad.value(), 1.0, epsilon = 1e-12);
991    }
992
993    #[test]
994    fn conversion_turns_to_degrees() {
995        let turn = Turns::new(2.5);
996        let deg = turn.to::<Degree>();
997        assert_abs_diff_eq!(deg.value(), 900.0, epsilon = 1e-12);
998    }
999
1000    #[test]
1001    fn conversion_turns_to_radians() {
1002        let turn = Turns::new(1.0);
1003        let rad = turn.to::<Radian>();
1004        assert_abs_diff_eq!(rad.value(), TAU, epsilon = 1e-12);
1005    }
1006
1007    #[test]
1008    fn from_impl_new_units() {
1009        // Test From trait implementations for new units
1010        let deg = Degrees::new(1.0);
1011        let arcm: Arcminutes = deg.into();
1012        assert_abs_diff_eq!(arcm.value(), 60.0, epsilon = 1e-12);
1013
1014        let gon = Gradians::new(100.0);
1015        let deg2: Degrees = gon.into();
1016        assert_abs_diff_eq!(deg2.value(), 90.0, epsilon = 1e-12);
1017
1018        let turn = Turns::new(0.25);
1019        let deg3: Degrees = turn.into();
1020        assert_abs_diff_eq!(deg3.value(), 90.0, epsilon = 1e-12);
1021    }
1022
1023    #[test]
1024    fn roundtrip_arcminute_arcsecond() {
1025        let original = Arcminutes::new(5.0);
1026        let arcs = original.to::<Arcsecond>();
1027        let back = arcs.to::<Arcminute>();
1028        assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
1029    }
1030
1031    #[test]
1032    fn roundtrip_gradian_degree() {
1033        let original = Gradians::new(123.456);
1034        let deg = original.to::<Degree>();
1035        let back = deg.to::<Gradian>();
1036        assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
1037    }
1038
1039    #[test]
1040    fn roundtrip_turn_radian() {
1041        let original = Turns::new(2.717);
1042        let rad = original.to::<Radian>();
1043        let back = rad.to::<Turn>();
1044        assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
1045    }
1046
1047    #[test]
1048    fn gradian_full_turn() {
1049        assert_abs_diff_eq!(Gradian::FULL_TURN, 400.0, epsilon = 1e-12);
1050    }
1051
1052    #[test]
1053    fn turn_full_turn() {
1054        assert_abs_diff_eq!(Turn::FULL_TURN, 1.0, epsilon = 1e-12);
1055    }
1056
1057    #[test]
1058    fn arcminute_full_turn() {
1059        assert_abs_diff_eq!(Arcminute::FULL_TURN, 21_600.0, epsilon = 1e-9);
1060    }
1061
1062    #[test]
1063    fn microarcsecond_conversion_chain() {
1064        // Test a long conversion chain
1065        let uas = MicroArcseconds::new(1e9);
1066        let mas = uas.to::<MilliArcsecond>();
1067        let arcs = mas.to::<Arcsecond>();
1068        let arcm = arcs.to::<Arcminute>();
1069        let deg = arcm.to::<Degree>();
1070
1071        assert_abs_diff_eq!(mas.value(), 1_000_000.0, epsilon = 1e-6);
1072        assert_abs_diff_eq!(arcs.value(), 1_000.0, epsilon = 1e-9);
1073        assert_abs_diff_eq!(arcm.value(), 1_000.0 / 60.0, epsilon = 1e-9);
1074        assert_relative_eq!(deg.value(), 1_000.0 / 3600.0, max_relative = 1e-9);
1075    }
1076
1077    #[test]
1078    fn wrap_pos_with_turns() {
1079        let turn = Turns::new(2.7);
1080        let wrapped = turn.wrap_pos();
1081        assert_abs_diff_eq!(wrapped.value(), 0.7, epsilon = 1e-12);
1082    }
1083
1084    #[test]
1085    fn wrap_signed_with_gradians() {
1086        let gon = Gradians::new(350.0);
1087        let wrapped = gon.wrap_signed();
1088        assert_abs_diff_eq!(wrapped.value(), -50.0, epsilon = 1e-12);
1089    }
1090
1091    #[test]
1092    fn trig_with_gradians() {
1093        let gon = Gradians::new(100.0); // 90 degrees
1094        assert_abs_diff_eq!(gon.sin(), 1.0, epsilon = 1e-12);
1095        assert_abs_diff_eq!(gon.cos(), 0.0, epsilon = 1e-12);
1096    }
1097
1098    #[test]
1099    fn trig_with_turns() {
1100        let turn = Turns::new(0.25); // 90 degrees
1101        assert_abs_diff_eq!(turn.sin(), 1.0, epsilon = 1e-12);
1102        assert_abs_diff_eq!(turn.cos(), 0.0, epsilon = 1e-12);
1103    }
1104
1105    #[test]
1106    fn all_units_to_degrees() {
1107        // Verify all units convert correctly to degrees
1108        assert_abs_diff_eq!(
1109            Radians::new(PI).to::<Degree>().value(),
1110            180.0,
1111            epsilon = 1e-12
1112        );
1113        assert_abs_diff_eq!(
1114            Arcminutes::new(60.0).to::<Degree>().value(),
1115            1.0,
1116            epsilon = 1e-12
1117        );
1118        assert_abs_diff_eq!(
1119            Arcseconds::new(3600.0).to::<Degree>().value(),
1120            1.0,
1121            epsilon = 1e-12
1122        );
1123        assert_abs_diff_eq!(
1124            MilliArcseconds::new(3_600_000.0).to::<Degree>().value(),
1125            1.0,
1126            epsilon = 1e-9
1127        );
1128        assert_abs_diff_eq!(
1129            MicroArcseconds::new(3_600_000_000.0).to::<Degree>().value(),
1130            1.0,
1131            epsilon = 1e-6
1132        );
1133        assert_abs_diff_eq!(
1134            Gradians::new(100.0).to::<Degree>().value(),
1135            90.0,
1136            epsilon = 1e-12
1137        );
1138        assert_abs_diff_eq!(
1139            Turns::new(1.0).to::<Degree>().value(),
1140            360.0,
1141            epsilon = 1e-12
1142        );
1143        assert_abs_diff_eq!(
1144            HourAngles::new(1.0).to::<Degree>().value(),
1145            15.0,
1146            epsilon = 1e-12
1147        );
1148    }
1149
1150    // ─────────────────────────────────────────────────────────────────────────────
1151    // Property-based tests
1152    // ─────────────────────────────────────────────────────────────────────────────
1153
1154    proptest! {
1155        #[test]
1156        fn prop_wrap_pos_range(angle in -1e6..1e6f64) {
1157            let wrapped = Degrees::new(angle).wrap_pos();
1158            prop_assert!(wrapped.value() >= 0.0);
1159            prop_assert!(wrapped.value() < 360.0);
1160        }
1161
1162        #[test]
1163        fn prop_wrap_signed_range(angle in -1e6..1e6f64) {
1164            let wrapped = Degrees::new(angle).wrap_signed();
1165            prop_assert!(wrapped.value() > -180.0);
1166            prop_assert!(wrapped.value() <= 180.0);
1167        }
1168
1169        #[test]
1170        fn prop_wrap_quarter_fold_range(angle in -1e6..1e6f64) {
1171            let wrapped = Degrees::new(angle).wrap_quarter_fold();
1172            prop_assert!(wrapped.value() >= -90.0);
1173            prop_assert!(wrapped.value() <= 90.0);
1174        }
1175
1176        #[test]
1177        fn prop_pythagorean_identity(angle in -360.0..360.0f64) {
1178            let a = Degrees::new(angle);
1179            let sin = a.sin();
1180            let cos = a.cos();
1181            assert_abs_diff_eq!(sin * sin + cos * cos, 1.0, epsilon = 1e-12);
1182        }
1183
1184        #[test]
1185        fn prop_conversion_roundtrip(angle in -1e6..1e6f64) {
1186            let deg = Degrees::new(angle);
1187            let rad = deg.to::<Radian>();
1188            let back = rad.to::<Degree>();
1189            assert_relative_eq!(back.value(), deg.value(), max_relative = 1e-12);
1190        }
1191
1192        #[test]
1193        fn prop_abs_separation_symmetric(a in -360.0..360.0f64, b in -360.0..360.0f64) {
1194            let da = Degrees::new(a);
1195            let db = Degrees::new(b);
1196            assert_abs_diff_eq!(
1197                da.abs_separation(db).value(),
1198                db.abs_separation(da).value(),
1199                epsilon = 1e-12
1200            );
1201        }
1202    }
1203
1204    /// Invoke derive-generated PartialEq/Clone/Debug on each unit struct to
1205    /// cover the #[derive] attribute lines tracked by llvm-cov.
1206    #[test]
1207    fn derive_coverage_unit_structs() {
1208        // Direct struct value comparisons invoke <T as PartialEq>::eq() which
1209        // is the derive-generated implementation, covering the #[derive] line.
1210        assert!(Degree == Degree);
1211        assert!(Radian == Radian);
1212        assert!(Milliradian == Milliradian);
1213        assert!(Arcminute == Arcminute);
1214        assert!(Arcsecond == Arcsecond);
1215        assert!(MilliArcsecond == MilliArcsecond);
1216        assert!(MicroArcsecond == MicroArcsecond);
1217        assert!(Gradian == Gradian);
1218        assert!(Turn == Turn);
1219        assert!(HourAngle == HourAngle);
1220        // signum_const: cover both positive and negative branches
1221        let pos = Degrees::new(90.0);
1222        let neg = Degrees::new(-45.0);
1223        assert_eq!(pos.signum_const(), 1.0);
1224        assert_eq!(neg.signum_const(), -1.0);
1225    }
1226}