1use std::{
11 ops::{Add, AddAssign, Sub, SubAssign},
12 time::Duration,
13};
14
15use crate::time::{MJD, UTC_LEAPS, UtcParams, UtcTime, WEEK, consts};
16
17#[derive(Debug, Copy, Clone)]
19pub struct GpsTime {
20 tow: f64,
22 wn: i16,
24}
25
26pub const GAL_TIME_START: GpsTime = GpsTime {
28 wn: consts::GAL_WEEK_TO_GPS_WEEK,
29 tow: consts::GAL_SECOND_TO_GPS_SECOND,
30};
31
32pub const BDS_TIME_START: GpsTime = GpsTime {
34 wn: consts::BDS_WEEK_TO_GPS_WEEK,
35 tow: consts::BDS_SECOND_TO_GPS_SECOND,
36};
37
38#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, thiserror::Error)]
40pub enum InvalidGpsTime {
41 #[error("Invalid Week Number: {0}")]
42 InvalidWN(i16),
44 #[error("Invalid Time of Week: {0}")]
45 InvalidTOW(f64),
47}
48
49impl GpsTime {
50 pub fn new(wn: i16, tow: f64) -> Result<GpsTime, InvalidGpsTime> {
58 if wn < 0 {
59 Err(InvalidGpsTime::InvalidWN(wn))
60 } else if !tow.is_finite() || tow < 0.0 || tow >= WEEK.as_secs_f64() {
61 Err(InvalidGpsTime::InvalidTOW(tow))
62 } else {
63 Ok(GpsTime { tow, wn })
64 }
65 }
66 pub(crate) const fn new_unchecked(wn: i16, tow: f64) -> GpsTime {
68 GpsTime { tow, wn }
69 }
70
71 #[must_use]
73 pub fn from_parts(
74 year: u16,
75 month: u8,
76 day: u8,
77 hour: u8,
78 minute: u8,
79 seconds: f64,
80 utc_params: &UtcParams,
81 ) -> GpsTime {
82 MJD::from_parts(year, month, day, hour, minute, seconds).to_gps(utc_params)
83 }
84
85 #[must_use]
92 pub fn from_parts_hardcoded(
93 year: u16,
94 month: u8,
95 day: u8,
96 hour: u8,
97 minute: u8,
98 seconds: f64,
99 ) -> GpsTime {
100 MJD::from_parts(year, month, day, hour, minute, seconds).to_gps_hardcoded()
101 }
102
103 #[must_use]
105 pub fn wn(&self) -> i16 {
106 self.wn
107 }
108
109 #[must_use]
111 pub fn tow(&self) -> f64 {
112 self.tow
113 }
114
115 #[must_use]
117 pub fn is_valid(&self) -> bool {
118 self.tow.is_finite()
119 && self.tow >= 0.0
120 && self.tow < f64::from(consts::WEEK_SECS)
121 && self.wn >= 0
122 }
123
124 fn normalize(&mut self) {
126 while self.tow < 0.0 {
127 self.tow += f64::from(consts::WEEK_SECS);
128 self.wn -= 1;
129 }
130
131 while self.tow >= f64::from(consts::WEEK_SECS) {
132 self.tow -= f64::from(consts::WEEK_SECS);
133 self.wn += 1;
134 }
135 }
136
137 pub fn add_duration(&mut self, duration: &Duration) {
139 self.tow += duration.as_secs_f64();
140 self.normalize();
141 }
142
143 pub fn subtract_duration(&mut self, duration: &Duration) {
145 self.tow -= duration.as_secs_f64();
146 self.normalize();
147 }
148
149 #[must_use]
151 pub fn diff(&self, other: &Self) -> f64 {
152 let dt = self.tow - other.tow;
153 dt + f64::from(self.wn - other.wn) * f64::from(consts::WEEK_SECS)
154 }
155
156 fn internal_to_utc(self, params: Option<&UtcParams>) -> UtcTime {
159 let (is_lse, dt_utc) = params.map_or_else(
162 || {
163 (
164 self.is_leap_second_event_hardcoded(),
165 self.gps_utc_offset_hardcoded(),
166 )
167 },
168 |p| (self.is_leap_second_event(p), self.gps_utc_offset(p)),
169 );
170
171 let mut tow_utc = self.tow - dt_utc;
172
173 if is_lse {
174 tow_utc -= 1.0;
178 }
179
180 let mut utc_time = GpsTime {
181 wn: self.wn,
182 tow: tow_utc,
183 };
184 utc_time.normalize();
185
186 let mut utc_time: UtcTime = UtcTime::from_gps_no_leap(utc_time);
188
189 if is_lse {
190 assert!(utc_time.hour() == 23);
191 assert!(utc_time.minute() == 59);
192 assert!(utc_time.seconds_int() == 59);
193 utc_time.add_second();
195 }
196
197 utc_time
198 }
199
200 #[must_use]
206 pub fn to_utc(self, utc_params: &UtcParams) -> UtcTime {
207 self.internal_to_utc(Some(utc_params))
208 }
209
210 #[must_use]
222 pub fn to_utc_hardcoded(self) -> UtcTime {
223 self.internal_to_utc(None)
224 }
225
226 pub(crate) fn gps_utc_offset(&self, utc_params: &UtcParams) -> f64 {
228 let dt = self.diff(&utc_params.tot());
229
230 let mut dt_utc: f64 =
232 utc_params.a0() + (utc_params.a1() * dt) + (utc_params.a2() * dt * dt);
233
234 if self.diff(&utc_params.t_lse()) >= 1.0 {
236 dt_utc += f64::from(utc_params.dt_lsf());
237 } else {
238 dt_utc += f64::from(utc_params.dt_ls());
239 }
240
241 dt_utc
242 }
243
244 pub(crate) fn gps_utc_offset_hardcoded(&self) -> f64 {
253 for (t_leap, offset) in UTC_LEAPS.iter().rev() {
254 if self.diff(t_leap) >= 1.0 {
255 return *offset;
256 }
257 }
258
259 0.0
261 }
262
263 pub(crate) fn utc_gps_offset(&self, utc_params: &UtcParams) -> f64 {
266 let dt = self.diff(&utc_params.tot()) + f64::from(utc_params.dt_ls());
267
268 let mut dt_utc = utc_params.a0() + utc_params.a1() * dt + utc_params.a2() * dt * dt;
270
271 if self.diff(&utc_params.t_lse()) >= (f64::from(-utc_params.dt_ls()) - dt_utc) {
273 dt_utc += f64::from(utc_params.dt_lsf());
274 } else {
275 dt_utc += f64::from(utc_params.dt_ls());
276 }
277
278 -dt_utc
279 }
280
281 pub(crate) fn utc_gps_offset_hardcoded(&self) -> f64 {
289 for (t_leap, offset) in UTC_LEAPS.iter().rev() {
290 if self.diff(t_leap) >= (-offset + 1.0) {
291 return -offset;
292 }
293 }
294
295 0.0
297 }
298
299 #[must_use]
301 pub fn is_leap_second_event(&self, params: &UtcParams) -> bool {
302 let dt = self.diff(¶ms.t_lse());
305
306 (0.0..1.0).contains(&dt)
308 }
309
310 #[must_use]
319 pub fn is_leap_second_event_hardcoded(&self) -> bool {
320 for (t_leap, _offset) in UTC_LEAPS.iter().rev() {
321 let dt = self.diff(t_leap);
322
323 if dt > 1.0 {
324 return false;
326 }
327 if (0.0..1.0).contains(&dt) {
328 return true;
330 }
331 }
332
333 false
335 }
336
337 #[must_use]
339 pub fn to_mjd(self, utc_params: &UtcParams) -> MJD {
340 self.to_utc(utc_params).to_mjd()
341 }
342
343 #[must_use]
352 pub fn to_mjd_hardcoded(self) -> MJD {
353 self.to_utc_hardcoded().to_mjd()
354 }
355
356 #[must_use]
358 pub fn round_to_epoch(&self, soln_freq: f64) -> GpsTime {
359 let rounded_tow = (self.tow * soln_freq).round() / soln_freq;
360 let mut rounded_time = Self::new_unchecked(self.wn, rounded_tow);
361 rounded_time.normalize();
363 rounded_time
364 }
365
366 #[must_use]
368 pub fn floor_to_epoch(&self, soln_freq: f64) -> GpsTime {
369 let rounded_tow = (self.tow * soln_freq).floor() / soln_freq;
371 let mut rounded_time = GpsTime::new_unchecked(self.wn, rounded_tow);
372 rounded_time.normalize();
374 rounded_time
375 }
376
377 #[must_use]
384 pub fn to_gal(self) -> GalTime {
385 assert!(self.is_valid());
386 assert!(self >= GAL_TIME_START);
387 GalTime {
388 wn: self.wn() - consts::GAL_WEEK_TO_GPS_WEEK,
389 tow: self.tow(),
390 }
391 }
392
393 #[must_use]
400 pub fn to_bds(self) -> BdsTime {
401 assert!(self.is_valid());
402 assert!(self >= BDS_TIME_START);
403 let bds = GpsTime {
404 wn: self.wn() - consts::BDS_WEEK_TO_GPS_WEEK,
405 tow: self.tow(),
406 };
407 let bds = bds - Duration::from_secs_f64(consts::BDS_SECOND_TO_GPS_SECOND);
408 BdsTime {
409 wn: bds.wn(),
410 tow: bds.tow(),
411 }
412 }
413
414 #[rustversion::since(1.62)]
415 #[must_use]
419 pub fn total_cmp(&self, other: &GpsTime) -> std::cmp::Ordering {
420 if self.wn() == other.wn() {
421 let other = other.tow();
422 self.tow().total_cmp(&other)
423 } else {
424 self.wn().cmp(&other.wn())
425 }
426 }
427
428 #[must_use]
437 pub fn to_fractional_year(&self, utc_params: &UtcParams) -> f64 {
438 let utc = self.to_utc(utc_params);
439 utc.to_fractional_year()
440 }
441
442 #[must_use]
457 pub fn to_fractional_year_hardcoded(&self) -> f64 {
458 let utc = self.to_utc_hardcoded();
459 utc.to_fractional_year()
460 }
461
462 #[must_use]
464 pub fn to_date(self, utc_params: &UtcParams) -> (u16, u8, u8, u8, u8, f64) {
465 self.to_utc(utc_params).to_date()
466 }
467
468 #[must_use]
476 pub fn to_date_hardcoded(self) -> (u16, u8, u8, u8, u8, f64) {
477 self.to_utc_hardcoded().to_date()
478 }
479}
480
481impl Default for GpsTime {
482 fn default() -> Self {
483 GpsTime::new_unchecked(0, 0.0)
484 }
485}
486
487impl PartialEq for GpsTime {
488 fn eq(&self, other: &Self) -> bool {
489 let diff_seconds = self.diff(other).abs();
490 diff_seconds < consts::JIFFY
491 }
492}
493
494impl PartialOrd for GpsTime {
495 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
496 let diff_seconds = self.diff(other);
497
498 if diff_seconds.abs() < consts::JIFFY {
499 Some(std::cmp::Ordering::Equal)
500 } else if diff_seconds > 0.0 {
501 Some(std::cmp::Ordering::Greater)
502 } else {
503 Some(std::cmp::Ordering::Less)
504 }
505 }
506}
507
508impl Add<Duration> for GpsTime {
509 type Output = Self;
510 fn add(mut self, rhs: Duration) -> Self {
511 self.add_duration(&rhs);
512 self
513 }
514}
515
516impl AddAssign<Duration> for GpsTime {
517 fn add_assign(&mut self, rhs: Duration) {
518 self.add_duration(&rhs);
519 }
520}
521
522impl Sub<Duration> for GpsTime {
523 type Output = Self;
524 fn sub(mut self, rhs: Duration) -> Self::Output {
525 self.subtract_duration(&rhs);
526 self
527 }
528}
529
530impl SubAssign<Duration> for GpsTime {
531 fn sub_assign(&mut self, rhs: Duration) {
532 self.subtract_duration(&rhs);
533 }
534}
535
536impl From<GalTime> for GpsTime {
537 fn from(gal: GalTime) -> Self {
538 gal.to_gps()
539 }
540}
541
542impl From<BdsTime> for GpsTime {
543 fn from(bds: BdsTime) -> Self {
544 bds.to_gps()
545 }
546}
547
548#[derive(Debug, Copy, Clone)]
550pub struct GalTime {
551 wn: i16,
552 tow: f64,
553}
554
555impl GalTime {
556 pub fn new(wn: i16, tow: f64) -> Result<GalTime, InvalidGpsTime> {
564 if wn < 0 {
565 Err(InvalidGpsTime::InvalidWN(wn))
566 } else if !tow.is_finite() || tow < 0.0 || tow >= WEEK.as_secs_f64() {
567 Err(InvalidGpsTime::InvalidTOW(tow))
568 } else {
569 Ok(GalTime { wn, tow })
570 }
571 }
572
573 #[must_use]
574 pub fn wn(&self) -> i16 {
575 self.wn
576 }
577
578 #[must_use]
579 pub fn tow(&self) -> f64 {
580 self.tow
581 }
582
583 #[must_use]
584 pub fn to_gps(self) -> GpsTime {
585 GpsTime {
586 wn: self.wn + consts::GAL_WEEK_TO_GPS_WEEK,
587 tow: self.tow,
588 }
589 }
590
591 #[must_use]
592 pub fn to_bds(self) -> BdsTime {
593 self.to_gps().to_bds()
594 }
595}
596
597impl From<GpsTime> for GalTime {
598 fn from(gps: GpsTime) -> Self {
599 gps.to_gal()
600 }
601}
602
603impl From<BdsTime> for GalTime {
604 fn from(bds: BdsTime) -> Self {
605 bds.to_gal()
606 }
607}
608
609#[derive(Debug, Copy, Clone)]
611pub struct BdsTime {
612 wn: i16,
613 tow: f64,
614}
615
616impl BdsTime {
617 pub fn new(wn: i16, tow: f64) -> Result<BdsTime, InvalidGpsTime> {
625 if wn < 0 {
626 Err(InvalidGpsTime::InvalidWN(wn))
627 } else if !tow.is_finite() || tow < 0.0 || tow >= WEEK.as_secs_f64() {
628 Err(InvalidGpsTime::InvalidTOW(tow))
629 } else {
630 Ok(BdsTime { wn, tow })
631 }
632 }
633
634 #[must_use]
635 pub fn wn(&self) -> i16 {
636 self.wn
637 }
638
639 #[must_use]
640 pub fn tow(&self) -> f64 {
641 self.tow
642 }
643
644 #[must_use]
645 pub fn to_gps(self) -> GpsTime {
646 let gps = GpsTime {
647 wn: self.wn() + consts::BDS_WEEK_TO_GPS_WEEK,
648 tow: self.tow(),
649 };
650 gps + Duration::from_secs_f64(consts::BDS_SECOND_TO_GPS_SECOND)
651 }
652
653 #[must_use]
654 pub fn to_gal(self) -> GalTime {
655 self.to_gps().to_gal()
656 }
657}
658
659impl From<GpsTime> for BdsTime {
660 fn from(gps: GpsTime) -> Self {
661 gps.to_bds()
662 }
663}
664
665impl From<GalTime> for BdsTime {
666 fn from(gal: GalTime) -> Self {
667 gal.to_bds()
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674
675 #[test]
676 fn validity() {
677 assert!(GpsTime::new(0, 0.0).is_ok());
678 assert!(GpsTime::new(-1, -1.0).is_err());
679 assert!(GpsTime::new(-1, -1.0).is_err());
680 assert!(GpsTime::new(12, WEEK.as_secs_f64()).is_err());
681 assert!(GpsTime::new(12, f64::NAN).is_err());
682 assert!(GpsTime::new(12, f64::INFINITY).is_err());
683 }
684
685 #[test]
686 fn equality() {
687 let t1 = GpsTime::new(10, 234.567).unwrap();
688 assert!(t1 == t1);
689
690 let t2 = GpsTime::new(10, 234.5678).unwrap();
691 assert!(t1 != t2);
692 assert!(t2 != t1);
693 }
694
695 #[test]
696 fn ordering() {
697 let t1 = GpsTime::new(10, 234.566).unwrap();
698 let t2 = GpsTime::new(10, 234.567).unwrap();
699 let t3 = GpsTime::new(10, 234.568).unwrap();
700
701 assert!(t1 < t2);
702 assert!(t1 < t3);
703 assert!(t2 > t1);
704 assert!(t2 < t3);
705 assert!(t3 > t1);
706 assert!(t3 > t2);
707
708 assert!(t1 <= t1);
709 assert!(t1 >= t1);
710 assert!(t1 <= t2);
711 assert!(t1 <= t3);
712 assert!(t2 >= t1);
713 assert!(t2 <= t2);
714 assert!(t2 >= t2);
715 assert!(t2 <= t3);
716 assert!(t3 >= t1);
717 assert!(t3 >= t2);
718 assert!(t3 <= t3);
719 assert!(t3 >= t3);
720 }
721
722 #[rustversion::since(1.62)]
723 #[test]
724 fn total_order() {
725 use std::cmp::Ordering;
726
727 let t1 = GpsTime::new(10, 234.566).unwrap();
728 let t2 = GpsTime::new(10, 234.567).unwrap();
729 let t3 = GpsTime::new(10, 234.568).unwrap();
730
731 assert!(t1.total_cmp(&t2) == Ordering::Less);
732 assert!(t2.total_cmp(&t3) == Ordering::Less);
733 assert!(t1.total_cmp(&t3) == Ordering::Less);
734
735 assert!(t2.total_cmp(&t1) == Ordering::Greater);
736 assert!(t3.total_cmp(&t2) == Ordering::Greater);
737 assert!(t3.total_cmp(&t1) == Ordering::Greater);
738
739 assert!(t1.total_cmp(&t1) == Ordering::Equal);
740 }
741
742 #[test]
743 fn add_duration() {
744 let mut t = GpsTime::new(0, 0.0).unwrap();
745 let t_expected = GpsTime::new(0, 1.001).unwrap();
746 let d = Duration::new(1, 1_000_000);
747
748 t.add_duration(&d);
749 assert_eq!(t, t_expected);
750
751 let t = GpsTime::new(0, 0.0).unwrap();
752 let t = t + d;
753 assert_eq!(t, t_expected);
754
755 let mut t = GpsTime::new(0, 0.0).unwrap();
756 t += d;
757 assert_eq!(t, t_expected);
758 }
759
760 #[test]
761 fn subtract_duration() {
762 let mut t = GpsTime::new(0, 1.001).unwrap();
763 let t_expected = GpsTime::new(0, 0.0).unwrap();
764 let d = Duration::new(1, 1_000_000);
765
766 t.subtract_duration(&d);
767 assert_eq!(t, t_expected);
768
769 t.subtract_duration(&d);
770 assert!(!t.is_valid());
771
772 let t = GpsTime::new(0, 1.001).unwrap();
773 let t = t - d;
774 assert_eq!(t, t_expected);
775
776 let mut t = GpsTime::new(0, 1.001).unwrap();
777 t -= d;
778 assert_eq!(t, t_expected);
779 }
780
781 #[test]
782 fn round_to_epoch() {
783 let soln_freq = 10.0;
784 let epsilon = 1e-5;
785
786 let test_cases = [
787 GpsTime::new_unchecked(1234, 567_890.01),
788 GpsTime::new_unchecked(1234, 567_890.050_1),
789 GpsTime::new_unchecked(1234, 604_800.06),
790 ];
791
792 let expectations = [
793 GpsTime::new_unchecked(1234, 567_890.00),
794 GpsTime::new_unchecked(1234, 567_890.10),
795 GpsTime::new_unchecked(1235, 0.1),
796 ];
797
798 for (test_case, expectation) in test_cases.iter().zip(expectations.iter()) {
799 let rounded = test_case.round_to_epoch(soln_freq);
800
801 let diff = if &rounded >= expectation {
802 rounded.diff(expectation)
803 } else {
804 expectation.diff(&rounded)
805 };
806 assert!(diff < epsilon);
807 }
808 }
809
810 #[test]
811 fn floor_to_epoch() {
812 let soln_freq = 10.0;
813 let epsilon = 1e-6;
814
815 let test_cases = [
816 GpsTime::new_unchecked(1234, 567_890.01),
817 GpsTime::new_unchecked(1234, 567_890.050_1),
818 GpsTime::new_unchecked(1234, 604_800.06),
819 ];
820
821 let expectations = [
822 GpsTime::new_unchecked(1234, 567_890.00),
823 GpsTime::new_unchecked(1234, 567_890.00),
824 GpsTime::new_unchecked(1235, 0.0),
825 ];
826
827 for (test_case, expectation) in test_cases.iter().zip(expectations.iter()) {
828 let rounded = test_case.floor_to_epoch(soln_freq);
829 assert!(rounded.diff(expectation) < epsilon);
830 }
831 }
832
833 #[test]
834 fn gps_to_gal() {
835 let gal = GAL_TIME_START.to_gal();
836 assert_eq!(gal.wn(), 0);
837 assert!(gal.tow().abs() < 1e-9);
838 let gps = gal.to_gps();
839 assert_eq!(gps.wn(), consts::GAL_WEEK_TO_GPS_WEEK);
840 assert!(gps.tow().abs() < 1e-9);
841
842 assert!(GalTime::new(-1, 0.0).is_err());
843 assert!(GalTime::new(0, -1.0).is_err());
844 assert!(GalTime::new(0, f64::from(consts::WEEK_SECS) + 1.0).is_err());
845 }
846
847 #[test]
848 fn gps_to_bds() {
849 let bds = BDS_TIME_START.to_bds();
850 assert_eq!(bds.wn(), 0);
851 assert!(bds.tow().abs() < 1e-9);
852 let gps = bds.to_gps();
853 assert_eq!(gps.wn(), consts::BDS_WEEK_TO_GPS_WEEK);
854 assert!((gps.tow() - consts::BDS_SECOND_TO_GPS_SECOND).abs() < 1e-9);
855
856 assert!(BdsTime::new(-1, 0.0).is_err());
857 assert!(BdsTime::new(0, -1.0).is_err());
858 assert!(BdsTime::new(0, f64::from(consts::WEEK_SECS) + 1.0).is_err());
859 }
860}