swiftnav/time/
gnss.rs

1// Copyright (c) 2025 Swift Navigation Inc.
2// Contact: Swift Navigation <dev@swiftnav.com>
3//
4// This source is subject to the license found in the file 'LICENSE' which must
5// be be distributed together with this source. All other rights reserved.
6//
7// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
8// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
9// WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
10use std::{
11    ops::{Add, AddAssign, Sub, SubAssign},
12    time::Duration,
13};
14
15use crate::time::{MJD, UTC_LEAPS, UtcParams, UtcTime, WEEK, consts};
16
17/// Representation of GPS Time
18#[derive(Debug, Copy, Clone)]
19pub struct GpsTime {
20    /// Seconds since the GPS start of week.
21    tow: f64,
22    /// GPS week number
23    wn: i16,
24}
25
26/// GPS timestamp of the start of Galileo time
27pub const GAL_TIME_START: GpsTime = GpsTime {
28    wn: consts::GAL_WEEK_TO_GPS_WEEK,
29    tow: consts::GAL_SECOND_TO_GPS_SECOND,
30};
31
32/// GPS timestamp of the start of Beidou time
33pub 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/// Error type when a given GPS time is not valid
39#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, thiserror::Error)]
40pub enum InvalidGpsTime {
41    #[error("Invalid Week Number: {0}")]
42    /// Indicates an invalid week number was given, with the invalid value returned
43    InvalidWN(i16),
44    #[error("Invalid Time of Week: {0}")]
45    /// Indicates an invalid time of week was given, with the invalid value returned
46    InvalidTOW(f64),
47}
48
49impl GpsTime {
50    /// Makes a new GPS time object and checks the validity of the given values.
51    ///
52    /// # Errors
53    ///
54    /// An error will be returned if an invalid time is given. A valid time
55    /// must have a non-negative week number, and a time of week value between 0
56    /// and 604800.
57    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    /// Makes a new GPS time object without checking the validity of the given values.
67    pub(crate) const fn new_unchecked(wn: i16, tow: f64) -> GpsTime {
68        GpsTime { tow, wn }
69    }
70
71    /// Makes a new GPS time object from a date and time
72    #[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    /// Makes a new GPS time object from a date and time using a hardcoded list of leap seconds
86    ///
87    /// # ⚠️  🦘  ⏱  ⚠️  - Leap Seconds
88    ///
89    /// The hard coded list of leap seconds will get out of date, it is
90    /// preferable to use [`GpsTime::from_parts()`] with the newest set of UTC parameters
91    #[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    /// Gets the week number
104    #[must_use]
105    pub fn wn(&self) -> i16 {
106        self.wn
107    }
108
109    /// Gets the time of week
110    #[must_use]
111    pub fn tow(&self) -> f64 {
112        self.tow
113    }
114
115    /// Checks if the stored time is valid
116    #[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    /// Normalize time of week value so it's within the length of a week
125    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    /// Adds a duration to the time
138    pub fn add_duration(&mut self, duration: &Duration) {
139        self.tow += duration.as_secs_f64();
140        self.normalize();
141    }
142
143    /// Subtracts a duration from the time
144    pub fn subtract_duration(&mut self, duration: &Duration) {
145        self.tow -= duration.as_secs_f64();
146        self.normalize();
147    }
148
149    /// Gets the difference between this and another time value in seconds
150    #[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    /// Convert a [`GpsTime`] into a [`UtcTime`] using the [`UtcParams`] if
157    /// available, or hardcoded leap seconds otherwise
158    fn internal_to_utc(self, params: Option<&UtcParams>) -> UtcTime {
159        // Is it during a (positive) leap second event
160        // Get the UTC offset at the time we're converting
161        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            /* positive leap second event ongoing, so we are at 23:59:60.xxxx
175             * subtract one second from time for now to make the conversion
176             * into yyyy/mm/dd HH:MM:SS.sssss format, and add it back later */
177            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        /* break the time into components */
187        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            /* add the extra second back in */
194            utc_time.add_second();
195        }
196
197        utc_time
198    }
199
200    /// Converts the GPS time into UTC time
201    ///
202    /// # Panics
203    ///
204    /// This function will panic if the GPS time is not valid
205    #[must_use]
206    pub fn to_utc(self, utc_params: &UtcParams) -> UtcTime {
207        self.internal_to_utc(Some(utc_params))
208    }
209
210    /// Converts the GPS time into UTC time using the hardcoded list of leap
211    /// seconds.
212    ///
213    /// # ⚠️  🦘  ⏱  ⚠️  - Leap Seconds
214    ///
215    /// The hard coded list of leap seconds will get out of date, it is
216    /// preferable to use [`GpsTime::to_utc()`] with the newest set of UTC parameters
217    ///
218    /// # Panics
219    ///
220    /// This function will panic if the GPS time is not valid
221    #[must_use]
222    pub fn to_utc_hardcoded(self) -> UtcTime {
223        self.internal_to_utc(None)
224    }
225
226    /// Gets the number of seconds difference between GPS and UTC times
227    pub(crate) fn gps_utc_offset(&self, utc_params: &UtcParams) -> f64 {
228        let dt = self.diff(&utc_params.tot());
229
230        /* The polynomial UTC to GPS correction */
231        let mut dt_utc: f64 =
232            utc_params.a0() + (utc_params.a1() * dt) + (utc_params.a2() * dt * dt);
233
234        /* the new UTC offset takes effect after the leap second event */
235        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    /// Gets the number of seconds difference between GPS and UTC using the hardcoded
245    /// list of leap seconds
246    ///
247    /// # ⚠️  🦘  ⏱  ⚠️  - Leap Seconds
248    ///
249    /// The hard coded list of leap seconds will get out of date, it is
250    /// preferable to use [`GpsTime::gps_utc_offset()`] with the newest set
251    /// of UTC parameters
252    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        /* time is before the first known leap second event */
260        0.0
261    }
262
263    /// Gets the number of seconds difference between UTC and GPS using the hardcoded
264    /// list of leap seconds
265    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        /* The polynomial UTC to GPS correction */
269        let mut dt_utc = utc_params.a0() + utc_params.a1() * dt + utc_params.a2() * dt * dt;
270
271        /* the new UTC offset takes effect after the leap second event */
272        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    /// Gets the number of seconds difference between UTC and GPS using the hardcoded
282    /// list of leap seconds
283    ///
284    /// # ⚠️  🦘  ⏱  ⚠️  - Leap Seconds
285    /// The hard coded list of leap seconds will get out of date, it is
286    /// preferable to use [`GpsTime::utc_gps_offset()`] with the newest set
287    /// of UTC parameters
288    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        /* time is before the first known leap second event */
296        0.0
297    }
298
299    /// Checks to see if this point in time is a UTC leap second event
300    #[must_use]
301    pub fn is_leap_second_event(&self, params: &UtcParams) -> bool {
302        /* the UTC offset takes effect exactly 1 second after the start of
303         * the (positive) leap second event */
304        let dt = self.diff(&params.t_lse());
305
306        /* True only when self is during the leap second event */
307        (0.0..1.0).contains(&dt)
308    }
309
310    /// Checks to see if this point in time is a UTC leap second event using the
311    /// hardcoded list of leap seconds
312    ///
313    /// # ⚠️  🦘  ⏱  ⚠️  - Leap Seconds
314    ///
315    /// The hard coded list of leap seconds will get out of date, it is
316    /// preferable to use [`GpsTime::is_leap_second_event()`] with the newest
317    /// set of UTC parameters
318    #[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                /* time is past the last known leap second event */
325                return false;
326            }
327            if (0.0..1.0).contains(&dt) {
328                /* time is during the leap second event */
329                return true;
330            }
331        }
332
333        /* time is before the first known leap second event */
334        false
335    }
336
337    /// Converts the GPS time into a [`MJD`] (modified julian date)
338    #[must_use]
339    pub fn to_mjd(self, utc_params: &UtcParams) -> MJD {
340        self.to_utc(utc_params).to_mjd()
341    }
342
343    /// Converts the GPS time into a [`MJD`] (modified julian date) using a hard
344    /// coded list of leap seconds
345    ///
346    /// # ⚠️  🦘  ⏱  ⚠️  - Leap Seconds
347    ///
348    /// The hard coded list of leap seconds will get out of date, it is
349    /// preferable to use [`GpsTime::to_mjd()`] with the newest
350    /// set of UTC parameters
351    #[must_use]
352    pub fn to_mjd_hardcoded(self) -> MJD {
353        self.to_utc_hardcoded().to_mjd()
354    }
355
356    /// Gets the GPS time of the nearest solution epoch
357    #[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        /* handle case where rounding caused tow roll-over */
362        rounded_time.normalize();
363        rounded_time
364    }
365
366    /// Gets the GPS time of the previous solution epoch
367    #[must_use]
368    pub fn floor_to_epoch(&self, soln_freq: f64) -> GpsTime {
369        /* round the time-of-week */
370        let rounded_tow = (self.tow * soln_freq).floor() / soln_freq;
371        let mut rounded_time = GpsTime::new_unchecked(self.wn, rounded_tow);
372        /* handle case where rounding caused tow roll-over */
373        rounded_time.normalize();
374        rounded_time
375    }
376
377    /// Converts the GPS time into Galileo time
378    ///
379    /// # Panics
380    ///
381    /// This function will panic if the GPS time is before the start of Galileo
382    /// time, i.e. [`GAL_TIME_START`]
383    #[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    /// Converts the GPS time into Beidou time
394    ///
395    /// # Panics
396    ///
397    /// This function will panic if the GPS time is before the start of Beidou
398    /// time, i.e. [`BDS_TIME_START`]
399    #[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    /// Compare between itself and other `GpsTime`
416    /// Checks whether week number is same which then mirrors
417    /// [f64::total_cmp()](https://doc.rust-lang.org/std/primitive.f64.html#method.total_cmp)
418    #[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    /// Converts the GPS time into a fractional year
429    ///
430    /// # Notes
431    ///
432    /// A fractional year is a decimal representation of the date. For example
433    /// January 1, 2025 has a fractional year value of $2025.0$, while January
434    /// 30, 2025 is 30 days into the year so has a fractional year value of
435    /// approximately $2025.082$ ($30 \div 365 \approx 0.082$).
436    #[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    /// Converts the GPS time into a fractional year
443    ///
444    /// # Notes
445    ///
446    /// A fractional year is a decimal representation of the date. For example
447    /// January 1, 2025 has a fractional year value of $2025.0$, while January
448    /// 30, 2025 is 30 days into the year so has a fractional year value of
449    /// approximately $2025.082$ ($30 \div 365 \approx 0.082$).
450    ///
451    /// # ⚠️  🦘  ⏱  ⚠️  - Leap Seconds
452    ///
453    /// The hard coded list of leap seconds will get out of date, it is
454    /// preferable to use [`GpsTime::to_fractional_year()`] with the newest
455    /// set of UTC parameters
456    #[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    /// Converts the GPS time into a date and time
463    #[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    /// Converts the GPS time into a date and time
469    ///
470    /// # ⚠️  🦘  ⏱  ⚠️  - Leap Seconds
471    ///
472    /// The hard coded list of leap seconds will get out of date, it is
473    /// preferable to use [`GpsTime::to_date()`] with the newest
474    /// set of UTC parameters
475    #[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/// Representation of Galileo Time
549#[derive(Debug, Copy, Clone)]
550pub struct GalTime {
551    wn: i16,
552    tow: f64,
553}
554
555impl GalTime {
556    /// Makes a new Galileo time object and checks the validity of the given values.
557    ///
558    /// # Errors
559    ///
560    /// An error will be returned if an invalid time is given. A valid time
561    /// must have a non-negative week number, and a time of week value between 0
562    /// and 604800.
563    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/// Representation of Beidou Time
610#[derive(Debug, Copy, Clone)]
611pub struct BdsTime {
612    wn: i16,
613    tow: f64,
614}
615
616impl BdsTime {
617    /// Makes a new Beidou time object and checks the validity of the given values.
618    ///
619    /// # Errors
620    ///
621    /// An error will be returned if an invalid time is given. A valid time
622    /// must have a non-negative week number, and a time of week value between 0
623    /// and 604800.
624    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}