Skip to main content

gnss_time/
time.rs

1//! # `Time<S>` — the core timestamp type.
2//!
3//! Stores **nanoseconds since the epoch of scale `S`** in a `u64`.
4//! The phantom `S: TimeScale` enforces domain correctness at compile time —
5//! you cannot subtract a GPS timestamp from a GLONASS timestamp.
6//!
7//! ## Size guarantee
8//!
9//! ```rust
10//! # use gnss_time::{Time, scale::Gps};
11//!
12//! assert_eq!(core::mem::size_of::<Time<Gps>>(), 8); // identical to u64
13//! ```
14//!
15//! ## Representable range and overflow semantics
16//!
17//! The internal representation is a `u64` counting **nanoseconds from the
18//! scale's epoch**.  `u64::MAX` nanoseconds ≈ **584.5 years**, so:
19//!
20//! | Scale   | Epoch            | `Time::MAX` corresponds to |
21//! |---------|------------------|----------------------------|
22//! | GLONASS | 1996-01-01       | ≈ **2580-07-01**           |
23//! | GPS     | 1980-01-06       | ≈ **2564-07-04**           |
24//! | Galileo | 1999-08-22       | ≈ **2584-02-15**           |
25//! | BeiDou  | 2006-01-01       | ≈ **2590-07-02**           |
26//! | TAI     | 1958-01-01       | ≈ **2542-07-05**           |
27//! | UTC     | 1972-01-01       | ≈ **2556-07-03**           |
28//!
29//! All arithmetic is **checked by default** - panicking operators (`+`, `-`)
30//! are only suitable for cases you know cannot oberflow. Fo embedded code or
31//! long-running servers, prefer:
32//!
33//! ```rust
34//! use gnss_time::{scale::Gps, Duration, Time};
35//!
36//! let t = Time::<Gps>::MAX;
37//! let d = Duration::from_seconds(1);
38//!
39//! // Checked - returns None on overflow
40//! assert!(t.checked_add(d).is_none());
41//!
42//! // Saturating - clamps at MAX/EPOCH instead of panicking
43//! assert_eq!(t.saturating_add(d), Time::<Gps>::MAX);
44//!
45//! // Fallible - returns Err(GnssTimeError::Overflow)
46//! assert!(t.try_add(d).is_err());
47//! ```
48//!
49//! ## Lint: `arithmetic_overflow` is always an error
50//!
51//! This crate runs with `#[deny(arithmetic_overflow)]` in CI (via `clippy.toml`
52//! and `RUSTFLAGS=-D warnings`).  Do **not** add
53//! `#[allow(arithmetic_overflow)]` anywhere — instead use the
54//! checked/saturating/fallible variants above.
55
56use core::{
57    fmt,
58    marker::PhantomData,
59    ops::{Add, AddAssign, Sub, SubAssign},
60};
61
62use crate::{
63    gps_to_utc, utc_to_gps, DisplayStyle, Duration, Glonass, GnssTimeError, Gps, LeapSeconds,
64    LeapSecondsProvider, OffsetToTai, Tai, TimeScale, Utc,
65};
66
67/// A timestamp in time scale `S`, stored as nanoseconds since the epoch of the
68/// scale.
69///
70/// # Examples
71///
72/// ```rust
73/// use gnss_time::{Duration, Glonass, Gps, Time};
74///
75/// let t: Time<Gps> = Time::from_nanos(0); // GPS epoch
76/// let later = t + Duration::from_seconds(3600);
77///
78/// assert_eq!((later - t).as_seconds(), 3600);
79///
80/// // Compile-time error — different time scales are incompatible:
81/// // let glo: Time<Glonass> = Time::from_nanos(0);
82/// // let _ = later - glo; // ← ERROR
83/// ```
84#[derive(Copy, Clone, Eq, PartialEq, Hash)]
85#[must_use = "Time<S> is a value type; ignoring it has no effect"]
86pub struct Time<S: TimeScale> {
87    nanos: u64,
88    _scale: PhantomData<S>,
89}
90
91/// Split seconds into whole seconds and nanoseconds.
92///
93/// This type is used for GNSS week/day constructors so that the core API
94/// stays fully deterministic and `no_std`-friendly.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
96pub struct DurationParts {
97    /// Whole seconds part (non-negative).
98    pub seconds: u64,
99
100    /// Nanosecond part, must be in `[0, 999_999_999]`.
101    pub nanos: u32,
102}
103
104impl<S: TimeScale> Time<S> {
105    /// The scale's epoch — 0 nanoseconds.
106    ///
107    /// Corresponds to the calendar date defined by [`TimeScale::EPOCH_CIVIL`]
108    /// (e.g. `1980-01-06` for GPS, `1996-01-01` for GLONASS).
109    pub const EPOCH: Self = Time {
110        nanos: 0,
111        _scale: PhantomData,
112    };
113
114    /// Минимальное представляемое значение (синоним EPOCH).
115    pub const MIN: Self = Self::EPOCH;
116
117    /// Maximum representable instant.
118    ///
119    /// `u64::MAX` nanoseconds ≈ **584.5 years** past the scale's epoch.
120    ///
121    /// | Scale   | Epoch      | `MAX` ≈ calendar date |
122    /// |---------|------------|-----------------------|
123    /// | GLONASS | 1996-01-01 | 2580-07-01            |
124    /// | GPS     | 1980-01-06 | 2564-07-04            |
125    /// | Galileo | 1999-08-22 | 2584-02-15            |
126    /// | BeiDou  | 2006-01-01 | 2590-07-02            |
127    /// | TAI     | 1958-01-01 | 2542-07-05            |
128    /// | UTC     | 1972-01-01 | 2556-07-03            |
129    ///
130    /// Arithmetic near `MAX` **will** overflow — use
131    /// [`checked_add`](Self::checked_add),
132    /// [`saturating_add`](Self::saturating_add), or [`try_add`](Self::try_add).
133    pub const MAX: Self = Time {
134        nanos: u64::MAX,
135        _scale: PhantomData,
136    };
137
138    /// Nanoseconds per non-leap year (365 days).
139    ///
140    /// Useful for sanity-checking that a value is within a reasonable range:
141    /// ```rust
142    /// use gnss_time::{scale::Gps, Time};
143    ///
144    /// // 50 years from GPS epoch
145    /// let fifty_years = Time::<Gps>::from_nanos(50 * Time::<Gps>::NANOS_PER_YEAR);
146    ///
147    /// assert!(fifty_years.as_nanos() > 0);
148    /// ```
149    pub const NANOS_PER_YEAR: u64 = 365 * 24 * 3_600 * 1_000_000_000;
150
151    /// Construct from raw nanoseconds since this scale's epoch.
152    #[inline(always)]
153    pub const fn from_nanos(nanos: u64) -> Self {
154        Time {
155            nanos,
156            _scale: PhantomData,
157        }
158    }
159
160    /// Construct from whole seconds since this scale's epoch.
161    ///
162    /// # Panics
163    /// Panics if `secs * 1_000_000_000` overflows `u64`.
164    #[inline]
165    pub const fn from_seconds(secs: u64) -> Self {
166        match secs.checked_mul(1_000_000_000) {
167            Some(n) => Time::from_nanos(n),
168            None => panic!("Time::from_seconds overflow"),
169        }
170    }
171
172    /// Construct from whole seconds, returning `None` on overflow.
173    #[inline]
174    #[must_use = "returns None on overflow; check the result"]
175    pub const fn checked_from_seconds(secs: u64) -> Option<Self> {
176        match secs.checked_mul(1_000_000_000) {
177            Some(n) => Some(Time::from_nanos(n)),
178            None => None,
179        }
180    }
181}
182
183impl<S: TimeScale> Time<S> {
184    /// Raw nanoseconds since this scale's epoch.
185    #[inline(always)]
186    #[must_use]
187    pub const fn as_nanos(self) -> u64 {
188        self.nanos
189    }
190
191    /// Whole seconds since this scale's epoch (truncated).
192    #[inline]
193    #[must_use]
194    pub const fn as_seconds(self) -> u64 {
195        self.nanos / 1_000_000_000
196    }
197
198    /// Seconds as `f64`. For large timestamps (> ~2^53 ns), precision loss
199    /// affects even milliseconds
200    #[inline]
201    #[must_use]
202    pub fn as_seconds_f64(self) -> f64 {
203        self.nanos as f64 / 1_000_000_000.0
204    }
205}
206
207impl<S: TimeScale> Time<S> {
208    /// Convert to TAI using the scale's fixed offset.
209    ///
210    /// Returns [`GnssTimeError::LeapSecondsRequired`] for contextual scales
211    /// (UTC, GLONASS) and [`GnssTimeError::Overflow`] for out-of-range results.
212    pub fn to_tai(self) -> Result<Time<Tai>, GnssTimeError> {
213        match S::OFFSET_TO_TAI {
214            OffsetToTai::Fixed(offset) => {
215                let nanos = (self.nanos as i128) + (offset as i128);
216
217                if nanos < 0 || nanos > u64::MAX as i128 {
218                    return Err(GnssTimeError::Overflow);
219                }
220
221                Ok(Time::from_nanos(nanos as u64))
222            }
223            OffsetToTai::Contextual => Err(GnssTimeError::LeapSecondsRequired),
224        }
225    }
226
227    /// Construct `Time<S>` from a TAI timestamp using the scale's fixed offset.
228    pub fn from_tai(tai: Time<Tai>) -> Result<Self, GnssTimeError> {
229        match S::OFFSET_TO_TAI {
230            OffsetToTai::Fixed(offset) => {
231                let nanos = (tai.as_nanos() as i128) - (offset as i128);
232
233                if nanos < 0 || nanos > u64::MAX as i128 {
234                    return Err(GnssTimeError::Overflow);
235                }
236
237                Ok(Time::from_nanos(nanos as u64))
238            }
239            OffsetToTai::Contextual => Err(GnssTimeError::LeapSecondsRequired),
240        }
241    }
242
243    /// Convert directly between two fixed-offset scales via TAI.
244    ///
245    /// Fails if either source or target scale requires leap seconds.
246    pub fn try_convert<T: TimeScale>(self) -> Result<Time<T>, GnssTimeError> {
247        let tai = self.to_tai()?;
248
249        Time::<T>::from_tai(tai)
250    }
251}
252
253impl<S: TimeScale> Time<S> {
254    /// Add a `Duration`, returning `None` on overflow or underflow.
255    #[inline]
256    #[must_use = "returns None on overflow; check the result"]
257    pub fn checked_add(
258        self,
259        d: Duration,
260    ) -> Option<Self> {
261        let result = (self.nanos as i128) + (d.as_nanos() as i128);
262
263        if result < 0 || result > u64::MAX as i128 {
264            return None;
265        };
266
267        Some(Time::from_nanos(result as u64))
268    }
269
270    /// Subtract a `Duration`, returning `None` on overflow or underflow.
271    #[inline]
272    #[must_use = "returns None on underflow; check the result"]
273    pub fn checked_sub_duration(
274        self,
275        d: Duration,
276    ) -> Option<Self> {
277        let result = (self.nanos as i128) - (d.as_nanos() as i128);
278
279        if result < 0 || result > u64::MAX as i128 {
280            return None;
281        }
282
283        Some(Time::from_nanos(result as u64))
284    }
285
286    /// Add, saturating at `EPOCH` (below) and `MAX` (above).
287    #[inline]
288    #[must_use = "saturating_add returns a new Time<S>; the original is unchanged"]
289    pub fn saturating_add(
290        self,
291        d: Duration,
292    ) -> Self {
293        self.checked_add(d).unwrap_or(if d.is_negative() {
294            Time::EPOCH
295        } else {
296            Time::MAX
297        })
298    }
299
300    /// Subtract duration, saturating at bounds.
301    #[inline]
302    #[must_use = "saturating_sub_duration returns a new Time<S>; the original is unchanged"]
303    pub fn saturating_sub_duration(
304        self,
305        d: Duration,
306    ) -> Self {
307        self.checked_sub_duration(d).unwrap_or(if d.is_negative() {
308            Time::MAX
309        } else {
310            Time::EPOCH
311        })
312    }
313
314    /// Fallible add — [`GnssTimeError::Overflow`] on failure.
315    #[inline]
316    pub fn try_add(
317        self,
318        d: Duration,
319    ) -> Result<Self, GnssTimeError> {
320        self.checked_add(d).ok_or(GnssTimeError::Overflow)
321    }
322
323    /// Fallible subtract — [`GnssTimeError::Overflow`] on failure.
324    #[inline]
325    pub fn try_sub_duration(
326        self,
327        d: Duration,
328    ) -> Result<Self, GnssTimeError> {
329        self.checked_sub_duration(d).ok_or(GnssTimeError::Overflow)
330    }
331}
332
333impl<S: TimeScale> Time<S> {
334    /// Signed interval `self − earlier`. Returns `None` if it overflows `i64`.
335    #[inline]
336    #[must_use = "returns None on overflow; check the result"]
337    pub const fn checked_elapsed(
338        self,
339        earlier: Time<S>,
340    ) -> Option<Duration> {
341        let diff = (self.nanos as i128) - (earlier.nanos as i128);
342
343        if diff > i64::MAX as i128 || diff < i64::MIN as i128 {
344            return None;
345        }
346
347        Some(Duration::from_nanos(diff as i64))
348    }
349}
350
351impl<S: TimeScale> Add<Duration> for Time<S> {
352    type Output = Time<S>;
353
354    #[inline]
355    fn add(
356        self,
357        rhs: Duration,
358    ) -> Time<S> {
359        self.checked_add(rhs)
360            .expect("Time<S> + Duration overflowed")
361    }
362}
363
364impl<S: TimeScale> AddAssign<Duration> for Time<S> {
365    #[inline]
366    fn add_assign(
367        &mut self,
368        rhs: Duration,
369    ) {
370        *self = *self + rhs
371    }
372}
373
374impl<S: TimeScale> Sub<Duration> for Time<S> {
375    type Output = Time<S>;
376
377    #[inline]
378    fn sub(
379        self,
380        rhs: Duration,
381    ) -> Self::Output {
382        self.checked_sub_duration(rhs)
383            .expect("Time<S> - Duration underflowed")
384    }
385}
386
387impl<S: TimeScale> SubAssign<Duration> for Time<S> {
388    #[inline]
389    fn sub_assign(
390        &mut self,
391        rhs: Duration,
392    ) {
393        *self = *self - rhs;
394    }
395}
396
397impl<S: TimeScale> Sub<Time<S>> for Time<S> {
398    type Output = Duration;
399
400    #[inline]
401    fn sub(
402        self,
403        rhs: Time<S>,
404    ) -> Self::Output {
405        self.checked_elapsed(rhs)
406            .expect("Time<S> - Time<S> overflowed i64")
407    }
408}
409
410impl DurationParts {
411    /// Number of nanoseconds in one second.
412    pub const NANOS_PER_SECOND: u32 = 1_000_000_000;
413
414    /// Creates a new `DurationParts` from whole seconds and nanoseconds.
415    ///
416    /// # Parameters
417    /// - `seconds` – whole seconds (non‑negative)
418    /// - `nanos` – additional nanoseconds, **must be less than**
419    ///   `1_000_000_000`
420    ///
421    /// # Errors
422    /// Returns [`GnssTimeError::InvalidInput`] if `nanos >= 1_000_000_000`.
423    ///
424    /// # Example
425    /// ```
426    /// use gnss_time::DurationParts;
427    ///
428    /// let parts = DurationParts::new(5, 500_000_000).unwrap();
429    ///
430    /// assert_eq!(parts.as_nanos(), 5_500_000_000);
431    /// ```
432    #[inline]
433    pub const fn new(
434        seconds: u64,
435        nanos: u32,
436    ) -> Result<Self, GnssTimeError> {
437        if nanos >= Self::NANOS_PER_SECOND {
438            return Err(GnssTimeError::InvalidInput(
439                "nanos must be in [0, 1_000_000_000]",
440            ));
441        }
442
443        Ok(Self { seconds, nanos })
444    }
445
446    /// Converts the `DurationParts` into a total number of nanoseconds as
447    /// `u128`.
448    ///
449    /// # Example
450    /// ```
451    /// use gnss_time::DurationParts;
452    ///
453    /// let parts = DurationParts {
454    ///     seconds: 2,
455    ///     nanos: 123_456_789,
456    /// };
457    ///
458    /// assert_eq!(parts.as_nanos(), 2_123_456_789);
459    /// ```
460    #[inline]
461    #[must_use]
462    pub const fn as_nanos(self) -> u128 {
463        (self.seconds as u128) * Self::NANOS_PER_SECOND as u128 + self.nanos as u128
464    }
465}
466
467impl Time<Glonass> {
468    /// Construct from GLONASS day number and time-of-day.
469    ///
470    /// `tod.seconds` must be in `[0, 86_400)`.
471    /// `tod.nanos` must be in `[0, 1_000_000_000)`.
472    ///
473    /// # Errors
474    ///
475    /// [`GnssTimeError::InvalidInput`] if `tod_s ∉ [0, 86 400)`.
476    pub fn from_day_tod(
477        day: u32,
478        tod: DurationParts,
479    ) -> Result<Self, GnssTimeError> {
480        if tod.seconds >= 86_400 {
481            return Err(GnssTimeError::InvalidInput(
482                "tod.seconds must be in [0, 86_400)",
483            ));
484        }
485
486        if tod.nanos >= DurationParts::NANOS_PER_SECOND {
487            return Err(GnssTimeError::InvalidInput(
488                "tod.nanos must be in [0, 1_000_000_000)",
489            ));
490        }
491
492        let day_ns = (day as u64)
493            .checked_mul(86_400_000_000_000)
494            .ok_or(GnssTimeError::Overflow)?;
495
496        let tod_ns = tod
497            .seconds
498            .checked_mul(1_000_000_000)
499            .ok_or(GnssTimeError::Overflow)?
500            .checked_add(tod.nanos as u64)
501            .ok_or(GnssTimeError::Overflow)?;
502
503        let total = day_ns.checked_add(tod_ns).ok_or(GnssTimeError::Overflow)?;
504        Ok(Time::from_nanos(total))
505    }
506
507    /// Day number since GLONASS epoch.
508    #[inline]
509    #[must_use]
510    pub const fn day(self) -> u32 {
511        (self.nanos / 86_400_000_000_000u64) as u32
512    }
513
514    /// Time of day in whole seconds.
515    #[inline]
516    #[must_use]
517    pub const fn tod_seconds(self) -> u32 {
518        ((self.nanos % 86_400_000_000_000u64) / 1_000_000_000u64) as u32
519    }
520
521    /// Sub-second nanosecond remainder within the current second.
522    #[inline]
523    #[must_use]
524    pub const fn sub_second_nanos(self) -> u32 {
525        (self.nanos % 1_000_000_000u64) as u32
526    }
527
528    /// Day of week: **1 = Monday … 7 = Sunday** (NavIC / ISO 8601 convention).
529    ///
530    /// GLONASS epoch (1996-01-01) was a **Monday**, so day 0 → 1 (Monday).
531    ///
532    /// The formula is simply `(day % 7) + 1`.
533    ///
534    /// # GLONASS ICD note
535    ///
536    /// The GLONASS Interface Control Document defines the "day number within
537    /// the four-year interval" (`N_T`) starting from 1, but for simplicity
538    /// this crate uses 0-based day counts from the epoch and exposes the
539    /// ISO / NavIC weekday (1=Mon … 7=Sun) through this method.
540    ///
541    /// # Examples
542    ///
543    /// ```rust
544    /// use gnss_time::{scale::Glonass, DurationParts, Time};
545    ///
546    /// // Day 0 = 1996-01-01 = Monday
547    /// let t = Time::<Glonass>::from_day_tod(
548    ///     0,
549    ///     DurationParts {
550    ///         seconds: 0,
551    ///         nanos: 0,
552    ///     },
553    /// )
554    /// .unwrap();
555    ///
556    /// assert_eq!(t.day_of_week(), 1); // Monday
557    ///
558    /// // Day 6 = 1996-01-07 = Sunday
559    /// let t2 = Time::<Glonass>::from_day_tod(
560    ///     6,
561    ///     DurationParts {
562    ///         seconds: 0,
563    ///         nanos: 0,
564    ///     },
565    /// )
566    /// .unwrap();
567    ///
568    /// assert_eq!(t2.day_of_week(), 7); // Sunday
569    ///
570    /// // Day 7 = 1996-01-08 = Monday again
571    /// let t3 = Time::<Glonass>::from_day_tod(
572    ///     7,
573    ///     DurationParts {
574    ///         seconds: 0,
575    ///         nanos: 0,
576    ///     },
577    /// )
578    /// .unwrap();
579    ///
580    /// assert_eq!(t3.day_of_week(), 1);
581    /// ```
582    #[inline]
583    #[must_use]
584    pub const fn day_of_week(self) -> u8 {
585        // GLONASS epoch starts on Monday → day 0 corresponds to 1
586        (self.day() % 7) as u8 + 1
587    }
588
589    /// Returns `true` if the current day-of-week is Saturday (6) or Sunday (7).
590    #[inline]
591    #[must_use]
592    pub const fn is_weekend(self) -> bool {
593        let d = self.day_of_week();
594
595        d == 6 || d == 7
596    }
597}
598
599impl Time<Gps> {
600    /// Construct from GPS week number and time-of-week.
601    ///
602    /// `tow.seconds` must be in `[0, 604_800)`.
603    /// `tow.nanos` must be in `[0, 1_000_000_000)`.
604    pub fn from_week_tow(
605        week: u16,
606        tow: DurationParts,
607    ) -> Result<Self, GnssTimeError> {
608        if tow.seconds >= 604_800 {
609            return Err(GnssTimeError::InvalidInput(
610                "tow.seconds must be in [0, 604_800)",
611            ));
612        }
613
614        if tow.nanos >= DurationParts::NANOS_PER_SECOND {
615            return Err(GnssTimeError::InvalidInput(
616                "tow.nanos must be in [0, 1_000_000_000)",
617            ));
618        }
619
620        let week_ns = (week as u64)
621            .checked_mul(604_800_000_000_000)
622            .ok_or(GnssTimeError::Overflow)?;
623
624        let tow_ns = tow
625            .seconds
626            .checked_mul(1_000_000_000)
627            .ok_or(GnssTimeError::Overflow)?
628            .checked_add(tow.nanos as u64)
629            .ok_or(GnssTimeError::Overflow)?;
630
631        let total = week_ns.checked_add(tow_ns).ok_or(GnssTimeError::Overflow)?;
632        Ok(Time::from_nanos(total))
633    }
634
635    /// Conversion of GPS time to UTC using the built-in leap seconds table.
636    ///
637    /// # Accuracy
638    ///
639    /// For most timestamps, the conversion is accurate to the nanosecond.
640    /// During a leap second insertion window (e.g. 2016-12-31 23:59:60 UTC),
641    /// the result may differ by up to 1 second. If this is critical, use
642    /// [`to_utc_with`](Self::to_utc_with) with a custom provider that properly
643    /// handles the ambiguity.
644    pub fn to_utc(self) -> Result<Time<Utc>, GnssTimeError> {
645        gps_to_utc(self, LeapSeconds::builtin())
646    }
647
648    /// Conversion of GPS time to UTC using a custom leap seconds provider.
649    ///
650    /// The same accuracy notes apply as for [`to_utc`](Self::to_utc):
651    /// the conversion is precise for most timestamps, but during a leap second
652    /// insertion window it may differ by up to 1 second.
653    pub fn to_utc_with<P: LeapSecondsProvider>(
654        self,
655        ls: &P,
656    ) -> Result<Time<Utc>, GnssTimeError> {
657        gps_to_utc(self, ls)
658    }
659
660    /// GPS week number.
661    #[inline]
662    #[must_use]
663    pub const fn week(self) -> u32 {
664        (self.nanos / 604_800_000_000_000u64) as u32
665    }
666
667    /// Time of week in whole seconds.
668    #[inline]
669    #[must_use]
670    pub const fn tow_seconds(self) -> u32 {
671        ((self.nanos % 604_800_000_000_000u64) / 1_000_000_000u64) as u32
672    }
673
674    /// Sub-second nanosecond remainder within the current second.
675    #[inline]
676    #[must_use]
677    pub const fn sub_second_nanos(self) -> u32 {
678        (self.nanos % 1_000_000_000u64) as u32
679    }
680}
681
682impl Time<Utc> {
683    /// Conversion of UTC time to GPS using the built-in leap seconds table.
684    ///
685    /// # Accuracy
686    /// Same as in [`to_utc`](Time::<Gps>::to_utc) — ambiguity may occur during
687    /// leap second insertion events.
688    pub fn to_gps(self) -> Result<Time<Gps>, GnssTimeError> {
689        utc_to_gps(self, LeapSeconds::builtin())
690    }
691
692    /// Conversion of UTC time to GPS using a custom leap seconds provider.
693    ///
694    /// The same accuracy notes apply as for [`to_gps`](Self::to_gps):
695    /// ambiguity may occur during leap second insertion events.
696    pub fn to_gps_with<P: LeapSecondsProvider>(
697        self,
698        ls: &P,
699    ) -> Result<Time<Gps>, GnssTimeError> {
700        utc_to_gps(self, ls)
701    }
702}
703
704impl<S: TimeScale> PartialOrd for Time<S> {
705    #[inline]
706    fn partial_cmp(
707        &self,
708        other: &Self,
709    ) -> Option<core::cmp::Ordering> {
710        Some(self.cmp(other))
711    }
712}
713
714impl<S: TimeScale> Ord for Time<S> {
715    #[inline]
716    fn cmp(
717        &self,
718        other: &Self,
719    ) -> core::cmp::Ordering {
720        self.nanos.cmp(&other.nanos)
721    }
722}
723
724impl<S: TimeScale> fmt::Debug for Time<S> {
725    fn fmt(
726        &self,
727        f: &mut fmt::Formatter<'_>,
728    ) -> fmt::Result {
729        write!(f, "Time<{}>({}ns)", S::NAME, self.nanos)
730    }
731}
732
733impl<S: TimeScale> fmt::Display for Time<S> {
734    /// Formatting depends on the [`DisplayStyle`] of the time scale:
735    ///
736    /// | Style      | Example                    |
737    /// |------------|---------------------------|
738    /// | `WeekTow`  | `"GPS 2345:432000.000"`   |
739    /// | `DayTod`   | `"GLO 10512:43200.000"`   |
740    /// | `Simple`   | `"TAI +1000000000s 0ns"`  |
741    fn fmt(
742        &self,
743        f: &mut fmt::Formatter<'_>,
744    ) -> fmt::Result {
745        match S::DISPLAY_STYLE {
746            DisplayStyle::WeekTow => {
747                const WEEK_NS: u64 = 604_800_000_000_000;
748                let week = self.nanos / WEEK_NS;
749                let tow_ns = self.nanos % WEEK_NS;
750                let tow_s = tow_ns / 1_000_000_000;
751                let tow_ms = (tow_ns % 1_000_000_000) / 1_000_000;
752
753                write!(f, "{} {}:{:06}.{:03}", S::NAME, week, tow_s, tow_ms)
754            }
755            DisplayStyle::DayTod => {
756                const DAY_NS: u64 = 86_400_000_000_000;
757                let day = self.nanos / DAY_NS;
758                let tod_ns = self.nanos % DAY_NS;
759                let tod_s = tod_ns / 1_000_000_000;
760                let tod_ms = (tod_ns % 1_000_000_000) / 1_000_000;
761
762                write!(f, "{} {}:{:05}.{:03}", S::NAME, day, tod_s, tod_ms)
763            }
764            DisplayStyle::Simple => {
765                let secs = self.nanos / 1_000_000_000;
766                let ns_rem = self.nanos % 1_000_000_000;
767
768                write!(f, "{} +{}s {}ns", S::NAME, secs, ns_rem)
769            }
770        }
771    }
772}
773
774// defmt support
775
776#[cfg(feature = "defmt")]
777impl<S: TimeScale> defmt::Format for Time<S> {
778    fn format(
779        &self,
780        f: defmt::Formatter,
781    ) {
782        match S::DISPLAY_STYLE {
783            DisplayStyle::WeekTow => {
784                const WEEK_NS: u64 = 604_800_000_000_000;
785                let week = self.nanos / WEEK_NS;
786                let tow_ns = self.nanos % WEEK_NS;
787                let tow_s = tow_ns / 1_000_000_000;
788                let tow_ms = (tow_ns % 1_000_000_000) / 1_000_000;
789
790                defmt::write!(f, "{} {}:{:06}.{:03}", S::NAME, week, tow_s, tow_ms)
791            }
792            DisplayStyle::DayTod => {
793                const DAY_NS: u64 = 86_400_000_000_000;
794                let day = self.nanos / DAY_NS;
795                let tod_ns = self.nanos % DAY_NS;
796                let tod_s = tod_ns / 1_000_000_000;
797                let tod_ms = (tod_ns % 1_000_000_000) / 1_000_000;
798
799                defmt::write!(f, "{} {}:{:05}.{:03}", S::NAME, day, tod_s, tod_ms)
800            }
801            DisplayStyle::Simple => {
802                let secs = self.nanos / 1_000_000_000;
803                let ns_rem = self.nanos % 1_000_000_000;
804
805                defmt::write!(f, "{} +{}s {}ns", S::NAME, secs, ns_rem)
806            }
807        }
808    }
809}
810
811////////////////////////////////////////////////////////////////////////////////
812// Tests
813////////////////////////////////////////////////////////////////////////////////
814
815#[cfg(test)]
816mod tests {
817    #[allow(unused_imports)]
818    use std::format;
819    #[allow(unused_imports)]
820    use std::string::ToString;
821    #[allow(unused_imports)]
822    use std::vec;
823
824    use super::*;
825    use crate::scale::{Beidou, Galileo, Glonass, Gps, Tai, Utc};
826
827    #[test]
828    fn test_size_equals_u64() {
829        assert_eq!(core::mem::size_of::<Time<Gps>>(), 8);
830        assert_eq!(core::mem::size_of::<Time<Glonass>>(), 8);
831        assert_eq!(core::mem::size_of::<Time<Galileo>>(), 8);
832        assert_eq!(core::mem::size_of::<Time<Beidou>>(), 8);
833        assert_eq!(core::mem::size_of::<Time<Utc>>(), 8);
834        assert_eq!(core::mem::size_of::<Time<Tai>>(), 8);
835    }
836
837    #[test]
838    fn test_epoch_is_zero() {
839        assert_eq!(Time::<Gps>::EPOCH.as_nanos(), 0);
840    }
841
842    #[test]
843    fn test_from_week_tow_zero() {
844        let t = Time::<Gps>::from_week_tow(
845            0,
846            DurationParts {
847                seconds: 0,
848                nanos: 0,
849            },
850        )
851        .unwrap();
852
853        assert_eq!(t, Time::<Gps>::EPOCH);
854    }
855
856    #[test]
857    fn test_from_week_tow_roundtrip() {
858        let t = Time::<Gps>::from_week_tow(
859            2345,
860            DurationParts {
861                seconds: 432_000,
862                nanos: 0,
863            },
864        )
865        .unwrap();
866
867        assert_eq!(t.week(), 2345);
868        assert_eq!(t.tow_seconds(), 432_000);
869        assert_eq!(t.sub_second_nanos(), 0);
870    }
871
872    #[test]
873    fn test_from_week_tow_with_fractional() {
874        let t = Time::<Gps>::from_week_tow(
875            2300,
876            DurationParts {
877                seconds: 3661,
878                nanos: 500_000_000,
879            },
880        )
881        .unwrap();
882
883        assert_eq!(t.week(), 2300);
884        assert_eq!(t.tow_seconds(), 3661);
885        assert_eq!(t.sub_second_nanos(), 500_000_000);
886    }
887
888    #[test]
889    fn test_from_week_tow_invalid() {
890        assert!(matches!(
891            Time::<Gps>::from_week_tow(
892                0,
893                DurationParts {
894                    seconds: 604_800,
895                    nanos: 0
896                }
897            ),
898            Err(GnssTimeError::InvalidInput(_))
899        ));
900    }
901
902    #[test]
903    fn test_from_day_tod_zero() {
904        let t = Time::<Glonass>::from_day_tod(
905            0,
906            DurationParts {
907                seconds: 0,
908                nanos: 0,
909            },
910        )
911        .unwrap();
912
913        assert_eq!(t, Time::<Glonass>::EPOCH);
914    }
915
916    #[test]
917    fn test_from_day_tod_roundtrip() {
918        let t = Time::<Glonass>::from_day_tod(
919            10_512,
920            DurationParts {
921                seconds: 43_200,
922                nanos: 0,
923            },
924        )
925        .unwrap();
926
927        assert_eq!(t.day(), 10_512);
928        assert_eq!(t.tod_seconds(), 43_200);
929    }
930
931    #[test]
932    fn test_from_day_tod_invalid() {
933        assert!(matches!(
934            Time::<Glonass>::from_day_tod(
935                0,
936                DurationParts {
937                    seconds: 86_400,
938                    nanos: 0
939                }
940            ),
941            Err(GnssTimeError::InvalidInput(_))
942        ));
943    }
944
945    #[test]
946    fn test_add_positive_duration() {
947        let t = Time::<Gps>::from_seconds(100);
948
949        assert_eq!((t + Duration::from_seconds(50)).as_seconds(), 150);
950    }
951
952    #[test]
953    fn test_add_negative_duration_moves_back() {
954        let t = Time::<Gps>::from_seconds(100);
955
956        assert_eq!((t + Duration::from_nanos(-50_000_000_000)).as_seconds(), 50);
957    }
958
959    #[test]
960    fn test_roundtrip_add_sub() {
961        let t = Time::<Galileo>::from_seconds(1_000_000);
962        let d = Duration::from_seconds(12_345);
963
964        assert_eq!(t + d - d, t);
965    }
966
967    #[test]
968    fn test_sub_times_positive() {
969        let a = Time::<Gps>::from_seconds(200);
970        let b = Time::<Gps>::from_seconds(100);
971
972        assert_eq!((a - b).as_seconds(), 100);
973    }
974
975    #[test]
976    fn test_sub_times_negative() {
977        let a = Time::<Gps>::from_seconds(100);
978        let b = Time::<Gps>::from_seconds(200);
979
980        assert_eq!((a - b).as_seconds(), -100);
981    }
982
983    #[test]
984    fn test_sub_same_is_zero() {
985        let t = Time::<Gps>::from_seconds(42);
986
987        assert!((t - t).is_zero());
988    }
989
990    #[test]
991    #[should_panic]
992    fn test_add_overflow_panics() {
993        let _ = Time::<Gps>::MAX + Duration::ONE_NANOSECOND;
994    }
995
996    #[test]
997    fn test_checked_add_overflow() {
998        assert!(Time::<Gps>::MAX
999            .checked_add(Duration::ONE_NANOSECOND)
1000            .is_none());
1001    }
1002
1003    #[test]
1004    fn test_checked_sub_underflow() {
1005        assert!(Time::<Gps>::EPOCH
1006            .checked_sub_duration(Duration::ONE_NANOSECOND)
1007            .is_none());
1008    }
1009
1010    #[test]
1011    fn test_saturating_add_clamps() {
1012        assert_eq!(
1013            Time::<Gps>::MAX.saturating_add(Duration::from_seconds(1)),
1014            Time::<Gps>::MAX
1015        );
1016    }
1017
1018    #[test]
1019    fn test_gps_to_tai_adds_19s() {
1020        let gps = Time::<Gps>::from_seconds(100);
1021        let tai = gps.to_tai().unwrap();
1022
1023        assert_eq!(tai.as_seconds(), 119);
1024    }
1025
1026    #[test]
1027    fn test_tai_to_gps_subtracts_19s() {
1028        let tai = Time::<Tai>::from_seconds(119);
1029        let gps = Time::<Gps>::from_tai(tai).unwrap();
1030
1031        assert_eq!(gps.as_seconds(), 100);
1032    }
1033
1034    #[test]
1035    fn test_roundtrip_via_tai() {
1036        let original = Time::<Gps>::from_seconds(5_000_000);
1037        let back = Time::<Gps>::from_tai(original.to_tai().unwrap()).unwrap();
1038
1039        assert_eq!(original, back);
1040    }
1041
1042    #[test]
1043    fn test_gps_galileo_identity_via_tai() {
1044        // Same TAI offset → identical TAI instant → GPS→Galileo preserves nanoseconds
1045        let gps = Time::<Gps>::from_seconds(12_345);
1046        let gal = gps.try_convert::<Galileo>().unwrap();
1047
1048        assert_eq!(gps.as_nanos(), gal.as_nanos());
1049    }
1050
1051    #[test]
1052    fn test_gps_to_beidou_via_tai() {
1053        // GPS(100s) → TAI(119s) → BDT: 119 - 33 = 86s
1054        let gps = Time::<Gps>::from_seconds(100);
1055        let bdt = gps.try_convert::<Beidou>().unwrap();
1056
1057        assert_eq!(bdt.as_seconds(), 86);
1058    }
1059
1060    #[test]
1061    fn test_contextual_scale_to_tai_fails() {
1062        let glo = Time::<Glonass>::from_seconds(100);
1063
1064        assert!(matches!(
1065            glo.to_tai(),
1066            Err(GnssTimeError::LeapSecondsRequired)
1067        ));
1068    }
1069
1070    #[test]
1071    fn test_tai_to_contextual_fails() {
1072        let tai = Time::<Tai>::from_seconds(100);
1073
1074        assert!(matches!(
1075            Time::<Utc>::from_tai(tai),
1076            Err(GnssTimeError::LeapSecondsRequired)
1077        ));
1078    }
1079
1080    #[test]
1081    fn test_to_tai_overflow() {
1082        let t = Time::<Gps>::from_nanos(u64::MAX);
1083
1084        assert!(matches!(t.to_tai(), Err(GnssTimeError::Overflow)));
1085    }
1086
1087    #[test]
1088    fn test_from_tai_underflow() {
1089        // TAI(0) - 19s offset → negative GPS time → overflow
1090        let tai = Time::<Tai>::from_nanos(0);
1091
1092        assert!(matches!(
1093            Time::<Gps>::from_tai(tai),
1094            Err(GnssTimeError::Overflow)
1095        ));
1096    }
1097
1098    #[test]
1099    fn test_gps_display_week_tow_format() {
1100        let t = Time::<Gps>::from_week_tow(
1101            2345,
1102            DurationParts {
1103                seconds: 432_000,
1104                nanos: 0,
1105            },
1106        )
1107        .unwrap();
1108
1109        assert_eq!(t.to_string(), "GPS 2345:432000.000");
1110    }
1111
1112    #[test]
1113    fn test_gps_display_epoch_is_week_0() {
1114        let s = Time::<Gps>::EPOCH.to_string();
1115
1116        assert_eq!(s, "GPS 0:000000.000");
1117    }
1118
1119    #[test]
1120    fn test_gps_display_tow_zero_padded() {
1121        // TOW = 1 second → should be displayed as 000001
1122        let t = Time::<Gps>::from_week_tow(
1123            1,
1124            DurationParts {
1125                seconds: 1,
1126                nanos: 0,
1127            },
1128        )
1129        .unwrap();
1130
1131        assert_eq!(t.to_string(), "GPS 1:000001.000");
1132    }
1133
1134    #[test]
1135    fn test_gps_display_with_millis() {
1136        let t = Time::<Gps>::from_week_tow(
1137            100,
1138            DurationParts {
1139                seconds: 0,
1140                nanos: 500_000_000,
1141            },
1142        )
1143        .unwrap();
1144
1145        assert_eq!(t.to_string(), "GPS 100:000000.500");
1146    }
1147
1148    #[test]
1149    fn test_glonass_display_day_tod_format() {
1150        let t = Time::<Glonass>::from_day_tod(
1151            10_512,
1152            DurationParts {
1153                seconds: 43_200,
1154                nanos: 0,
1155            },
1156        )
1157        .unwrap();
1158
1159        assert_eq!(t.to_string(), "GLO 10512:43200.000");
1160    }
1161
1162    #[test]
1163    fn test_glonass_display_epoch() {
1164        let s = Time::<Glonass>::EPOCH.to_string();
1165
1166        assert_eq!(s, "GLO 0:00000.000");
1167    }
1168
1169    #[test]
1170    fn test_galileo_display_week_format() {
1171        let s = Time::<Galileo>::EPOCH.to_string();
1172
1173        assert!(s.starts_with("GAL "));
1174        assert!(s.contains(':'));
1175    }
1176
1177    #[test]
1178    fn test_tai_display_simple_format() {
1179        let t = Time::<Tai>::from_seconds(1_000_000_000);
1180        let s = t.to_string();
1181
1182        assert!(s.starts_with("TAI +"));
1183        assert!(s.contains("1000000000s"));
1184    }
1185
1186    #[test]
1187    fn test_utc_display_simple_format() {
1188        let s = Time::<Utc>::EPOCH.to_string();
1189
1190        assert!(s.starts_with("UTC +"));
1191    }
1192
1193    #[test]
1194    fn test_debug_shows_scale_and_nanos() {
1195        let t = Time::<Glonass>::from_nanos(777);
1196        let s = format!("{t:?}");
1197
1198        assert!(s.contains("GLO") && s.contains("777"));
1199    }
1200
1201    #[test]
1202    fn test_ordering() {
1203        let t0 = Time::<Gps>::from_seconds(0);
1204        let t1 = Time::<Gps>::from_seconds(1);
1205        let t2 = Time::<Gps>::from_seconds(2);
1206        let mut v = vec![t2, t0, t1];
1207
1208        v.sort();
1209
1210        assert_eq!(v, vec![t0, t1, t2]);
1211    }
1212
1213    #[test]
1214    fn test_glonass_day_accessor() {
1215        let t = Time::<Glonass>::from_day_tod(
1216            42,
1217            DurationParts {
1218                seconds: 3600,
1219                nanos: 0,
1220            },
1221        )
1222        .unwrap();
1223
1224        assert_eq!(t.day(), 42);
1225        assert_eq!(t.tod_seconds(), 3600);
1226    }
1227
1228    #[test]
1229    fn test_time_max_behavior() {
1230        let max = Time::<Gps>::MAX;
1231        let one_ns = Duration::ONE_NANOSECOND;
1232
1233        // checked_add returns None on overflow
1234        assert!(max.checked_add(one_ns).is_none());
1235
1236        // saturating_add clamps at MAX
1237        assert_eq!(max.saturating_add(one_ns), max);
1238
1239        // try_add returns error on overflow
1240        assert!(max.try_add(one_ns).is_err());
1241    }
1242
1243    #[test]
1244    fn test_max_is_u64_max() {
1245        assert_eq!(Time::<Gps>::MAX.as_nanos(), u64::MAX);
1246        assert_eq!(Time::<Glonass>::MAX.as_nanos(), u64::MAX);
1247        assert_eq!(Time::<Galileo>::MAX.as_nanos(), u64::MAX);
1248        assert_eq!(Time::<Beidou>::MAX.as_nanos(), u64::MAX);
1249        assert_eq!(Time::<Tai>::MAX.as_nanos(), u64::MAX);
1250        assert_eq!(Time::<Utc>::MAX.as_nanos(), u64::MAX);
1251    }
1252
1253    #[test]
1254    fn test_nanos_per_year_is_correct() {
1255        let expected: u64 = 365 * 24 * 3_600 * 1_000_000_000;
1256
1257        assert_eq!(Time::<Gps>::NANOS_PER_YEAR, expected);
1258    }
1259
1260    #[test]
1261    fn test_max_covers_at_least_500_years() {
1262        let years = Time::<Gps>::MAX.as_nanos() / Time::<Gps>::NANOS_PER_YEAR;
1263
1264        assert!(
1265            years >= 500,
1266            "MAX should cover at least 500 years, got {years}"
1267        );
1268    }
1269
1270    #[test]
1271    fn test_checked_add_one_ns_before_max_succeeds() {
1272        let t = Time::<Gps>::from_nanos(u64::MAX - 1);
1273        let result = t.checked_add(Duration::from_nanos(1));
1274
1275        assert_eq!(result, Some(Time::<Gps>::MAX));
1276    }
1277
1278    #[test]
1279    fn test_checked_add_at_max_overflows() {
1280        assert!(Time::<Gps>::MAX
1281            .checked_add(Duration::from_nanos(1))
1282            .is_none());
1283    }
1284
1285    #[test]
1286    fn test_checked_add_large_positive_overflows() {
1287        let t = Time::<Gps>::from_nanos(u64::MAX - 100);
1288
1289        assert!(t.checked_add(Duration::from_seconds(1)).is_none());
1290    }
1291
1292    #[test]
1293    fn test_checked_sub_one_ns_after_epoch_succeeds() {
1294        let t = Time::<Gps>::from_nanos(1);
1295        let result = t.checked_sub_duration(Duration::from_nanos(1));
1296
1297        assert_eq!(result, Some(Time::<Gps>::EPOCH));
1298    }
1299
1300    #[test]
1301    fn test_checked_sub_at_epoch_underflows() {
1302        assert!(Time::<Gps>::EPOCH
1303            .checked_sub_duration(Duration::from_nanos(1))
1304            .is_none());
1305    }
1306
1307    #[test]
1308    fn test_checked_sub_large_amount_underflows() {
1309        let t = Time::<Gps>::from_nanos(50);
1310
1311        assert!(t.checked_sub_duration(Duration::from_seconds(1)).is_none());
1312    }
1313
1314    #[test]
1315    fn test_saturating_add_clamps_at_max() {
1316        assert_eq!(
1317            Time::<Gps>::MAX.saturating_add(Duration::from_nanos(1)),
1318            Time::<Gps>::MAX
1319        );
1320        assert_eq!(
1321            Time::<Gps>::MAX.saturating_add(Duration::from_seconds(9999)),
1322            Time::<Gps>::MAX
1323        );
1324    }
1325
1326    #[test]
1327    fn test_saturating_add_negative_clamps_at_epoch() {
1328        assert_eq!(
1329            Time::<Gps>::EPOCH.saturating_add(Duration::from_nanos(-1)),
1330            Time::<Gps>::EPOCH
1331        );
1332    }
1333
1334    #[test]
1335    fn test_saturating_add_normal_value_works() {
1336        let t = Time::<Gps>::from_seconds(100);
1337
1338        assert_eq!(
1339            t.saturating_add(Duration::from_seconds(50)),
1340            Time::<Gps>::from_seconds(150)
1341        );
1342    }
1343
1344    #[test]
1345    fn test_saturating_sub_clamps_at_epoch() {
1346        assert_eq!(
1347            Time::<Gps>::EPOCH.saturating_sub_duration(Duration::from_nanos(1)),
1348            Time::<Gps>::EPOCH
1349        );
1350    }
1351
1352    #[test]
1353    fn test_saturating_sub_normal_value_works() {
1354        let t = Time::<Gps>::from_seconds(100);
1355
1356        assert_eq!(
1357            t.saturating_sub_duration(Duration::from_seconds(30)),
1358            Time::<Gps>::from_seconds(70)
1359        );
1360    }
1361
1362    #[test]
1363    fn test_try_add_overflow_returns_err() {
1364        let result = Time::<Gps>::MAX.try_add(Duration::from_nanos(1));
1365
1366        assert!(matches!(result, Err(GnssTimeError::Overflow)));
1367    }
1368
1369    #[test]
1370    fn test_try_sub_duration_underflow_returns_err() {
1371        let result = Time::<Gps>::EPOCH.try_sub_duration(Duration::from_nanos(1));
1372
1373        assert!(matches!(result, Err(GnssTimeError::Overflow)));
1374    }
1375
1376    #[test]
1377    fn test_try_add_valid_value_works() {
1378        let t = Time::<Gps>::from_seconds(1_000);
1379        let result = t.try_add(Duration::from_seconds(500)).unwrap();
1380
1381        assert_eq!(result.as_seconds(), 1_500);
1382    }
1383
1384    #[test]
1385    #[should_panic]
1386    fn test_add_operator_panics_at_max() {
1387        let _ = Time::<Gps>::MAX + Duration::from_nanos(1);
1388    }
1389
1390    #[test]
1391    #[should_panic]
1392    fn test_sub_operator_panics_at_epoch() {
1393        let _ = Time::<Gps>::EPOCH - Duration::from_nanos(1);
1394    }
1395
1396    #[test]
1397    fn test_checked_elapsed_zero_gives_zero_duration() {
1398        let t = Time::<Gps>::from_seconds(1_000);
1399        assert_eq!(t.checked_elapsed(t), Some(Duration::ZERO));
1400    }
1401
1402    #[test]
1403    fn test_checked_elapsed_overflows_when_gap_exceeds_i64() {
1404        // MAX - EPOCH = u64::MAX nanoseconds; i64 can hold roughly half of this range
1405        // The difference u64::MAX fits into i128, but not into i64 → None
1406        let result = Time::<Gps>::MAX.checked_elapsed(Time::<Gps>::EPOCH);
1407
1408        assert!(result.is_none(), "gap exceeds i64::MAX so must return None");
1409    }
1410
1411    #[test]
1412    fn test_checked_elapsed_within_i64_range_works() {
1413        let a = Time::<Gps>::from_seconds(1_000_000);
1414        let b = Time::<Gps>::from_seconds(500_000);
1415        let elapsed = a.checked_elapsed(b).unwrap();
1416
1417        assert_eq!(elapsed.as_seconds(), 500_000);
1418    }
1419}