space_dust/
time.rs

1//! Time systems and transformations for astrodynamics.
2//!
3//! This module provides representations and conversions between different
4//! astronomical time scales:
5//!
6//! - **UTC** - Coordinated Universal Time (civil time standard)
7//! - **TAI** - International Atomic Time (continuous, no leap seconds)
8//! - **TT** - Terrestrial Time (modern astronomical standard)
9//! - **GPS** - GPS Time
10//! - **JulianDate** - Julian Date and Modified Julian Date
11//! - **GMST** - Greenwich Mean Sidereal Time
12//!
13//! ## Time Scale Relationships
14//!
15//! ```text
16//! UTC <---> TAI <---> TT
17//!   |         |
18//!   |         +--> GPS
19//!   |
20//!   +--> JD/MJD
21//!   |
22//!   +--> GMST
23//! ```
24//!
25//! - TAI = UTC + leap_seconds
26//! - TT = TAI + 32.184s
27//! - GPS = TAI - 19s
28
29use crate::constants::Constants;
30use crate::data::LeapSeconds;
31use crate::math::poly_eval;
32use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
33use std::f64::consts::PI;
34
35// ============================================================================
36// UTC - Coordinated Universal Time
37// ============================================================================
38
39/// Coordinated Universal Time (UTC).
40///
41/// UTC is the primary civil time standard. It is kept within 0.9 seconds of UT1
42/// (astronomical time based on Earth's rotation) by the insertion of leap seconds.
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct UTC {
45    /// Seconds since Unix epoch (1970-01-01 00:00:00 UTC)
46    unix_seconds: f64,
47}
48
49impl UTC {
50    /// Create a new UTC time from Unix seconds.
51    pub fn new(unix_seconds: f64) -> Self {
52        Self { unix_seconds }
53    }
54
55    /// Create a UTC time from a chrono DateTime.
56    pub fn from_datetime(dt: &DateTime<Utc>) -> Self {
57        let secs = dt.timestamp() as f64;
58        let nanos = dt.timestamp_subsec_nanos() as f64 / 1_000_000_000.0;
59        Self::new(secs + nanos)
60    }
61
62    /// Convert to a chrono DateTime.
63    pub fn to_datetime(&self) -> DateTime<Utc> {
64        let secs = self.unix_seconds.floor() as i64;
65        let nanos = ((self.unix_seconds - secs as f64) * 1_000_000_000.0) as u32;
66        Utc.timestamp_opt(secs, nanos).unwrap()
67    }
68
69    /// Create from year, month, day, hour, minute, second components.
70    pub fn from_components(
71        year: i32,
72        month: u32,
73        day: u32,
74        hour: u32,
75        minute: u32,
76        second: f64,
77    ) -> Self {
78        let whole_second = second.floor() as u32;
79        let nanos = ((second - whole_second as f64) * 1_000_000_000.0) as u32;
80
81        let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
82        let time = NaiveTime::from_hms_nano_opt(hour, minute, whole_second, nanos).unwrap();
83        let naive_dt = NaiveDateTime::new(date, time);
84        let dt = DateTime::<Utc>::from_naive_utc_and_offset(naive_dt, Utc);
85
86        Self::from_datetime(&dt)
87    }
88
89    /// Get the Unix seconds value.
90    pub fn unix_seconds(&self) -> f64 {
91        self.unix_seconds
92    }
93
94    /// Convert to Julian Date.
95    pub fn to_jd(&self) -> f64 {
96        self.unix_seconds / Constants::SECONDS_PER_DAY + Constants::UNIX_EPOCH_JD
97    }
98
99    /// Create from Julian Date.
100    pub fn from_jd(jd: f64) -> Self {
101        let unix_seconds = (jd - Constants::UNIX_EPOCH_JD) * Constants::SECONDS_PER_DAY;
102        Self::new(unix_seconds)
103    }
104
105    /// Convert to Modified Julian Date.
106    pub fn to_mjd(&self) -> f64 {
107        self.to_jd() - Constants::MJD_OFFSET
108    }
109
110    /// Create from Modified Julian Date.
111    pub fn from_mjd(mjd: f64) -> Self {
112        Self::from_jd(mjd + Constants::MJD_OFFSET)
113    }
114
115    /// Add seconds to the UTC time.
116    pub fn add_seconds(&self, seconds: f64) -> Self {
117        Self::new(self.unix_seconds + seconds)
118    }
119
120    /// Difference between two UTC times in seconds.
121    pub fn diff(&self, other: &UTC) -> f64 {
122        self.unix_seconds - other.unix_seconds
123    }
124}
125
126impl From<DateTime<Utc>> for UTC {
127    fn from(dt: DateTime<Utc>) -> Self {
128        Self::from_datetime(&dt)
129    }
130}
131
132impl From<UTC> for DateTime<Utc> {
133    fn from(utc: UTC) -> Self {
134        utc.to_datetime()
135    }
136}
137
138impl Default for UTC {
139    fn default() -> Self {
140        Self::new(0.0)
141    }
142}
143
144// ============================================================================
145// TAI - International Atomic Time
146// ============================================================================
147
148/// International Atomic Time (TAI).
149///
150/// TAI is a continuous time scale based on atomic clocks. Unlike UTC, TAI does not
151/// include leap seconds, making it ideal for precise scientific calculations.
152///
153/// TAI = UTC + leap_seconds
154#[derive(Debug, Clone, Copy, PartialEq)]
155pub struct TAI {
156    /// TAI seconds since the epoch corresponding to Unix epoch
157    tai_seconds: f64,
158}
159
160impl TAI {
161    /// Create a new TAI time from TAI seconds.
162    pub fn new(tai_seconds: f64) -> Self {
163        Self { tai_seconds }
164    }
165
166    /// Get the TAI seconds value.
167    pub fn seconds(&self) -> f64 {
168        self.tai_seconds
169    }
170
171    /// Add seconds to the TAI time.
172    pub fn add_seconds(&self, seconds: f64) -> Self {
173        Self::new(self.tai_seconds + seconds)
174    }
175
176    /// Difference between two TAI times in seconds.
177    pub fn diff(&self, other: &TAI) -> f64 {
178        self.tai_seconds - other.tai_seconds
179    }
180
181    /// Convert to Julian Date (in TAI scale).
182    pub fn to_jd(&self) -> f64 {
183        self.tai_seconds / Constants::SECONDS_PER_DAY + Constants::UNIX_EPOCH_JD
184    }
185
186    /// Create from Julian Date (in TAI scale).
187    pub fn from_jd(jd: f64) -> Self {
188        let tai_seconds = (jd - Constants::UNIX_EPOCH_JD) * Constants::SECONDS_PER_DAY;
189        Self::new(tai_seconds)
190    }
191}
192
193impl Default for TAI {
194    fn default() -> Self {
195        Self::new(0.0)
196    }
197}
198
199// ============================================================================
200// TT - Terrestrial Time
201// ============================================================================
202
203/// Terrestrial Time (TT).
204///
205/// TT is the modern astronomical time standard for geocentric ephemerides.
206/// It provides a uniform time scale for planetary and lunar ephemeris calculations.
207///
208/// TT = TAI + 32.184 seconds
209#[derive(Debug, Clone, Copy, PartialEq)]
210pub struct TT {
211    /// TT seconds since the epoch corresponding to Unix epoch
212    tt_seconds: f64,
213}
214
215impl TT {
216    /// Create a new TT time from TT seconds.
217    pub fn new(tt_seconds: f64) -> Self {
218        Self { tt_seconds }
219    }
220
221    /// Get the TT seconds value.
222    pub fn seconds(&self) -> f64 {
223        self.tt_seconds
224    }
225
226    /// Add seconds to the TT time.
227    pub fn add_seconds(&self, seconds: f64) -> Self {
228        Self::new(self.tt_seconds + seconds)
229    }
230
231    /// Difference between two TT times in seconds.
232    pub fn diff(&self, other: &TT) -> f64 {
233        self.tt_seconds - other.tt_seconds
234    }
235
236    /// Convert to Julian Date (in TT scale).
237    pub fn to_jd(&self) -> f64 {
238        self.tt_seconds / Constants::SECONDS_PER_DAY + Constants::UNIX_EPOCH_JD
239    }
240
241    /// Create from Julian Date (in TT scale).
242    pub fn from_jd(jd: f64) -> Self {
243        let tt_seconds = (jd - Constants::UNIX_EPOCH_JD) * Constants::SECONDS_PER_DAY;
244        Self::new(tt_seconds)
245    }
246
247    /// Calculate Julian centuries since J2000.0.
248    /// This is commonly used for precession/nutation calculations.
249    pub fn julian_centuries_j2000(&self) -> f64 {
250        (self.to_jd() - Constants::J2000_JD) / Constants::JULIAN_CENTURY
251    }
252}
253
254impl Default for TT {
255    fn default() -> Self {
256        Self::new(0.0)
257    }
258}
259
260// ============================================================================
261// GPS - GPS Time
262// ============================================================================
263
264/// GPS Time.
265///
266/// GPS Time is a continuous time scale used by the Global Positioning System.
267/// It started at midnight UTC on January 6, 1980 (the GPS epoch) and does not
268/// include leap seconds.
269///
270/// GPS = TAI - 19 seconds
271#[derive(Debug, Clone, Copy, PartialEq)]
272pub struct GPS {
273    /// GPS seconds since GPS epoch (1980-01-06 00:00:00 UTC)
274    gps_seconds: f64,
275}
276
277impl GPS {
278    /// Create a new GPS time from GPS seconds (since GPS epoch).
279    pub fn new(gps_seconds: f64) -> Self {
280        Self { gps_seconds }
281    }
282
283    /// Get the GPS seconds value (since GPS epoch).
284    pub fn seconds(&self) -> f64 {
285        self.gps_seconds
286    }
287
288    /// Create from GPS week number and seconds of week.
289    pub fn from_week_and_seconds(week: u32, seconds_of_week: f64) -> Self {
290        let gps_seconds = week as f64 * Constants::SECONDS_PER_GPS_WEEK + seconds_of_week;
291        Self::new(gps_seconds)
292    }
293
294    /// Convert to GPS week number and seconds of week.
295    pub fn to_week_and_seconds(&self) -> (u32, f64) {
296        let week = (self.gps_seconds / Constants::SECONDS_PER_GPS_WEEK).floor() as u32;
297        let seconds_of_week = self.gps_seconds - week as f64 * Constants::SECONDS_PER_GPS_WEEK;
298        (week, seconds_of_week)
299    }
300
301    /// Get GPS week number.
302    pub fn week_number(&self) -> u32 {
303        (self.gps_seconds / Constants::SECONDS_PER_GPS_WEEK).floor() as u32
304    }
305
306    /// Get seconds of week (0 to 604800).
307    pub fn seconds_of_week(&self) -> f64 {
308        let week = self.week_number();
309        self.gps_seconds - week as f64 * Constants::SECONDS_PER_GPS_WEEK
310    }
311
312    /// Get day of week (0 = Sunday).
313    pub fn day_of_week(&self) -> u32 {
314        (self.seconds_of_week() / Constants::SECONDS_PER_DAY).floor() as u32
315    }
316
317    /// Add seconds to the GPS time.
318    pub fn add_seconds(&self, seconds: f64) -> Self {
319        Self::new(self.gps_seconds + seconds)
320    }
321
322    /// Difference between two GPS times in seconds.
323    pub fn diff(&self, other: &GPS) -> f64 {
324        self.gps_seconds - other.gps_seconds
325    }
326
327    /// GPS epoch in Unix seconds.
328    pub const GPS_EPOCH_UNIX: i64 = Constants::GPS_EPOCH_UNIX;
329}
330
331impl Default for GPS {
332    fn default() -> Self {
333        Self::new(0.0)
334    }
335}
336
337// ============================================================================
338// JulianDate - Julian Date
339// ============================================================================
340
341/// Julian Date (JD).
342///
343/// The Julian Date is a continuous count of days since the beginning of the
344/// Julian Period on January 1, 4713 BC (proleptic Julian calendar) at noon
345/// Universal Time.
346///
347/// Common reference epochs:
348/// - J2000.0: JD 2451545.0 (2000-01-01 12:00:00 TT)
349/// - Unix epoch: JD 2440587.5 (1970-01-01 00:00:00 UTC)
350///
351/// Modified Julian Date (MJD) = JD - 2400000.5
352#[derive(Debug, Clone, Copy, PartialEq)]
353pub struct JulianDate {
354    /// Julian Date value
355    jd: f64,
356}
357
358impl JulianDate {
359    /// Create a new Julian Date.
360    pub fn new(jd: f64) -> Self {
361        Self { jd }
362    }
363
364    /// Get the Julian Date value.
365    pub fn value(&self) -> f64 {
366        self.jd
367    }
368
369    /// Create from Modified Julian Date.
370    pub fn from_mjd(mjd: f64) -> Self {
371        Self::new(mjd + Constants::MJD_OFFSET)
372    }
373
374    /// Convert to Modified Julian Date.
375    pub fn to_mjd(&self) -> f64 {
376        self.jd - Constants::MJD_OFFSET
377    }
378
379    /// Get J2000.0 epoch as Julian Date.
380    pub fn j2000() -> Self {
381        Self::new(Constants::J2000_JD)
382    }
383
384    /// Calculate Julian centuries since J2000.0.
385    pub fn julian_centuries_j2000(&self) -> f64 {
386        (self.jd - Constants::J2000_JD) / Constants::JULIAN_CENTURY
387    }
388
389    /// Calculate Julian millennia since J2000.0.
390    pub fn julian_millennia_j2000(&self) -> f64 {
391        (self.jd - Constants::J2000_JD) / (Constants::JULIAN_CENTURY * 10.0)
392    }
393
394    /// Add days to the Julian Date.
395    pub fn add_days(&self, days: f64) -> Self {
396        Self::new(self.jd + days)
397    }
398
399    /// Add seconds to the Julian Date.
400    pub fn add_seconds(&self, seconds: f64) -> Self {
401        Self::new(self.jd + seconds / Constants::SECONDS_PER_DAY)
402    }
403
404    /// Difference between two Julian Dates in days.
405    pub fn diff_days(&self, other: &JulianDate) -> f64 {
406        self.jd - other.jd
407    }
408
409    /// Difference between two Julian Dates in seconds.
410    pub fn diff_seconds(&self, other: &JulianDate) -> f64 {
411        (self.jd - other.jd) * Constants::SECONDS_PER_DAY
412    }
413
414    /// Convert to year and fractional day of year.
415    /// Returns (year, day_of_year) where day_of_year is 1-based and fractional.
416    pub fn to_year_and_day(&self) -> (i32, f64) {
417        let jd = self.jd;
418        let z = (jd + 0.5).floor() as i64;
419        let f = jd + 0.5 - z as f64;
420
421        let a = if z < 2_299_161 {
422            z
423        } else {
424            let alpha = ((z as f64 - 1_867_216.25) / 36524.25).floor() as i64;
425            z + 1 + alpha - alpha / 4
426        };
427
428        let b = a + 1524;
429        let c = ((b as f64 - 122.1) / 365.25).floor() as i64;
430        let d = (365.25 * c as f64).floor() as i64;
431        let e = ((b - d) as f64 / 30.6001).floor() as i64;
432
433        let _day = (b - d) as f64 - (30.6001 * e as f64).floor() + f;
434
435        let month = if e < 14 { e - 1 } else { e - 13 };
436        let year = if month > 2 { c - 4716 } else { c - 4715 };
437
438        // Calculate day of year
439        let jan1_jd = Self::from_calendar(year as i32, 1, 1.0);
440        let day_of_year = jd - jan1_jd.jd + 1.0;
441
442        (year as i32, day_of_year)
443    }
444
445    /// Create Julian Date from calendar date.
446    pub fn from_calendar(year: i32, month: u32, day: f64) -> Self {
447        let (y, m) = if month <= 2 {
448            (year - 1, month + 12)
449        } else {
450            (year, month)
451        };
452
453        let a = y / 100;
454        let b = 2 - a + a / 4;
455
456        let jd = (365.25 * (y + 4716) as f64).floor()
457            + (30.6001 * (m + 1) as f64).floor()
458            + day
459            + b as f64
460            - 1524.5;
461
462        Self::new(jd)
463    }
464}
465
466impl Default for JulianDate {
467    fn default() -> Self {
468        Self::j2000()
469    }
470}
471
472// ============================================================================
473// GMST - Greenwich Mean Sidereal Time
474// ============================================================================
475
476/// Greenwich Mean Sidereal Time (GMST).
477///
478/// GMST is the hour angle of the mean vernal equinox measured from the Greenwich
479/// meridian. It represents the Earth's rotation angle and is essential for
480/// converting between Earth-fixed and inertial reference frames.
481///
482/// GMST differs from Greenwich Apparent Sidereal Time (GAST) by the equation
483/// of the equinoxes (nutation in right ascension).
484#[derive(Debug, Clone, Copy, PartialEq)]
485pub struct GMST {
486    /// GMST angle in radians
487    radians: f64,
488}
489
490impl GMST {
491    /// Create a new GMST from radians.
492    pub fn new(radians: f64) -> Self {
493        Self { radians }
494    }
495
496    /// Create from degrees.
497    pub fn from_degrees(degrees: f64) -> Self {
498        Self::new(degrees * Constants::DEG_TO_RAD)
499    }
500
501    /// Create from hours.
502    pub fn from_hours(hours: f64) -> Self {
503        Self::new(hours * PI / 12.0)
504    }
505
506    /// Get the GMST angle in radians.
507    pub fn to_radians(&self) -> f64 {
508        self.radians
509    }
510
511    /// Get the GMST angle in degrees.
512    pub fn to_degrees(&self) -> f64 {
513        self.radians * Constants::RAD_TO_DEG
514    }
515
516    /// Get the GMST angle in hours.
517    pub fn to_hours(&self) -> f64 {
518        self.radians * 12.0 / PI
519    }
520
521    /// Normalize to range [0, 2π).
522    pub fn normalize(&self) -> Self {
523        let mut normalized = self.radians % Constants::TWO_PI;
524        if normalized < 0.0 {
525            normalized += Constants::TWO_PI;
526        }
527        Self::new(normalized)
528    }
529}
530
531impl Default for GMST {
532    fn default() -> Self {
533        Self::new(0.0)
534    }
535}
536
537// ============================================================================
538// TimeTransforms - Time System Conversions
539// ============================================================================
540
541/// Time system transformations.
542///
543/// Provides conversions between different astronomical time scales.
544pub struct TimeTransforms;
545
546impl TimeTransforms {
547    // GMST polynomial coefficients (IAU 1982)
548    const GMST_POLY: [f64; 4] = [
549        -6.2e-6,        // T³ coefficient (seconds per century³)
550        0.093104,       // T² coefficient (seconds per century²)
551        8640184.812866, // T coefficient (seconds per century)
552        24110.54841,    // constant (seconds)
553    ];
554
555    // ========================================================================
556    // UTC <-> TAI conversions
557    // ========================================================================
558
559    /// Convert UTC to TAI.
560    ///
561    /// TAI = UTC + leap_seconds
562    pub fn utc_to_tai(utc: &UTC) -> TAI {
563        let leap_seconds = LeapSeconds::get_leap_seconds_at_jd(utc.to_jd());
564        TAI::new(utc.unix_seconds() + leap_seconds as f64)
565    }
566
567    /// Convert TAI to UTC.
568    ///
569    /// UTC = TAI - leap_seconds
570    ///
571    /// Note: This requires knowing the leap seconds at the TAI time, which
572    /// technically requires iterating since leap seconds are defined in UTC.
573    /// We use an approximation that works for normal use cases.
574    pub fn tai_to_utc(tai: &TAI) -> UTC {
575        // First approximation: use current TAI time to estimate UTC
576        let approx_utc_jd = tai.tai_seconds / Constants::SECONDS_PER_DAY + Constants::UNIX_EPOCH_JD;
577        let leap_seconds = LeapSeconds::get_leap_seconds_at_jd(approx_utc_jd);
578        UTC::new(tai.tai_seconds - leap_seconds as f64)
579    }
580
581    // ========================================================================
582    // TAI <-> TT conversions
583    // ========================================================================
584
585    /// Convert TAI to TT.
586    ///
587    /// TT = TAI + 32.184 seconds
588    pub fn tai_to_tt(tai: &TAI) -> TT {
589        TT::new(tai.tai_seconds + Constants::TT_TAI_OFFSET)
590    }
591
592    /// Convert TT to TAI.
593    ///
594    /// TAI = TT - 32.184 seconds
595    pub fn tt_to_tai(tt: &TT) -> TAI {
596        TAI::new(tt.tt_seconds - Constants::TT_TAI_OFFSET)
597    }
598
599    // ========================================================================
600    // TAI <-> GPS conversions
601    // ========================================================================
602
603    /// Convert TAI to GPS time.
604    ///
605    /// GPS = TAI - 19 seconds (offset since GPS epoch)
606    pub fn tai_to_gps(tai: &TAI) -> GPS {
607        // GPS epoch in TAI = Unix epoch + leap seconds at GPS epoch (19)
608        let gps_epoch_tai = Constants::GPS_EPOCH_UNIX as f64 + 19.0;
609        GPS::new(tai.tai_seconds - gps_epoch_tai + Constants::GPS_TAI_OFFSET)
610    }
611
612    /// Convert GPS time to TAI.
613    pub fn gps_to_tai(gps: &GPS) -> TAI {
614        let gps_epoch_tai = Constants::GPS_EPOCH_UNIX as f64 + 19.0;
615        TAI::new(gps.gps_seconds + gps_epoch_tai - Constants::GPS_TAI_OFFSET)
616    }
617
618    // ========================================================================
619    // UTC <-> TT convenience conversions
620    // ========================================================================
621
622    /// Convert UTC directly to TT.
623    pub fn utc_to_tt(utc: &UTC) -> TT {
624        let tai = Self::utc_to_tai(utc);
625        Self::tai_to_tt(&tai)
626    }
627
628    /// Convert TT directly to UTC.
629    pub fn tt_to_utc(tt: &TT) -> UTC {
630        let tai = Self::tt_to_tai(tt);
631        Self::tai_to_utc(&tai)
632    }
633
634    // ========================================================================
635    // UTC <-> GPS convenience conversions
636    // ========================================================================
637
638    /// Convert UTC directly to GPS time.
639    pub fn utc_to_gps(utc: &UTC) -> GPS {
640        let tai = Self::utc_to_tai(utc);
641        Self::tai_to_gps(&tai)
642    }
643
644    /// Convert GPS time directly to UTC.
645    pub fn gps_to_utc(gps: &GPS) -> UTC {
646        let tai = Self::gps_to_tai(gps);
647        Self::tai_to_utc(&tai)
648    }
649
650    // ========================================================================
651    // Julian Date conversions
652    // ========================================================================
653
654    /// Convert UTC to Julian Date.
655    pub fn utc_to_jd(utc: &UTC) -> JulianDate {
656        JulianDate::new(utc.to_jd())
657    }
658
659    /// Convert Julian Date to UTC.
660    pub fn jd_to_utc(jd: &JulianDate) -> UTC {
661        UTC::from_jd(jd.value())
662    }
663
664    // ========================================================================
665    // GMST calculations
666    // ========================================================================
667
668    /// Calculate GMST from UTC time using the IAU 1982 expression.
669    pub fn utc_to_gmst(utc: &UTC) -> GMST {
670        let jd = utc.to_jd();
671        Self::calculate_gmst(jd)
672    }
673
674    /// Calculate GMST from Julian Date.
675    pub fn jd_to_gmst(jd: &JulianDate) -> GMST {
676        Self::calculate_gmst(jd.value())
677    }
678
679    /// Internal GMST calculation using IAU 1982 formula.
680    fn calculate_gmst(jd: f64) -> GMST {
681        // Julian centuries from J2000.0
682        let t = (jd - Constants::J2000_JD) / Constants::JULIAN_CENTURY;
683
684        // GMST at 0h UT in seconds (polynomial evaluation)
685        let gmst_0h = poly_eval(&Self::GMST_POLY, t);
686
687        // Add the rotation for the fractional day
688        // Earth rotates 360.98564736629 degrees per day (sidereal)
689        let frac_day = (jd + 0.5).fract();
690        let rotation_seconds = frac_day * Constants::SECONDS_PER_DAY * 1.00273790935;
691
692        let gmst_seconds = gmst_0h + rotation_seconds;
693
694        // Convert to radians (24 hours = 2π radians)
695        let gmst_radians = gmst_seconds / Constants::SECONDS_PER_DAY * Constants::TWO_PI;
696
697        GMST::new(gmst_radians).normalize()
698    }
699
700    // ========================================================================
701    // DateTime conversions
702    // ========================================================================
703
704    /// Convert a chrono `DateTime<Utc>` to UTC.
705    pub fn datetime_to_utc(dt: &DateTime<Utc>) -> UTC {
706        UTC::from_datetime(dt)
707    }
708
709    /// Convert UTC to a chrono `DateTime<Utc>`.
710    pub fn utc_to_datetime(utc: &UTC) -> DateTime<Utc> {
711        utc.to_datetime()
712    }
713
714    /// Convert a chrono `DateTime<Utc>` directly to TT.
715    pub fn datetime_to_tt(dt: &DateTime<Utc>) -> TT {
716        let utc = UTC::from_datetime(dt);
717        Self::utc_to_tt(&utc)
718    }
719
720    /// Convert a chrono `DateTime<Utc>` directly to Julian Date.
721    pub fn datetime_to_jd(dt: &DateTime<Utc>) -> JulianDate {
722        let utc = UTC::from_datetime(dt);
723        Self::utc_to_jd(&utc)
724    }
725
726    /// Get Julian centuries since J2000.0 for a DateTime.
727    pub fn julian_centuries_j2000(dt: &DateTime<Utc>) -> f64 {
728        let tt = Self::datetime_to_tt(dt);
729        tt.julian_centuries_j2000()
730    }
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736
737    const EPSILON: f64 = 1e-9;
738
739    #[test]
740    fn test_utc_from_datetime() {
741        let dt = Utc.with_ymd_and_hms(2000, 1, 1, 12, 0, 0).unwrap();
742        let utc = UTC::from_datetime(&dt);
743        let back = utc.to_datetime();
744        assert_eq!(dt, back);
745    }
746
747    #[test]
748    fn test_utc_to_jd() {
749        // J2000.0 epoch: 2000-01-01 12:00:00 TT
750        // In UTC this is approximately 2000-01-01 11:58:55.816 UTC
751        // For simplicity, test a known date
752        let utc = UTC::from_components(2000, 1, 1, 12, 0, 0.0);
753        let jd = utc.to_jd();
754        // The JD should be close to J2000 (2451545.0)
755        assert!((jd - 2451545.0).abs() < 1.0);
756    }
757
758    #[test]
759    fn test_utc_to_mjd() {
760        let utc = UTC::from_jd(2451545.0);
761        let mjd = utc.to_mjd();
762        assert!((mjd - 51544.5).abs() < EPSILON);
763    }
764
765    #[test]
766    fn test_tai_conversion() {
767        let utc = UTC::from_components(2020, 1, 1, 0, 0, 0.0);
768        let tai = TimeTransforms::utc_to_tai(&utc);
769        let utc_back = TimeTransforms::tai_to_utc(&tai);
770
771        // Should be approximately equal (within leap second precision)
772        assert!((utc.unix_seconds() - utc_back.unix_seconds()).abs() < 1.0);
773    }
774
775    #[test]
776    fn test_tt_conversion() {
777        let tai = TAI::new(1000000.0);
778        let tt = TimeTransforms::tai_to_tt(&tai);
779        let tai_back = TimeTransforms::tt_to_tai(&tt);
780
781        assert!((tai.seconds() - tai_back.seconds()).abs() < EPSILON);
782    }
783
784    #[test]
785    fn test_gps_week_conversion() {
786        let gps = GPS::from_week_and_seconds(2000, 345600.0);
787        let (week, sow) = gps.to_week_and_seconds();
788        assert_eq!(week, 2000);
789        assert!((sow - 345600.0).abs() < EPSILON);
790    }
791
792    #[test]
793    fn test_julian_date_mjd() {
794        let jd = JulianDate::new(2451545.0);
795        let mjd = jd.to_mjd();
796        let jd_back = JulianDate::from_mjd(mjd);
797        assert!((jd.value() - jd_back.value()).abs() < EPSILON);
798    }
799
800    #[test]
801    fn test_julian_centuries() {
802        let jd = JulianDate::j2000();
803        let t = jd.julian_centuries_j2000();
804        assert!(t.abs() < EPSILON);
805
806        // One century later
807        let jd2 = JulianDate::new(Constants::J2000_JD + Constants::JULIAN_CENTURY);
808        let t2 = jd2.julian_centuries_j2000();
809        assert!((t2 - 1.0).abs() < EPSILON);
810    }
811
812    #[test]
813    fn test_gmst_normalize() {
814        let gmst = GMST::new(3.0 * PI);
815        let normalized = gmst.normalize();
816        assert!((normalized.to_radians() - PI).abs() < EPSILON);
817
818        let gmst_neg = GMST::new(-PI / 2.0);
819        let normalized_neg = gmst_neg.normalize();
820        assert!((normalized_neg.to_radians() - 3.0 * PI / 2.0).abs() < EPSILON);
821    }
822
823    #[test]
824    fn test_gmst_conversions() {
825        let gmst = GMST::from_hours(12.0);
826        assert!((gmst.to_radians() - PI).abs() < EPSILON);
827        assert!((gmst.to_degrees() - 180.0).abs() < EPSILON);
828        assert!((gmst.to_hours() - 12.0).abs() < EPSILON);
829    }
830
831    #[test]
832    fn test_julian_date_from_calendar() {
833        // Test J2000.0: 2000-01-01.5 (noon)
834        let jd = JulianDate::from_calendar(2000, 1, 1.5);
835        assert!((jd.value() - 2451545.0).abs() < EPSILON);
836    }
837
838    #[test]
839    fn test_utc_add_seconds() {
840        let utc1 = UTC::new(1000.0);
841        let utc2 = utc1.add_seconds(500.0);
842        assert!((utc2.unix_seconds() - 1500.0).abs() < EPSILON);
843    }
844
845    #[test]
846    fn test_utc_diff() {
847        let utc1 = UTC::new(1500.0);
848        let utc2 = UTC::new(1000.0);
849        assert!((utc1.diff(&utc2) - 500.0).abs() < EPSILON);
850    }
851}