1use 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#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct UTC {
45 unix_seconds: f64,
47}
48
49impl UTC {
50 pub fn new(unix_seconds: f64) -> Self {
52 Self { unix_seconds }
53 }
54
55 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 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 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 pub fn unix_seconds(&self) -> f64 {
91 self.unix_seconds
92 }
93
94 pub fn to_jd(&self) -> f64 {
96 self.unix_seconds / Constants::SECONDS_PER_DAY + Constants::UNIX_EPOCH_JD
97 }
98
99 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 pub fn to_mjd(&self) -> f64 {
107 self.to_jd() - Constants::MJD_OFFSET
108 }
109
110 pub fn from_mjd(mjd: f64) -> Self {
112 Self::from_jd(mjd + Constants::MJD_OFFSET)
113 }
114
115 pub fn add_seconds(&self, seconds: f64) -> Self {
117 Self::new(self.unix_seconds + seconds)
118 }
119
120 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#[derive(Debug, Clone, Copy, PartialEq)]
155pub struct TAI {
156 tai_seconds: f64,
158}
159
160impl TAI {
161 pub fn new(tai_seconds: f64) -> Self {
163 Self { tai_seconds }
164 }
165
166 pub fn seconds(&self) -> f64 {
168 self.tai_seconds
169 }
170
171 pub fn add_seconds(&self, seconds: f64) -> Self {
173 Self::new(self.tai_seconds + seconds)
174 }
175
176 pub fn diff(&self, other: &TAI) -> f64 {
178 self.tai_seconds - other.tai_seconds
179 }
180
181 pub fn to_jd(&self) -> f64 {
183 self.tai_seconds / Constants::SECONDS_PER_DAY + Constants::UNIX_EPOCH_JD
184 }
185
186 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#[derive(Debug, Clone, Copy, PartialEq)]
210pub struct TT {
211 tt_seconds: f64,
213}
214
215impl TT {
216 pub fn new(tt_seconds: f64) -> Self {
218 Self { tt_seconds }
219 }
220
221 pub fn seconds(&self) -> f64 {
223 self.tt_seconds
224 }
225
226 pub fn add_seconds(&self, seconds: f64) -> Self {
228 Self::new(self.tt_seconds + seconds)
229 }
230
231 pub fn diff(&self, other: &TT) -> f64 {
233 self.tt_seconds - other.tt_seconds
234 }
235
236 pub fn to_jd(&self) -> f64 {
238 self.tt_seconds / Constants::SECONDS_PER_DAY + Constants::UNIX_EPOCH_JD
239 }
240
241 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 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#[derive(Debug, Clone, Copy, PartialEq)]
272pub struct GPS {
273 gps_seconds: f64,
275}
276
277impl GPS {
278 pub fn new(gps_seconds: f64) -> Self {
280 Self { gps_seconds }
281 }
282
283 pub fn seconds(&self) -> f64 {
285 self.gps_seconds
286 }
287
288 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 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 pub fn week_number(&self) -> u32 {
303 (self.gps_seconds / Constants::SECONDS_PER_GPS_WEEK).floor() as u32
304 }
305
306 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 pub fn day_of_week(&self) -> u32 {
314 (self.seconds_of_week() / Constants::SECONDS_PER_DAY).floor() as u32
315 }
316
317 pub fn add_seconds(&self, seconds: f64) -> Self {
319 Self::new(self.gps_seconds + seconds)
320 }
321
322 pub fn diff(&self, other: &GPS) -> f64 {
324 self.gps_seconds - other.gps_seconds
325 }
326
327 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#[derive(Debug, Clone, Copy, PartialEq)]
353pub struct JulianDate {
354 jd: f64,
356}
357
358impl JulianDate {
359 pub fn new(jd: f64) -> Self {
361 Self { jd }
362 }
363
364 pub fn value(&self) -> f64 {
366 self.jd
367 }
368
369 pub fn from_mjd(mjd: f64) -> Self {
371 Self::new(mjd + Constants::MJD_OFFSET)
372 }
373
374 pub fn to_mjd(&self) -> f64 {
376 self.jd - Constants::MJD_OFFSET
377 }
378
379 pub fn j2000() -> Self {
381 Self::new(Constants::J2000_JD)
382 }
383
384 pub fn julian_centuries_j2000(&self) -> f64 {
386 (self.jd - Constants::J2000_JD) / Constants::JULIAN_CENTURY
387 }
388
389 pub fn julian_millennia_j2000(&self) -> f64 {
391 (self.jd - Constants::J2000_JD) / (Constants::JULIAN_CENTURY * 10.0)
392 }
393
394 pub fn add_days(&self, days: f64) -> Self {
396 Self::new(self.jd + days)
397 }
398
399 pub fn add_seconds(&self, seconds: f64) -> Self {
401 Self::new(self.jd + seconds / Constants::SECONDS_PER_DAY)
402 }
403
404 pub fn diff_days(&self, other: &JulianDate) -> f64 {
406 self.jd - other.jd
407 }
408
409 pub fn diff_seconds(&self, other: &JulianDate) -> f64 {
411 (self.jd - other.jd) * Constants::SECONDS_PER_DAY
412 }
413
414 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 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 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#[derive(Debug, Clone, Copy, PartialEq)]
485pub struct GMST {
486 radians: f64,
488}
489
490impl GMST {
491 pub fn new(radians: f64) -> Self {
493 Self { radians }
494 }
495
496 pub fn from_degrees(degrees: f64) -> Self {
498 Self::new(degrees * Constants::DEG_TO_RAD)
499 }
500
501 pub fn from_hours(hours: f64) -> Self {
503 Self::new(hours * PI / 12.0)
504 }
505
506 pub fn to_radians(&self) -> f64 {
508 self.radians
509 }
510
511 pub fn to_degrees(&self) -> f64 {
513 self.radians * Constants::RAD_TO_DEG
514 }
515
516 pub fn to_hours(&self) -> f64 {
518 self.radians * 12.0 / PI
519 }
520
521 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
537pub struct TimeTransforms;
545
546impl TimeTransforms {
547 const GMST_POLY: [f64; 4] = [
549 -6.2e-6, 0.093104, 8640184.812866, 24110.54841, ];
554
555 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 pub fn tai_to_utc(tai: &TAI) -> UTC {
575 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 pub fn tai_to_tt(tai: &TAI) -> TT {
589 TT::new(tai.tai_seconds + Constants::TT_TAI_OFFSET)
590 }
591
592 pub fn tt_to_tai(tt: &TT) -> TAI {
596 TAI::new(tt.tt_seconds - Constants::TT_TAI_OFFSET)
597 }
598
599 pub fn tai_to_gps(tai: &TAI) -> GPS {
607 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 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 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 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 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 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 pub fn utc_to_jd(utc: &UTC) -> JulianDate {
656 JulianDate::new(utc.to_jd())
657 }
658
659 pub fn jd_to_utc(jd: &JulianDate) -> UTC {
661 UTC::from_jd(jd.value())
662 }
663
664 pub fn utc_to_gmst(utc: &UTC) -> GMST {
670 let jd = utc.to_jd();
671 Self::calculate_gmst(jd)
672 }
673
674 pub fn jd_to_gmst(jd: &JulianDate) -> GMST {
676 Self::calculate_gmst(jd.value())
677 }
678
679 fn calculate_gmst(jd: f64) -> GMST {
681 let t = (jd - Constants::J2000_JD) / Constants::JULIAN_CENTURY;
683
684 let gmst_0h = poly_eval(&Self::GMST_POLY, t);
686
687 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 let gmst_radians = gmst_seconds / Constants::SECONDS_PER_DAY * Constants::TWO_PI;
696
697 GMST::new(gmst_radians).normalize()
698 }
699
700 pub fn datetime_to_utc(dt: &DateTime<Utc>) -> UTC {
706 UTC::from_datetime(dt)
707 }
708
709 pub fn utc_to_datetime(utc: &UTC) -> DateTime<Utc> {
711 utc.to_datetime()
712 }
713
714 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 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 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 let utc = UTC::from_components(2000, 1, 1, 12, 0, 0.0);
753 let jd = utc.to_jd();
754 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 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 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 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}