Skip to main content

celestial_core/
utils.rs

1//! Utility functions for time and angle conversions.
2//!
3//! Helper functions for common operations: Julian Date to centuries conversion,
4//! angle normalization, and angular differences. These are building blocks used
5//! throughout the library.
6//!
7//! # Time Conversion
8//!
9//! [`jd_to_centuries`] converts a two-part Julian Date to Julian centuries from J2000.0,
10//! the time unit used by most IAU precession/nutation models.
11//!
12//! # Angle Normalization
13//!
14//! | Function | Input | Output Range |
15//! |----------|-------|--------------|
16//! | [`normalize_longitude`] | degrees | (-180°, 180°] |
17//! | [`normalize_latitude`] | degrees | [-90°, 90°] (clamped) |
18//! | [`normalize_angle_rad`] | radians | (-π, π] |
19//!
20//! # Angular Difference
21//!
22//! [`angular_difference`] computes the shortest signed difference between two angles
23//! in degrees, handling the wraparound at ±180°.
24
25use crate::constants::{DAYS_PER_JULIAN_CENTURY, J2000_JD, PI, TWOPI};
26
27/// Converts a two-part Julian Date to Julian centuries from J2000.0.
28///
29/// The two-part split preserves precision. Typically:
30/// - `jd1 = 2451545.0` (J2000.0 epoch)
31/// - `jd2` = days from that epoch
32///
33/// One Julian century = 36525 days.
34///
35/// # Example
36///
37/// ```
38/// use celestial_core::utils::jd_to_centuries;
39/// use celestial_core::constants::J2000_JD;
40///
41/// // At J2000.0 → t = 0
42/// assert_eq!(jd_to_centuries(J2000_JD, 0.0), 0.0);
43///
44/// // One century later → t = 1
45/// assert_eq!(jd_to_centuries(J2000_JD, celestial_core::constants::DAYS_PER_JULIAN_CENTURY), 1.0);
46/// ```
47#[inline]
48pub fn jd_to_centuries(jd1: f64, jd2: f64) -> f64 {
49    ((jd1 - J2000_JD) + jd2) / DAYS_PER_JULIAN_CENTURY
50}
51
52/// Normalizes longitude to the range (-180°, 180°].
53///
54/// Wraps values outside the range by adding/subtracting 360°.
55#[inline]
56pub fn normalize_longitude(lon: f64) -> f64 {
57    let mut normalized = lon % 360.0;
58    if normalized > 180.0 {
59        normalized -= 360.0;
60    } else if normalized < -180.0 {
61        normalized += 360.0;
62    }
63    normalized
64}
65
66/// Clamps latitude to the valid range [-90°, 90°].
67///
68/// Values outside the range are clamped to the nearest pole.
69#[inline]
70pub fn normalize_latitude(lat: f64) -> f64 {
71    lat.clamp(-90.0, 90.0)
72}
73
74/// Normalizes an angle in radians to the range (-π, π].
75#[inline]
76pub fn normalize_angle_rad(angle: f64) -> f64 {
77    let mut normalized = angle % TWOPI;
78    if normalized > PI {
79        normalized -= TWOPI;
80    } else if normalized < -PI {
81        normalized += TWOPI;
82    }
83    normalized
84}
85
86/// Normalizes an angle in radians to the range [0, 2π).
87#[inline]
88pub fn normalize_angle_to_positive(angle: f64) -> f64 {
89    let mut a = angle % TWOPI;
90    if a < 0.0 {
91        a += TWOPI;
92    }
93    a
94}
95
96/// Computes the shortest signed angular difference `a - b` in degrees.
97///
98/// Handles wraparound at ±180°. The result is in the range (-180°, 180°].
99///
100/// # Example
101///
102/// ```
103/// use celestial_core::utils::angular_difference;
104///
105/// // Simple case
106/// assert_eq!(angular_difference(90.0, 45.0), 45.0);
107///
108/// // Across the 0°/360° boundary: 10° is 20° ahead of 350°
109/// assert!((angular_difference(10.0, 350.0) - 20.0).abs() < 1e-12);
110/// ```
111#[inline]
112pub fn angular_difference(a: f64, b: f64) -> f64 {
113    let mut diff = a - b;
114    if diff > 180.0 {
115        diff -= 360.0;
116    } else if diff < -180.0 {
117        diff += 360.0;
118    }
119    diff
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_jd_to_centuries_j2000() {
128        let t = jd_to_centuries(J2000_JD, 0.0);
129        assert_eq!(t, 0.0);
130    }
131
132    #[test]
133    fn test_jd_to_centuries_one_century() {
134        let t = jd_to_centuries(J2000_JD, crate::constants::DAYS_PER_JULIAN_CENTURY);
135        assert_eq!(t, 1.0);
136    }
137
138    #[test]
139    fn test_jd_to_centuries_negative() {
140        let t = jd_to_centuries(J2000_JD, -crate::constants::DAYS_PER_JULIAN_CENTURY);
141        assert_eq!(t, -1.0);
142    }
143
144    #[test]
145    fn test_jd_to_centuries_two_part() {
146        let t = jd_to_centuries(crate::constants::MJD_ZERO_POINT, 51544.5);
147        assert_eq!(t, 0.0);
148    }
149
150    #[test]
151    fn test_jd_to_centuries_precision() {
152        let jd2 = 0.123456789;
153        let t = jd_to_centuries(J2000_JD, jd2);
154        let expected = 0.123456789 / crate::constants::DAYS_PER_JULIAN_CENTURY;
155        assert!((t - expected).abs() < 1e-15);
156    }
157
158    #[test]
159    fn test_normalize_longitude() {
160        assert_eq!(normalize_longitude(0.0), 0.0);
161        assert_eq!(normalize_longitude(180.0), 180.0);
162        assert_eq!(normalize_longitude(-180.0), -180.0);
163        assert_eq!(normalize_longitude(181.0), -179.0);
164        assert_eq!(normalize_longitude(-181.0), 179.0);
165        assert_eq!(normalize_longitude(360.0), 0.0);
166        assert_eq!(normalize_longitude(720.0), 0.0);
167        assert_eq!(normalize_longitude(450.0), 90.0);
168    }
169
170    #[test]
171    fn test_normalize_latitude() {
172        assert_eq!(normalize_latitude(0.0), 0.0);
173        assert_eq!(normalize_latitude(45.0), 45.0);
174        assert_eq!(normalize_latitude(-45.0), -45.0);
175        assert_eq!(normalize_latitude(90.0), 90.0);
176        assert_eq!(normalize_latitude(-90.0), -90.0);
177        assert_eq!(normalize_latitude(100.0), 90.0);
178        assert_eq!(normalize_latitude(-100.0), -90.0);
179    }
180
181    #[test]
182    fn test_normalize_angle_rad() {
183        assert_eq!(normalize_angle_rad(0.0), 0.0);
184        assert!((normalize_angle_rad(PI) - PI).abs() < 1e-15);
185        assert!((normalize_angle_rad(-PI) - (-PI)).abs() < 1e-15);
186        assert!((normalize_angle_rad(TWOPI)).abs() < 1e-15);
187        assert!((normalize_angle_rad(3.0 * PI) - PI).abs() < 1e-15);
188    }
189
190    #[test]
191    fn test_angular_difference() {
192        assert_eq!(angular_difference(0.0, 0.0), 0.0);
193        assert_eq!(angular_difference(90.0, 45.0), 45.0);
194        assert_eq!(angular_difference(45.0, 90.0), -45.0);
195        assert!((angular_difference(10.0, 350.0) - 20.0).abs() < 1e-12);
196        assert!((angular_difference(-170.0, 170.0) - 20.0).abs() < 1e-12);
197        assert!((angular_difference(350.0, 10.0) + 20.0).abs() < 1e-12);
198    }
199
200    #[test]
201    fn test_normalize_angle_to_positive() {
202        assert_eq!(normalize_angle_to_positive(0.0), 0.0);
203        assert!((normalize_angle_to_positive(TWOPI)).abs() < 1e-15);
204        assert!((normalize_angle_to_positive(-PI) - PI).abs() < 1e-15);
205        assert!((normalize_angle_to_positive(3.0 * PI) - PI).abs() < 1e-15);
206        assert!(normalize_angle_to_positive(1.0) >= 0.0);
207        assert!(normalize_angle_to_positive(-1.0) >= 0.0);
208        assert!(normalize_angle_to_positive(-1.0) < TWOPI);
209    }
210}