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//! ## Unix time interoperability
30//!
31//! `Time<Utc>` counts nanoseconds from **1972-01-01** (UTC epoch), while Unix
32//! time starts from **1970-01-01**. The gap is [`UTC_EPOCH_UNIX_OFFSET_S`] =
33//! 63 072 000 s (730 days).
34//!
35//! ```rust
36//! use gnss_time::{Time, Utc};
37//!
38//! // Unix epoch (1970-01-01) is before the UTC epoch (1972-01-01) → error
39//! assert!(Time::<Utc>::from_unix_seconds(0).is_err());
40//!
41//! // 1972-01-01 = UTC epoch
42//! let utc = Time::<Utc>::from_unix_seconds(63_072_000).unwrap();
43//! assert_eq!(utc, Time::<Utc>::EPOCH);
44//! assert_eq!(utc.as_unix_seconds(), 63_072_000);
45//! ```
46//!
47//! ## Arithmetic overflow semantics
48//!
49//! All arithmetic is **checked by default** - panicking operators (`+`, `-`)
50//! are only suitable for cases you know cannot oberflow. Fo embedded code or
51//! long-running servers, prefer:
52//!
53//! ```rust
54//! use gnss_time::{scale::Gps, Duration, Time};
55//!
56//! let t = Time::<Gps>::MAX;
57//! let d = Duration::from_seconds(1);
58//!
59//! // Checked - returns None on overflow
60//! assert!(t.checked_add(d).is_none());
61//!
62//! // Saturating - clamps at MAX/EPOCH instead of panicking
63//! assert_eq!(t.saturating_add(d), Time::<Gps>::MAX);
64//!
65//! // Fallible - returns Err(GnssTimeError::Overflow)
66//! assert!(t.try_add(d).is_err());
67//! ```
68
69use core::{
70    fmt,
71    marker::PhantomData,
72    ops::{Add, AddAssign, Sub, SubAssign},
73};
74
75use crate::{
76    gps_to_utc, utc_to_gps, CivilDateTime, DisplayStyle, Duration, Glonass, GnssTimeError, Gps,
77    LeapSeconds, LeapSecondsProvider, OffsetToTai, Tai, TimeScale, Utc, UTC_EPOCH_UNIX_OFFSET_NS,
78    UTC_EPOCH_UNIX_OFFSET_S,
79};
80
81/// A timestamp in time scale `S`, stored as nanoseconds since the epoch of the
82/// scale.
83///
84/// # Examples
85///
86/// ```rust
87/// use gnss_time::{Duration, Glonass, Gps, Time};
88///
89/// let t: Time<Gps> = Time::from_nanos(0); // GPS epoch
90/// let later = t + Duration::from_seconds(3600);
91///
92/// assert_eq!((later - t).as_seconds(), 3600);
93///
94/// // Compile-time error — different time scales are incompatible:
95/// // let glo: Time<Glonass> = Time::from_nanos(0);
96/// // let _ = later - glo; // ← ERROR
97/// ```
98#[derive(Copy, Clone, Eq, PartialEq, Hash)]
99#[must_use = "Time<S> is a value type; ignoring it has no effect"]
100pub struct Time<S: TimeScale> {
101    nanos: u64,
102    _scale: PhantomData<S>,
103}
104
105/// Split seconds into whole seconds and nanoseconds.
106///
107/// This type is used for GNSS week/day constructors so that the core API
108/// stays fully deterministic and `no_std`-friendly.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
110pub struct DurationParts {
111    /// Whole seconds part (non-negative).
112    pub seconds: u64,
113
114    /// Nanosecond part, must be in `[0, 999_999_999]`.
115    pub nanos: u32,
116}
117
118#[cfg(feature = "serde")]
119#[expect(dead_code)]
120struct TimeVisitor<S>(PhantomData<S>);
121
122impl<S: TimeScale> Time<S> {
123    /// The scale's epoch — 0 nanoseconds.
124    ///
125    /// Corresponds to the calendar date defined by [`TimeScale::EPOCH_CIVIL`]
126    /// (e.g. `1980-01-06` for GPS, `1996-01-01` for GLONASS).
127    pub const EPOCH: Self = Time {
128        nanos: 0,
129        _scale: PhantomData,
130    };
131
132    /// Minimum representable value (synonym for `EPOCH`).
133    pub const MIN: Self = Self::EPOCH;
134
135    /// Maximum representable instant (`u64::MAX` nanoseconds ≈ 584.5 years).
136    pub const MAX: Self = Time {
137        nanos: u64::MAX,
138        _scale: PhantomData,
139    };
140
141    /// Nanoseconds per non-leap year (365 days).
142    ///
143    /// Useful for sanity-checking that a value is within a reasonable range:
144    /// ```rust
145    /// use gnss_time::{scale::Gps, Time};
146    ///
147    /// // 50 years from GPS epoch
148    /// let fifty_years = Time::<Gps>::from_nanos(50 * Time::<Gps>::NANOS_PER_YEAR);
149    /// assert!(fifty_years.as_nanos() > 0);
150    /// ```
151    pub const NANOS_PER_YEAR: u64 = 365 * 24 * 3_600 * 1_000_000_000;
152
153    /// Construct from raw nanoseconds since this scale's epoch.
154    #[inline]
155    pub const fn from_nanos(nanos: u64) -> Self {
156        Time {
157            nanos,
158            _scale: PhantomData,
159        }
160    }
161
162    /// Construct from whole seconds since this scale's epoch.
163    ///
164    /// # Panics
165    ///
166    /// Panics if `secs * 1_000_000_000` does not fit into `u64`.
167    #[inline]
168    pub const fn from_seconds(secs: u64) -> Self {
169        match secs.checked_mul(1_000_000_000) {
170            Some(n) => Time::from_nanos(n),
171            None => panic!("Time::from_seconds overflow"),
172        }
173    }
174
175    /// Construct from whole seconds, returning `None` on overflow.
176    #[inline]
177    #[must_use = "returns None on overflow; check the result"]
178    pub const fn checked_from_seconds(secs: u64) -> Option<Self> {
179        match secs.checked_mul(1_000_000_000) {
180            Some(n) => Some(Time::from_nanos(n)),
181            None => None,
182        }
183    }
184
185    /// Raw nanoseconds since this scale's epoch.
186    #[inline]
187    #[must_use]
188    pub const fn as_nanos(self) -> u64 {
189        self.nanos
190    }
191
192    /// Whole seconds since this scale's epoch (truncated).
193    #[inline]
194    #[must_use]
195    pub const fn as_seconds(self) -> u64 {
196        self.nanos / 1_000_000_000
197    }
198
199    /// Seconds as `f64`. For large timestamps (> ~2^53 ns), precision loss
200    /// affects even milliseconds
201    #[inline]
202    #[must_use]
203    pub const fn as_parts(self) -> (u64, u32) {
204        (
205            self.nanos / 1_000_000_000,
206            (self.nanos % 1_000_000_000) as u32,
207        )
208    }
209
210    /// Convert to TAI using the scale's fixed offset.
211    ///
212    /// # Errors
213    ///
214    /// Returns:
215    ///
216    /// - [`GnssTimeError::LeapSecondsRequired`] if the time scale is contextual
217    ///   (e.g. UTC or GLONASS) and does not have a fixed TAI offset.
218    /// - [`GnssTimeError::Overflow`] if applying the offset causes the
219    ///   resulting timestamp to fall outside the valid `u64` nanosecond range.
220    pub fn to_tai(self) -> Result<Time<Tai>, GnssTimeError> {
221        match S::OFFSET_TO_TAI {
222            OffsetToTai::Fixed(offset) => {
223                let nanos = i128::from(self.nanos) + i128::from(offset);
224
225                if nanos < 0 || nanos > i128::from(u64::MAX) {
226                    return Err(GnssTimeError::Overflow);
227                }
228
229                let nanos = u64::try_from(nanos).map_err(|_| GnssTimeError::Overflow)?;
230
231                Ok(Time::from_nanos(nanos))
232            }
233            OffsetToTai::Contextual => Err(GnssTimeError::LeapSecondsRequired),
234        }
235    }
236
237    /// Construct `Time<S>` from a TAI timestamp using the scale's fixed offset.
238    ///
239    /// # Errors
240    ///
241    /// Returns:
242    ///
243    /// - [`GnssTimeError::LeapSecondsRequired`] if the target time scale is
244    ///   contextual and requires leap-second handling (e.g. UTC, GLONASS).
245    /// - [`GnssTimeError::Overflow`] if applying the offset causes the result
246    ///   to underflow below 0 or overflow past `u64::MAX`.
247    pub fn from_tai(tai: Time<Tai>) -> Result<Self, GnssTimeError> {
248        match S::OFFSET_TO_TAI {
249            OffsetToTai::Fixed(offset) => {
250                let nanos = i128::from(tai.as_nanos()) - i128::from(offset);
251
252                if nanos < 0 || nanos > i128::from(u64::MAX) {
253                    return Err(GnssTimeError::Overflow);
254                }
255
256                let nanos = u64::try_from(nanos).map_err(|_| GnssTimeError::Overflow)?;
257
258                Ok(Time::from_nanos(nanos))
259            }
260            OffsetToTai::Contextual => Err(GnssTimeError::LeapSecondsRequired),
261        }
262    }
263
264    /// Convert directly between two fixed-offset scales via TAI.
265    ///
266    /// Fails if either source or target scale requires leap seconds.
267    ///
268    /// # Errors
269    ///
270    /// Returns [`GnssTimeError::Overflow`] if the intermediate TAI conversion
271    /// would exceed the representable `u64` range.
272    ///
273    /// Returns [`GnssTimeError::LeapSecondsRequired`] if either the source or
274    /// the target scale is contextual and needs leap seconds.
275    pub fn try_convert<T: TimeScale>(self) -> Result<Time<T>, GnssTimeError> {
276        let tai = self.to_tai()?;
277
278        Time::<T>::from_tai(tai)
279    }
280
281    /// Add a `Duration`, returning `None` on overflow or underflow.
282    #[inline]
283    #[must_use = "returns None on overflow; check the result"]
284    pub fn checked_add(
285        self,
286        d: Duration,
287    ) -> Option<Self> {
288        let result = i128::from(self.nanos) + i128::from(d.as_nanos());
289        let nanos = u64::try_from(result).ok()?;
290
291        Some(Time::from_nanos(nanos))
292    }
293
294    /// Subtract a `Duration`, returning `None` on overflow or underflow.
295    #[inline]
296    #[must_use = "returns None on underflow; check the result"]
297    pub fn checked_sub_duration(
298        self,
299        d: Duration,
300    ) -> Option<Self> {
301        let result = i128::from(self.nanos) - i128::from(d.as_nanos());
302        let nanos = u64::try_from(result).ok()?;
303
304        Some(Time::from_nanos(nanos))
305    }
306
307    /// Add, saturating at `EPOCH` (below) and `MAX` (above).
308    #[inline]
309    #[must_use = "saturating_add returns a new Time<S>; the original is unchanged"]
310    pub fn saturating_add(
311        self,
312        d: Duration,
313    ) -> Self {
314        self.checked_add(d).unwrap_or(if d.is_negative() {
315            Time::EPOCH
316        } else {
317            Time::MAX
318        })
319    }
320
321    /// Subtract duration, saturating at bounds.
322    #[inline]
323    #[must_use = "saturating_sub_duration returns a new Time<S>; the original is unchanged"]
324    pub fn saturating_sub_duration(
325        self,
326        d: Duration,
327    ) -> Self {
328        self.checked_sub_duration(d).unwrap_or(if d.is_negative() {
329            Time::MAX
330        } else {
331            Time::EPOCH
332        })
333    }
334
335    /// Fallible add — [`GnssTimeError::Overflow`] on failure.
336    ///
337    /// # Errors
338    ///
339    /// Returns [`GnssTimeError::Overflow`] if the resulting timestamp would
340    /// exceed the representable range of `u64` nanoseconds since the scale’s
341    /// epoch.
342    ///
343    /// Returns [`GnssTimeError::InvalidInput`] if the input duration is not
344    /// valid for this operation (if applicable).
345    #[inline]
346    pub fn try_add(
347        self,
348        d: Duration,
349    ) -> Result<Self, GnssTimeError> {
350        self.checked_add(d).ok_or(GnssTimeError::Overflow)
351    }
352
353    /// Fallible subtract — [`GnssTimeError::Overflow`] on failure.
354    ///
355    /// # Errors
356    ///
357    /// Returns [`GnssTimeError::Overflow`] if the resulting timestamp would
358    /// be less than zero or exceed the representable range of `u64` nanoseconds
359    /// since the scale’s epoch.
360    ///
361    /// This can happen when subtracting a duration larger than the current
362    /// timestamp.
363    #[inline]
364    pub fn try_sub_duration(
365        self,
366        d: Duration,
367    ) -> Result<Self, GnssTimeError> {
368        self.checked_sub_duration(d).ok_or(GnssTimeError::Overflow)
369    }
370
371    /// Signed interval `self − earlier`. Returns `None` if it overflows `i64`.
372    #[inline]
373    #[must_use = "returns None on overflow; check the result"]
374    pub fn checked_elapsed(
375        self,
376        earlier: Time<S>,
377    ) -> Option<Duration> {
378        let diff = i128::from(self.nanos) - i128::from(earlier.nanos);
379
380        let diff = i64::try_from(diff).ok()?;
381        Some(Duration::from_nanos(diff))
382    }
383}
384
385impl<S: TimeScale> Add<Duration> for Time<S> {
386    type Output = Time<S>;
387
388    #[inline]
389    fn add(
390        self,
391        rhs: Duration,
392    ) -> Time<S> {
393        self.checked_add(rhs)
394            .expect("Time<S> + Duration overflowed")
395    }
396}
397
398impl<S: TimeScale> AddAssign<Duration> for Time<S> {
399    #[inline]
400    fn add_assign(
401        &mut self,
402        rhs: Duration,
403    ) {
404        *self = *self + rhs;
405    }
406}
407
408impl<S: TimeScale> Sub<Duration> for Time<S> {
409    type Output = Time<S>;
410
411    #[inline]
412    fn sub(
413        self,
414        rhs: Duration,
415    ) -> Self::Output {
416        self.checked_sub_duration(rhs)
417            .expect("Time<S> - Duration underflowed")
418    }
419}
420
421impl<S: TimeScale> SubAssign<Duration> for Time<S> {
422    #[inline]
423    fn sub_assign(
424        &mut self,
425        rhs: Duration,
426    ) {
427        *self = *self - rhs;
428    }
429}
430
431impl<S: TimeScale> Sub<Time<S>> for Time<S> {
432    type Output = Duration;
433
434    #[inline]
435    fn sub(
436        self,
437        rhs: Time<S>,
438    ) -> Self::Output {
439        self.checked_elapsed(rhs)
440            .expect("Time<S> - Time<S> overflowed i64")
441    }
442}
443
444impl DurationParts {
445    /// Number of nanoseconds in one second.
446    pub const NANOS_PER_SECOND: u32 = 1_000_000_000;
447
448    /// Creates a new `DurationParts` from whole seconds and nanoseconds.
449    ///
450    /// # Parameters
451    /// - `seconds` – whole seconds (non‑negative)
452    /// - `nanos` – additional nanoseconds, **must be less than**
453    ///   `1_000_000_000`
454    ///
455    /// # Errors
456    /// Returns [`GnssTimeError::InvalidInput`] if `nanos >= 1_000_000_000`.
457    ///
458    /// # Example
459    /// ```
460    /// use gnss_time::DurationParts;
461    ///
462    /// let parts = DurationParts::new(5, 500_000_000).unwrap();
463    ///
464    /// assert_eq!(parts.as_nanos(), 5_500_000_000);
465    /// ```
466    #[inline]
467    pub const fn new(
468        seconds: u64,
469        nanos: u32,
470    ) -> Result<Self, GnssTimeError> {
471        if nanos >= Self::NANOS_PER_SECOND {
472            return Err(GnssTimeError::InvalidInput(
473                "nanos must be in [0, 1_000_000_000)",
474            ));
475        }
476
477        Ok(Self { seconds, nanos })
478    }
479
480    /// Converts the `DurationParts` into a total number of nanoseconds as
481    /// `u128`.
482    ///
483    /// # Example
484    /// ```
485    /// use gnss_time::DurationParts;
486    ///
487    /// let parts = DurationParts {
488    ///     seconds: 2,
489    ///     nanos: 123_456_789,
490    /// };
491    ///
492    /// assert_eq!(parts.as_nanos(), 2_123_456_789);
493    /// ```
494    #[inline]
495    #[must_use]
496    pub const fn as_nanos(self) -> u128 {
497        (self.seconds as u128) * Self::NANOS_PER_SECOND as u128 + self.nanos as u128
498    }
499}
500
501impl Time<Glonass> {
502    /// Construct from GLONASS day number and time-of-day.
503    ///
504    /// `tod.seconds` must be in `[0, 86_400)`.
505    /// `tod.nanos` must be in `[0, 1_000_000_000)`.
506    ///
507    /// # Errors
508    ///
509    /// [`GnssTimeError::InvalidInput`] if `tod_s ∉ [0, 86 400)`.
510    pub fn from_day_tod(
511        day: u32,
512        tod: DurationParts,
513    ) -> Result<Self, GnssTimeError> {
514        if tod.seconds >= 86_400 {
515            return Err(GnssTimeError::InvalidInput(
516                "tod.seconds must be in [0, 86_400)",
517            ));
518        }
519        if tod.nanos >= DurationParts::NANOS_PER_SECOND {
520            return Err(GnssTimeError::InvalidInput(
521                "tod.nanos must be in [0, 1_000_000_000)",
522            ));
523        }
524
525        let day_ns = u64::from(day)
526            .checked_mul(86_400_000_000_000)
527            .ok_or(GnssTimeError::Overflow)?;
528        let tod_ns = tod
529            .seconds
530            .checked_mul(1_000_000_000)
531            .ok_or(GnssTimeError::Overflow)?
532            .checked_add(u64::from(tod.nanos))
533            .ok_or(GnssTimeError::Overflow)?;
534        let total = day_ns.checked_add(tod_ns).ok_or(GnssTimeError::Overflow)?;
535
536        Ok(Time::from_nanos(total))
537    }
538
539    /// Day number since GLONASS epoch.
540    #[inline]
541    #[must_use]
542    pub const fn day(self) -> u32 {
543        (self.nanos / 86_400_000_000_000u64) as u32
544    }
545
546    /// Time of day in whole seconds.
547    #[inline]
548    #[must_use]
549    pub const fn tod_seconds(self) -> u32 {
550        ((self.nanos % 86_400_000_000_000u64) / 1_000_000_000u64) as u32
551    }
552
553    /// Sub-second nanosecond remainder within the current second.
554    #[inline]
555    #[must_use]
556    pub const fn sub_second_nanos(self) -> u32 {
557        (self.nanos % 1_000_000_000u64) as u32
558    }
559
560    /// Day of week: **1 = Monday … 7 = Sunday** (`NavIC` / ISO 8601
561    /// convention).
562    ///
563    /// GLONASS epoch (1996-01-01) was a **Monday**, so day 0 → 1 (Monday).
564    ///
565    /// The formula is simply `(day % 7) + 1`.
566    ///
567    /// # GLONASS ICD note
568    ///
569    /// The GLONASS Interface Control Document defines the "day number within
570    /// the four-year interval" (`N_T`) starting from 1, but for simplicity
571    /// this crate uses 0-based day counts from the epoch and exposes the
572    /// ISO / `NavIC` weekday (1=Mon … 7=Sun) through this method.
573    ///
574    /// # Examples
575    ///
576    /// ```rust
577    /// use gnss_time::{scale::Glonass, DurationParts, Time};
578    ///
579    /// // Day 0 = 1996-01-01 = Monday
580    /// let t = Time::<Glonass>::from_day_tod(
581    ///     0,
582    ///     DurationParts {
583    ///         seconds: 0,
584    ///         nanos: 0,
585    ///     },
586    /// )
587    /// .unwrap();
588    ///
589    /// assert_eq!(t.day_of_week(), 1); // Monday
590    ///
591    /// // Day 6 = 1996-01-07 = Sunday
592    /// let t2 = Time::<Glonass>::from_day_tod(
593    ///     6,
594    ///     DurationParts {
595    ///         seconds: 0,
596    ///         nanos: 0,
597    ///     },
598    /// )
599    /// .unwrap();
600    ///
601    /// assert_eq!(t2.day_of_week(), 7); // Sunday
602    ///
603    /// // Day 7 = 1996-01-08 = Monday again
604    /// let t3 = Time::<Glonass>::from_day_tod(
605    ///     7,
606    ///     DurationParts {
607    ///         seconds: 0,
608    ///         nanos: 0,
609    ///     },
610    /// )
611    /// .unwrap();
612    ///
613    /// assert_eq!(t3.day_of_week(), 1);
614    /// ```
615    #[inline]
616    #[must_use]
617    pub const fn day_of_week(self) -> u8 {
618        // GLONASS epoch starts on Monday → day 0 corresponds to 1
619        (self.day() % 7) as u8 + 1
620    }
621
622    /// Returns `true` if the current day-of-week is Saturday (6) or Sunday (7).
623    #[inline]
624    #[must_use]
625    pub const fn is_weekend(self) -> bool {
626        let d = self.day_of_week();
627
628        d == 6 || d == 7
629    }
630}
631
632impl Time<Gps> {
633    /// Constructs a GPS time from GPS week number and time-of-week.
634    ///
635    /// `tow.seconds` must be in `[0, 604_800)`.
636    /// `tow.nanos` must be in `[0, 1_000_000_000)`.
637    ///
638    /// # Errors
639    ///
640    /// Returns [`GnssTimeError::InvalidInput`] if:
641    /// - `tow.seconds >= 604_800`
642    /// - `tow.nanos >= 1_000_000_000`
643    ///
644    /// Returns [`GnssTimeError::Overflow`] if the resulting nanosecond
645    /// calculation exceeds `u64::MAX`.
646    pub fn from_week_tow(
647        week: u16,
648        tow: DurationParts,
649    ) -> Result<Self, GnssTimeError> {
650        if tow.seconds >= 604_800 {
651            return Err(GnssTimeError::InvalidInput(
652                "tow.seconds must be in [0, 604_800)",
653            ));
654        }
655
656        if tow.nanos >= DurationParts::NANOS_PER_SECOND {
657            return Err(GnssTimeError::InvalidInput(
658                "tow.nanos must be in [0, 1_000_000_000)",
659            ));
660        }
661
662        let week_ns = u64::from(week)
663            .checked_mul(604_800_000_000_000)
664            .ok_or(GnssTimeError::Overflow)?;
665
666        let tow_ns = tow
667            .seconds
668            .checked_mul(1_000_000_000)
669            .ok_or(GnssTimeError::Overflow)?
670            .checked_add(u64::from(tow.nanos))
671            .ok_or(GnssTimeError::Overflow)?;
672
673        let total = week_ns.checked_add(tow_ns).ok_or(GnssTimeError::Overflow)?;
674
675        Ok(Time::from_nanos(total))
676    }
677
678    /// Создаёт GPS время из Unix timestamp (секунды с 1970-01-01 UTC).
679    ///
680    /// # Errors
681    ///
682    /// Returns [`GnssTimeError::Overflow`] if:
683    /// - the intermediate UTC conversion overflows internal `u64` nanoseconds
684    /// - the resulting GPS time cannot be represented in the internal range
685    ///
686    /// Returns any error propagated from UTC conversion, such as:
687    /// - [`GnssTimeError::Overflow`] when the Unix timestamp is before the UTC
688    ///   epoch
689    pub fn from_unix_seconds<P: LeapSecondsProvider>(
690        unix_seconds: i64,
691        ls: P,
692    ) -> Result<Self, GnssTimeError> {
693        let utc = Time::<Utc>::from_unix_seconds(unix_seconds)?;
694
695        utc_to_gps(utc, &ls)
696    }
697
698    /// Returns this GPS timestamp as a Unix timestamp (whole seconds since
699    /// 1970-01-01 UTC).
700    ///
701    /// The conversion is `GPS -> UTC -> Unix` and therefore requires an
702    /// explicit leap-second provider.
703    ///
704    /// # Errors
705    ///
706    /// [`GnssTimeError::Overflow`] if the UTC conversion fails.
707    ///
708    /// ```rust
709    /// use gnss_time::{Gps, LeapSeconds, Time};
710    ///
711    /// let ls = LeapSeconds::builtin();
712    /// // GPS epoch = 1980-01-06 → Unix 315_964_800
713    /// assert_eq!(Time::<Gps>::EPOCH.as_unix_seconds(ls).unwrap(), 315_964_800);
714    /// ```
715    pub fn as_unix_seconds<P: LeapSecondsProvider>(
716        self,
717        ls: P,
718    ) -> Result<i64, GnssTimeError> {
719        let utc = gps_to_utc(self, &ls)?;
720
721        Ok(utc.as_unix_seconds())
722    }
723
724    /// Conversion of GPS time to UTC using the built-in leap seconds table.
725    ///
726    /// # Accuracy
727    ///
728    /// For most timestamps, the conversion is accurate to the nanosecond.
729    /// During a leap second insertion window (e.g. 2016-12-31 23:59:60 UTC),
730    /// the result may differ by up to 1 second.
731    ///
732    /// # Errors
733    ///
734    /// Returns [`GnssTimeError::Overflow`] if arithmetic overflow occurs during
735    /// conversion.
736    ///
737    /// Returns [`GnssTimeError::LeapSecondsRequired`] if leap second data is
738    /// insufficient or conversion cannot be resolved.
739    pub fn to_utc(self) -> Result<Time<Utc>, GnssTimeError> {
740        gps_to_utc(self, LeapSeconds::builtin())
741    }
742
743    /// Conversion of GPS time to UTC using a custom leap seconds provider.
744    ///
745    /// The same accuracy notes apply as for [`to_utc`](Self::to_utc):
746    /// the conversion is precise for most timestamps, but during a leap second
747    /// insertion window it may differ by up to 1 second.
748    ///
749    /// # Errors
750    ///
751    /// Returns [`GnssTimeError::Overflow`] if the conversion fails due to
752    /// arithmetic overflow during GPS → UTC transformation.
753    ///
754    /// Returns any error propagated from the underlying conversion logic
755    /// (see [`gps_to_utc`]), such as invalid time ranges.
756    pub fn to_utc_with<P: LeapSecondsProvider>(
757        self,
758        ls: &P,
759    ) -> Result<Time<Utc>, GnssTimeError> {
760        gps_to_utc(self, ls)
761    }
762
763    /// GPS week number.
764    #[inline]
765    #[must_use]
766    pub const fn week(self) -> u32 {
767        (self.nanos / 604_800_000_000_000u64) as u32
768    }
769
770    /// Time of week in whole seconds.
771    #[inline]
772    #[must_use]
773    pub const fn tow_seconds(self) -> u32 {
774        ((self.nanos % 604_800_000_000_000u64) / 1_000_000_000u64) as u32
775    }
776
777    /// Sub-second nanosecond remainder within the current second.
778    #[inline]
779    #[must_use]
780    pub const fn sub_second_nanos(self) -> u32 {
781        (self.nanos % 1_000_000_000u64) as u32
782    }
783}
784
785impl Time<Utc> {
786    /// Construct from a Unix timestamp (whole seconds since 1970-01-01 UTC).
787    ///
788    /// `Time<Utc>` counts nanoseconds from **1972-01-01** (UTC epoch), while
789    /// Unix time starts from **1970-01-01**. The gap is
790    /// [`UTC_EPOCH_UNIX_OFFSET_S`] = 63 072 000 s (730 days).
791    ///
792    /// # Errors
793    ///
794    /// Returns [`GnssTimeError::Overflow`] if `unix_seconds < 63_072_000`
795    /// (i.e. the date is before 1972-01-01 00:00:00 UTC, the UTC epoch).
796    ///
797    /// # Example
798    ///
799    /// ```rust
800    /// use gnss_time::{Time, Utc, UTC_EPOCH_UNIX_OFFSET_S};
801    ///
802    /// // Unix epoch (1970-01-01) is before the UTC epoch → error
803    /// assert!(Time::<Utc>::from_unix_seconds(0).is_err());
804    ///
805    /// // 63_072_000 s from Unix epoch = 1972-01-01 = UTC epoch
806    /// let utc = Time::<Utc>::from_unix_seconds(UTC_EPOCH_UNIX_OFFSET_S).unwrap();
807    /// assert_eq!(utc, Time::<Utc>::EPOCH);
808    ///
809    /// // Round-trip
810    /// let unix_s: i64 = 1_700_000_000;
811    /// let utc2 = Time::<Utc>::from_unix_seconds(unix_s).unwrap();
812    /// assert_eq!(utc2.as_unix_seconds(), unix_s);
813    /// ```
814    pub fn from_unix_seconds(unix_seconds: i64) -> Result<Self, GnssTimeError> {
815        // utc_seconds_from_1972 = unix_seconds − UTC_EPOCH_UNIX_OFFSET_S
816        let utc_s = unix_seconds
817            .checked_sub(UTC_EPOCH_UNIX_OFFSET_S)
818            .ok_or(GnssTimeError::Overflow)?;
819
820        if utc_s < 0 {
821            return Err(GnssTimeError::Overflow);
822        }
823
824        let nanos = u64::try_from(utc_s)
825            .map_err(|_| GnssTimeError::Overflow)?
826            .checked_mul(1_000_000_000)
827            .ok_or(GnssTimeError::Overflow)?;
828
829        Ok(Time::from_nanos(nanos))
830    }
831
832    /// Construct from a Unix timestamp with nanosecond precision.
833    ///
834    /// `unix_nanos` is the number of nanoseconds since 1970-01-01 00:00:00 UTC.
835    ///
836    /// # Errors
837    ///
838    /// Returns [`GnssTimeError::Overflow`] if the result would be before the
839    /// UTC epoch (1972-01-01), i.e. `unix_nanos < UTC_EPOCH_UNIX_OFFSET_NS`.
840    ///
841    /// # Example
842    ///
843    /// ```rust
844    /// use gnss_time::{Time, Utc, UTC_EPOCH_UNIX_OFFSET_NS};
845    ///
846    /// // UTC epoch in Unix nanoseconds
847    /// let utc = Time::<Utc>::from_unix_nanos(UTC_EPOCH_UNIX_OFFSET_NS).unwrap();
848    /// assert_eq!(utc, Time::<Utc>::EPOCH);
849    ///
850    /// // Round-trip
851    /// let nanos: i64 = 1_700_000_000_123_456_789;
852    /// let utc2 = Time::<Utc>::from_unix_nanos(nanos).unwrap();
853    /// assert_eq!(utc2.as_unix_nanos(), nanos);
854    /// ```
855    pub fn from_unix_nanos(unix_nanos: i64) -> Result<Self, GnssTimeError> {
856        // utc_nanos_from_1972 = unix_nanos − UTC_EPOCH_UNIX_OFFSET_NS
857        let utc_ns = unix_nanos
858            .checked_sub(UTC_EPOCH_UNIX_OFFSET_NS)
859            .ok_or(GnssTimeError::Overflow)?;
860
861        if utc_ns < 0 {
862            return Err(GnssTimeError::Overflow);
863        }
864
865        let nanos = u64::try_from(utc_ns).map_err(|_| GnssTimeError::Overflow)?;
866
867        Ok(Time::from_nanos(nanos))
868    }
869
870    /// Returns this UTC timestamp as a Unix timestamp (whole seconds since
871    /// 1970-01-01 UTC).
872    ///
873    /// The result is always ≥ [`UTC_EPOCH_UNIX_OFFSET_S`] because `Time<Utc>`
874    /// cannot represent dates before 1972-01-01.
875    ///
876    /// # Overflow behavior
877    ///
878    /// If the result exceeds `i64::MAX`, it saturates at `i64::MAX`.
879    ///
880    /// # Example
881    ///
882    /// ```rust
883    /// use gnss_time::{Time, Utc, UTC_EPOCH_UNIX_OFFSET_S};
884    ///
885    /// // UTC epoch = 1972-01-01 = Unix 63_072_000
886    /// assert_eq!(
887    ///     Time::<Utc>::EPOCH.as_unix_seconds(),
888    ///     UTC_EPOCH_UNIX_OFFSET_S
889    /// );
890    ///
891    /// // Round-trip
892    /// let unix_s: i64 = 1_700_000_000;
893    /// let utc = Time::<Utc>::from_unix_seconds(unix_s).unwrap();
894    /// assert_eq!(utc.as_unix_seconds(), unix_s);
895    /// ```
896    #[inline]
897    #[must_use]
898    pub fn as_unix_seconds(self) -> i64 {
899        let secs = i128::from(self.nanos / 1_000_000_000) + i128::from(UTC_EPOCH_UNIX_OFFSET_S);
900
901        i64::try_from(secs).unwrap_or(i64::MAX)
902    }
903
904    /// Returns this UTC timestamp as a Unix timestamp with nanosecond
905    /// precision (nanoseconds since 1970-01-01 UTC).
906    ///
907    /// # Overflow note
908    ///
909    /// `i64` can represent nanoseconds up to ~year 2262 from the Unix epoch.
910    /// For timestamps beyond that, this method saturates at `i64::MAX`.
911    /// In practice, `Time<Utc>::MAX` corresponds to ~year 2556, which is
912    /// beyond `i64` range — plan accordingly.
913    ///
914    /// # Example
915    ///
916    /// ```rust
917    /// use gnss_time::{Time, Utc, UTC_EPOCH_UNIX_OFFSET_NS};
918    ///
919    /// assert_eq!(Time::<Utc>::EPOCH.as_unix_nanos(), UTC_EPOCH_UNIX_OFFSET_NS);
920    ///
921    /// // Round-trip (within i64 range)
922    /// let nanos: i64 = 1_700_000_000_123_456_789;
923    /// let utc = Time::<Utc>::from_unix_nanos(nanos).unwrap();
924    /// assert_eq!(utc.as_unix_nanos(), nanos);
925    /// ```
926    #[inline]
927    #[must_use]
928    pub fn as_unix_nanos(self) -> i64 {
929        // self.nanos is u64; cast to i64 wraps above i64::MAX (~year 2262).
930        // We use saturating_add to avoid UB and stay predictable.
931        i64::try_from(self.nanos).map_or(i64::MAX, |ns| ns.saturating_add(UTC_EPOCH_UNIX_OFFSET_NS))
932    }
933
934    /// Conversion of UTC time to GPS using the built-in leap seconds table.
935    ///
936    /// # Errors
937    ///
938    /// Returns [`GnssTimeError::Overflow`] if the conversion fails due to
939    /// arithmetic overflow during UTC → GPS transformation.
940    ///
941    /// Returns [`GnssTimeError::LeapSecondsRequired`] if the conversion cannot
942    /// be resolved due to missing or inconsistent leap second data.
943    pub fn to_gps(self) -> Result<Time<Gps>, GnssTimeError> {
944        utc_to_gps(self, LeapSeconds::builtin())
945    }
946
947    /// Converts UTC time to GPS time using a custom leap seconds provider.
948    ///
949    /// # Errors
950    ///
951    /// Returns:
952    ///
953    /// - [`GnssTimeError::Overflow`] if the resulting GPS timestamp cannot be
954    ///   represented in `u64` nanoseconds
955    /// - [`GnssTimeError::LeapSecondsRequired`] if the conversion cannot be
956    ///   resolved due to missing or invalid leap second data in `ls`
957    pub fn to_gps_with<P: LeapSecondsProvider>(
958        self,
959        ls: &P,
960    ) -> Result<Time<Gps>, GnssTimeError> {
961        utc_to_gps(self, ls)
962    }
963
964    /// Converts this UTC timestamp to a [`CivilDateTime`].
965    ///
966    /// The result expresses the instant as a human-readable date and
967    /// time-of-day in the proleptic Gregorian calendar.
968    ///
969    /// # Panics
970    ///
971    /// Panics if converting the internal UTC nanosecond count to
972    /// [`CivilDateTime`] fails. In normal use this should not happen because
973    /// `Time<Utc>` is already constrained to the UTC epoch range.
974    ///
975    /// # Examples
976    ///
977    /// ```rust
978    /// use gnss_time::{Time, Utc};
979    ///
980    /// // UTC epoch = 1972-01-01T00:00:00.000000000Z
981    /// let dt = Time::<Utc>::EPOCH.to_civil();
982    /// assert_eq!(dt.to_string(), "1972-01-01T00:00:00.000000000Z");
983    ///
984    /// // GPS epoch (1980-01-06) as UTC
985    /// let utc = Time::<Utc>::from_nanos(252_892_800_000_000_000);
986    /// let dt = utc.to_civil();
987    /// assert_eq!(dt.year, 1980);
988    /// assert_eq!(dt.month, 1);
989    /// assert_eq!(dt.day, 6);
990    /// assert_eq!(dt.hour, 0);
991    /// ```
992    #[must_use]
993    pub fn to_civil(self) -> CivilDateTime {
994        CivilDateTime::from_utc_nanos(self.as_nanos()).unwrap()
995    }
996}
997
998impl<S: TimeScale> PartialOrd for Time<S> {
999    #[inline]
1000    fn partial_cmp(
1001        &self,
1002        other: &Self,
1003    ) -> Option<core::cmp::Ordering> {
1004        Some(self.cmp(other))
1005    }
1006}
1007
1008impl<S: TimeScale> Ord for Time<S> {
1009    #[inline]
1010    fn cmp(
1011        &self,
1012        other: &Self,
1013    ) -> core::cmp::Ordering {
1014        self.nanos.cmp(&other.nanos)
1015    }
1016}
1017
1018impl<S: TimeScale> fmt::Debug for Time<S> {
1019    fn fmt(
1020        &self,
1021        f: &mut fmt::Formatter<'_>,
1022    ) -> fmt::Result {
1023        write!(f, "Time<{}>({}ns)", S::NAME, self.nanos)
1024    }
1025}
1026
1027impl<S: TimeScale> fmt::Display for Time<S> {
1028    /// Formatting depends on the [`DisplayStyle`] of the time scale:
1029    ///
1030    /// | Style      | Example                    |
1031    /// |------------|---------------------------|
1032    /// | `WeekTow`  | `"GPS 2345:432000.000"`   |
1033    /// | `DayTod`   | `"GLO 10512:43200.000"`   |
1034    /// | `Simple`   | `"TAI +1000000000s 0ns"`  |
1035    fn fmt(
1036        &self,
1037        f: &mut fmt::Formatter<'_>,
1038    ) -> fmt::Result {
1039        match S::DISPLAY_STYLE {
1040            DisplayStyle::WeekTow => {
1041                const WEEK_NS: u64 = 604_800_000_000_000;
1042                let week = self.nanos / WEEK_NS;
1043                let tow_ns = self.nanos % WEEK_NS;
1044                let tow_s = tow_ns / 1_000_000_000;
1045                let tow_ms = (tow_ns % 1_000_000_000) / 1_000_000;
1046
1047                write!(f, "{} {}:{:06}.{:03}", S::NAME, week, tow_s, tow_ms)
1048            }
1049            DisplayStyle::DayTod => {
1050                const DAY_NS: u64 = 86_400_000_000_000;
1051                let day = self.nanos / DAY_NS;
1052                let tod_ns = self.nanos % DAY_NS;
1053                let tod_s = tod_ns / 1_000_000_000;
1054                let tod_ms = (tod_ns % 1_000_000_000) / 1_000_000;
1055
1056                write!(f, "{} {}:{:05}.{:03}", S::NAME, day, tod_s, tod_ms)
1057            }
1058            DisplayStyle::Simple => {
1059                let secs = self.nanos / 1_000_000_000;
1060                let ns_rem = self.nanos % 1_000_000_000;
1061
1062                write!(f, "{} +{}s {}ns", S::NAME, secs, ns_rem)
1063            }
1064        }
1065    }
1066}
1067
1068// defmt support
1069
1070#[cfg(feature = "defmt")]
1071impl<S: TimeScale> defmt::Format for Time<S> {
1072    fn format(
1073        &self,
1074        f: defmt::Formatter,
1075    ) {
1076        match S::DISPLAY_STYLE {
1077            DisplayStyle::WeekTow => {
1078                const WEEK_NS: u64 = 604_800_000_000_000;
1079                let week = self.nanos / WEEK_NS;
1080                let tow_ns = self.nanos % WEEK_NS;
1081                let tow_s = tow_ns / 1_000_000_000;
1082                let tow_ms = (tow_ns % 1_000_000_000) / 1_000_000;
1083
1084                defmt::write!(f, "{} {}:{:06}.{:03}", S::NAME, week, tow_s, tow_ms);
1085            }
1086            DisplayStyle::DayTod => {
1087                const DAY_NS: u64 = 86_400_000_000_000;
1088                let day = self.nanos / DAY_NS;
1089                let tod_ns = self.nanos % DAY_NS;
1090                let tod_s = tod_ns / 1_000_000_000;
1091                let tod_ms = (tod_ns % 1_000_000_000) / 1_000_000;
1092
1093                defmt::write!(f, "{} {}:{:05}.{:03}", S::NAME, day, tod_s, tod_ms);
1094            }
1095            DisplayStyle::Simple => {
1096                let secs = self.nanos / 1_000_000_000;
1097                let ns_rem = self.nanos % 1_000_000_000;
1098
1099                defmt::write!(f, "{} +{}s {}ns", S::NAME, secs, ns_rem);
1100            }
1101        }
1102    }
1103}
1104
1105////////////////////////////////////////////////////////////////////////////////
1106// Tests
1107////////////////////////////////////////////////////////////////////////////////
1108
1109#[cfg(test)]
1110mod tests {
1111    #[allow(unused_imports)]
1112    use std::format;
1113    #[allow(unused_imports)]
1114    use std::string::ToString;
1115    #[allow(unused_imports)]
1116    use std::vec;
1117
1118    use super::*;
1119    use crate::scale::{Beidou, Galileo, Glonass, Gps, Tai, Utc};
1120
1121    #[test]
1122    fn test_size_equals_u64() {
1123        assert_eq!(core::mem::size_of::<Time<Gps>>(), 8);
1124        assert_eq!(core::mem::size_of::<Time<Glonass>>(), 8);
1125        assert_eq!(core::mem::size_of::<Time<Galileo>>(), 8);
1126        assert_eq!(core::mem::size_of::<Time<Beidou>>(), 8);
1127        assert_eq!(core::mem::size_of::<Time<Utc>>(), 8);
1128        assert_eq!(core::mem::size_of::<Time<Tai>>(), 8);
1129    }
1130
1131    #[test]
1132    fn test_epoch_is_zero() {
1133        assert_eq!(Time::<Gps>::EPOCH.as_nanos(), 0);
1134        assert_eq!(Time::<Utc>::EPOCH.as_nanos(), 0);
1135    }
1136
1137    #[test]
1138    fn test_from_week_tow_zero() {
1139        let t = Time::<Gps>::from_week_tow(
1140            0,
1141            DurationParts {
1142                seconds: 0,
1143                nanos: 0,
1144            },
1145        )
1146        .unwrap();
1147
1148        assert_eq!(t, Time::<Gps>::EPOCH);
1149    }
1150
1151    #[test]
1152    fn test_from_week_tow_roundtrip() {
1153        let t = Time::<Gps>::from_week_tow(
1154            2345,
1155            DurationParts {
1156                seconds: 432_000,
1157                nanos: 0,
1158            },
1159        )
1160        .unwrap();
1161
1162        assert_eq!(t.week(), 2345);
1163        assert_eq!(t.tow_seconds(), 432_000);
1164        assert_eq!(t.sub_second_nanos(), 0);
1165    }
1166
1167    #[test]
1168    fn test_from_week_tow_with_fractional() {
1169        let t = Time::<Gps>::from_week_tow(
1170            2300,
1171            DurationParts {
1172                seconds: 3661,
1173                nanos: 500_000_000,
1174            },
1175        )
1176        .unwrap();
1177
1178        assert_eq!(t.week(), 2300);
1179        assert_eq!(t.tow_seconds(), 3661);
1180        assert_eq!(t.sub_second_nanos(), 500_000_000);
1181    }
1182
1183    #[test]
1184    fn test_from_week_tow_invalid() {
1185        assert!(matches!(
1186            Time::<Gps>::from_week_tow(
1187                0,
1188                DurationParts {
1189                    seconds: 604_800,
1190                    nanos: 0
1191                }
1192            ),
1193            Err(GnssTimeError::InvalidInput(_))
1194        ));
1195    }
1196
1197    #[test]
1198    fn test_from_day_tod_zero() {
1199        let t = Time::<Glonass>::from_day_tod(
1200            0,
1201            DurationParts {
1202                seconds: 0,
1203                nanos: 0,
1204            },
1205        )
1206        .unwrap();
1207
1208        assert_eq!(t, Time::<Glonass>::EPOCH);
1209    }
1210
1211    #[test]
1212    fn test_from_day_tod_roundtrip() {
1213        let t = Time::<Glonass>::from_day_tod(
1214            10_512,
1215            DurationParts {
1216                seconds: 43_200,
1217                nanos: 0,
1218            },
1219        )
1220        .unwrap();
1221
1222        assert_eq!(t.day(), 10_512);
1223        assert_eq!(t.tod_seconds(), 43_200);
1224    }
1225
1226    #[test]
1227    fn test_from_day_tod_invalid() {
1228        assert!(matches!(
1229            Time::<Glonass>::from_day_tod(
1230                0,
1231                DurationParts {
1232                    seconds: 86_400,
1233                    nanos: 0
1234                }
1235            ),
1236            Err(GnssTimeError::InvalidInput(_))
1237        ));
1238    }
1239
1240    #[test]
1241    fn test_add_positive_duration() {
1242        let t = Time::<Gps>::from_seconds(100);
1243
1244        assert_eq!((t + Duration::from_seconds(50)).as_seconds(), 150);
1245    }
1246
1247    #[test]
1248    fn test_add_negative_duration_moves_back() {
1249        let t = Time::<Gps>::from_seconds(100);
1250
1251        assert_eq!((t + Duration::from_nanos(-50_000_000_000)).as_seconds(), 50);
1252    }
1253
1254    #[test]
1255    fn test_roundtrip_add_sub() {
1256        let t = Time::<Galileo>::from_seconds(1_000_000);
1257        let d = Duration::from_seconds(12_345);
1258
1259        assert_eq!(t + d - d, t);
1260    }
1261
1262    #[test]
1263    fn test_sub_times_positive() {
1264        let a = Time::<Gps>::from_seconds(200);
1265        let b = Time::<Gps>::from_seconds(100);
1266
1267        assert_eq!((a - b).as_seconds(), 100);
1268    }
1269
1270    #[test]
1271    fn test_sub_times_negative() {
1272        let a = Time::<Gps>::from_seconds(100);
1273        let b = Time::<Gps>::from_seconds(200);
1274
1275        assert_eq!((a - b).as_seconds(), -100);
1276    }
1277
1278    #[test]
1279    fn test_sub_same_is_zero() {
1280        let t = Time::<Gps>::from_seconds(42);
1281
1282        assert!((t - t).is_zero());
1283    }
1284
1285    #[test]
1286    #[should_panic(expected = "Time<S> + Duration overflowed")]
1287    fn test_add_overflow_panics() {
1288        let _ = Time::<Gps>::MAX + Duration::ONE_NANOSECOND;
1289    }
1290
1291    #[test]
1292    fn test_checked_add_overflow() {
1293        assert!(Time::<Gps>::MAX
1294            .checked_add(Duration::ONE_NANOSECOND)
1295            .is_none());
1296    }
1297
1298    #[test]
1299    fn test_checked_sub_underflow() {
1300        assert!(Time::<Gps>::EPOCH
1301            .checked_sub_duration(Duration::ONE_NANOSECOND)
1302            .is_none());
1303    }
1304
1305    #[test]
1306    fn test_saturating_add_clamps() {
1307        assert_eq!(
1308            Time::<Gps>::MAX.saturating_add(Duration::from_seconds(1)),
1309            Time::<Gps>::MAX
1310        );
1311    }
1312
1313    #[test]
1314    fn test_gps_to_tai_adds_19s() {
1315        let gps = Time::<Gps>::from_seconds(100);
1316        let tai = gps.to_tai().unwrap();
1317
1318        assert_eq!(tai.as_seconds(), 119);
1319    }
1320
1321    #[test]
1322    fn test_tai_to_gps_subtracts_19s() {
1323        let tai = Time::<Tai>::from_seconds(119);
1324        let gps = Time::<Gps>::from_tai(tai).unwrap();
1325
1326        assert_eq!(gps.as_seconds(), 100);
1327    }
1328
1329    #[test]
1330    fn test_roundtrip_via_tai() {
1331        let original = Time::<Gps>::from_seconds(5_000_000);
1332        let back = Time::<Gps>::from_tai(original.to_tai().unwrap()).unwrap();
1333
1334        assert_eq!(original, back);
1335    }
1336
1337    #[test]
1338    fn test_gps_galileo_identity_via_tai() {
1339        // Same TAI offset → identical TAI instant → GPS→Galileo preserves nanoseconds
1340        let gps = Time::<Gps>::from_seconds(12_345);
1341        let gal = gps.try_convert::<Galileo>().unwrap();
1342
1343        assert_eq!(gps.as_nanos(), gal.as_nanos());
1344    }
1345
1346    #[test]
1347    fn test_gps_to_beidou_via_tai() {
1348        // GPS(100s) → TAI(119s) → BDT: 119 - 33 = 86s
1349        let gps = Time::<Gps>::from_seconds(100);
1350        let bdt = gps.try_convert::<Beidou>().unwrap();
1351
1352        assert_eq!(bdt.as_seconds(), 86);
1353    }
1354
1355    #[test]
1356    fn test_contextual_scale_to_tai_fails() {
1357        let glo = Time::<Glonass>::from_seconds(100);
1358
1359        assert!(matches!(
1360            glo.to_tai(),
1361            Err(GnssTimeError::LeapSecondsRequired)
1362        ));
1363    }
1364
1365    #[test]
1366    fn test_tai_to_contextual_fails() {
1367        let tai = Time::<Tai>::from_seconds(100);
1368
1369        assert!(matches!(
1370            Time::<Utc>::from_tai(tai),
1371            Err(GnssTimeError::LeapSecondsRequired)
1372        ));
1373    }
1374
1375    #[test]
1376    fn test_to_tai_overflow() {
1377        let t = Time::<Gps>::from_nanos(u64::MAX);
1378
1379        assert!(matches!(t.to_tai(), Err(GnssTimeError::Overflow)));
1380    }
1381
1382    #[test]
1383    fn test_from_tai_underflow() {
1384        // TAI(0) - 19s offset → negative GPS time → overflow
1385        let tai = Time::<Tai>::from_nanos(0);
1386
1387        assert!(matches!(
1388            Time::<Gps>::from_tai(tai),
1389            Err(GnssTimeError::Overflow)
1390        ));
1391    }
1392
1393    #[test]
1394    fn test_gps_display_week_tow_format() {
1395        let t = Time::<Gps>::from_week_tow(
1396            2345,
1397            DurationParts {
1398                seconds: 432_000,
1399                nanos: 0,
1400            },
1401        )
1402        .unwrap();
1403
1404        assert_eq!(t.to_string(), "GPS 2345:432000.000");
1405    }
1406
1407    #[test]
1408    fn test_gps_display_epoch_is_week_0() {
1409        let s = Time::<Gps>::EPOCH.to_string();
1410
1411        assert_eq!(s, "GPS 0:000000.000");
1412    }
1413
1414    #[test]
1415    fn test_gps_display_tow_zero_padded() {
1416        // TOW = 1 second → should be displayed as 000001
1417        let t = Time::<Gps>::from_week_tow(
1418            1,
1419            DurationParts {
1420                seconds: 1,
1421                nanos: 0,
1422            },
1423        )
1424        .unwrap();
1425
1426        assert_eq!(t.to_string(), "GPS 1:000001.000");
1427    }
1428
1429    #[test]
1430    fn test_gps_display_with_millis() {
1431        let t = Time::<Gps>::from_week_tow(
1432            100,
1433            DurationParts {
1434                seconds: 0,
1435                nanos: 500_000_000,
1436            },
1437        )
1438        .unwrap();
1439
1440        assert_eq!(t.to_string(), "GPS 100:000000.500");
1441    }
1442
1443    #[test]
1444    fn test_glonass_display_day_tod_format() {
1445        let t = Time::<Glonass>::from_day_tod(
1446            10_512,
1447            DurationParts {
1448                seconds: 43_200,
1449                nanos: 0,
1450            },
1451        )
1452        .unwrap();
1453
1454        assert_eq!(t.to_string(), "GLO 10512:43200.000");
1455    }
1456
1457    #[test]
1458    fn test_glonass_display_epoch() {
1459        let s = Time::<Glonass>::EPOCH.to_string();
1460
1461        assert_eq!(s, "GLO 0:00000.000");
1462    }
1463
1464    #[test]
1465    fn test_galileo_display_week_format() {
1466        let s = Time::<Galileo>::EPOCH.to_string();
1467
1468        assert!(s.starts_with("GAL "));
1469        assert!(s.contains(':'));
1470    }
1471
1472    #[test]
1473    fn test_tai_display_simple_format() {
1474        let t = Time::<Tai>::from_seconds(1_000_000_000);
1475        let s = t.to_string();
1476
1477        assert!(s.starts_with("TAI +"));
1478        assert!(s.contains("1000000000s"));
1479    }
1480
1481    #[test]
1482    fn test_utc_display_simple_format() {
1483        let s = Time::<Utc>::EPOCH.to_string();
1484
1485        assert!(s.starts_with("UTC +"));
1486    }
1487
1488    #[test]
1489    fn test_debug_shows_scale_and_nanos() {
1490        let t = Time::<Glonass>::from_nanos(777);
1491        let s = format!("{t:?}");
1492
1493        assert!(s.contains("GLO") && s.contains("777"));
1494    }
1495
1496    #[test]
1497    fn test_ordering() {
1498        let t0 = Time::<Gps>::from_seconds(0);
1499        let t1 = Time::<Gps>::from_seconds(1);
1500        let t2 = Time::<Gps>::from_seconds(2);
1501        let mut v = vec![t2, t0, t1];
1502
1503        v.sort();
1504
1505        assert_eq!(v, vec![t0, t1, t2]);
1506    }
1507
1508    #[test]
1509    fn test_glonass_day_accessor() {
1510        let t = Time::<Glonass>::from_day_tod(
1511            42,
1512            DurationParts {
1513                seconds: 3600,
1514                nanos: 0,
1515            },
1516        )
1517        .unwrap();
1518
1519        assert_eq!(t.day(), 42);
1520        assert_eq!(t.tod_seconds(), 3600);
1521    }
1522
1523    #[test]
1524    fn test_time_max_behavior() {
1525        let max = Time::<Gps>::MAX;
1526        let one_ns = Duration::ONE_NANOSECOND;
1527
1528        // checked_add returns None on overflow
1529        assert!(max.checked_add(one_ns).is_none());
1530
1531        // saturating_add clamps at MAX
1532        assert_eq!(max.saturating_add(one_ns), max);
1533
1534        // try_add returns error on overflow
1535        assert!(max.try_add(one_ns).is_err());
1536    }
1537
1538    #[test]
1539    fn test_max_is_u64_max() {
1540        assert_eq!(Time::<Gps>::MAX.as_nanos(), u64::MAX);
1541        assert_eq!(Time::<Glonass>::MAX.as_nanos(), u64::MAX);
1542        assert_eq!(Time::<Galileo>::MAX.as_nanos(), u64::MAX);
1543        assert_eq!(Time::<Beidou>::MAX.as_nanos(), u64::MAX);
1544        assert_eq!(Time::<Tai>::MAX.as_nanos(), u64::MAX);
1545        assert_eq!(Time::<Utc>::MAX.as_nanos(), u64::MAX);
1546    }
1547
1548    #[test]
1549    fn test_nanos_per_year_is_correct() {
1550        let expected: u64 = 365 * 24 * 3_600 * 1_000_000_000;
1551
1552        assert_eq!(Time::<Gps>::NANOS_PER_YEAR, expected);
1553    }
1554
1555    #[test]
1556    fn test_max_covers_at_least_500_years() {
1557        let years = Time::<Gps>::MAX.as_nanos() / Time::<Gps>::NANOS_PER_YEAR;
1558
1559        assert!(
1560            years >= 500,
1561            "MAX should cover at least 500 years, got {years}"
1562        );
1563    }
1564
1565    #[test]
1566    fn test_checked_add_one_ns_before_max_succeeds() {
1567        let t = Time::<Gps>::from_nanos(u64::MAX - 1);
1568        let result = t.checked_add(Duration::from_nanos(1));
1569
1570        assert_eq!(result, Some(Time::<Gps>::MAX));
1571    }
1572
1573    #[test]
1574    fn test_checked_add_at_max_overflows() {
1575        assert!(Time::<Gps>::MAX
1576            .checked_add(Duration::from_nanos(1))
1577            .is_none());
1578    }
1579
1580    #[test]
1581    fn test_checked_add_large_positive_overflows() {
1582        let t = Time::<Gps>::from_nanos(u64::MAX - 100);
1583
1584        assert!(t.checked_add(Duration::from_seconds(1)).is_none());
1585    }
1586
1587    #[test]
1588    fn test_checked_sub_one_ns_after_epoch_succeeds() {
1589        let t = Time::<Gps>::from_nanos(1);
1590        let result = t.checked_sub_duration(Duration::from_nanos(1));
1591
1592        assert_eq!(result, Some(Time::<Gps>::EPOCH));
1593    }
1594
1595    #[test]
1596    fn test_checked_sub_at_epoch_underflows() {
1597        assert!(Time::<Gps>::EPOCH
1598            .checked_sub_duration(Duration::from_nanos(1))
1599            .is_none());
1600    }
1601
1602    #[test]
1603    fn test_checked_sub_large_amount_underflows() {
1604        let t = Time::<Gps>::from_nanos(50);
1605
1606        assert!(t.checked_sub_duration(Duration::from_seconds(1)).is_none());
1607    }
1608
1609    #[test]
1610    fn test_saturating_add_negative_clamps_at_epoch() {
1611        assert_eq!(
1612            Time::<Gps>::EPOCH.saturating_add(Duration::from_nanos(-1)),
1613            Time::<Gps>::EPOCH
1614        );
1615    }
1616
1617    #[test]
1618    fn test_saturating_add_normal_value_works() {
1619        let t = Time::<Gps>::from_seconds(100);
1620
1621        assert_eq!(
1622            t.saturating_add(Duration::from_seconds(50)),
1623            Time::<Gps>::from_seconds(150)
1624        );
1625    }
1626
1627    #[test]
1628    fn test_saturating_sub_clamps_at_epoch() {
1629        assert_eq!(
1630            Time::<Gps>::EPOCH.saturating_sub_duration(Duration::from_nanos(1)),
1631            Time::<Gps>::EPOCH
1632        );
1633    }
1634
1635    #[test]
1636    fn test_saturating_sub_normal_value_works() {
1637        let t = Time::<Gps>::from_seconds(100);
1638
1639        assert_eq!(
1640            t.saturating_sub_duration(Duration::from_seconds(30)),
1641            Time::<Gps>::from_seconds(70)
1642        );
1643    }
1644
1645    #[test]
1646    fn test_try_add_overflow_returns_err() {
1647        let result = Time::<Gps>::MAX.try_add(Duration::from_nanos(1));
1648
1649        assert!(matches!(result, Err(GnssTimeError::Overflow)));
1650    }
1651
1652    #[test]
1653    fn test_try_add_valid_value_works() {
1654        let t = Time::<Gps>::from_seconds(1_000);
1655        let result = t.try_add(Duration::from_seconds(500)).unwrap();
1656
1657        assert_eq!(result.as_seconds(), 1_500);
1658    }
1659
1660    #[test]
1661    #[should_panic(expected = "Time<S> + Duration overflowed")]
1662    fn test_add_operator_panics_at_max() {
1663        let _ = Time::<Gps>::MAX + Duration::from_nanos(1);
1664    }
1665
1666    #[test]
1667    #[should_panic(expected = "Time<S> - Duration underflowed")]
1668    fn test_sub_operator_panics_at_epoch() {
1669        let _ = Time::<Gps>::EPOCH - Duration::from_nanos(1);
1670    }
1671
1672    #[test]
1673    fn test_checked_elapsed_zero_gives_zero_duration() {
1674        let t = Time::<Gps>::from_seconds(1_000);
1675        assert_eq!(t.checked_elapsed(t), Some(Duration::ZERO));
1676    }
1677
1678    #[test]
1679    fn test_checked_elapsed_overflows_when_gap_exceeds_i64() {
1680        // MAX - EPOCH = u64::MAX nanoseconds; i64 can hold roughly half of this range
1681        // The difference u64::MAX fits into i128, but not into i64 → None
1682        let result = Time::<Gps>::MAX.checked_elapsed(Time::<Gps>::EPOCH);
1683
1684        assert!(result.is_none(), "gap exceeds i64::MAX so must return None");
1685    }
1686
1687    #[test]
1688    fn test_checked_elapsed_within_i64_range_works() {
1689        let a = Time::<Gps>::from_seconds(1_000_000);
1690        let b = Time::<Gps>::from_seconds(500_000);
1691        let elapsed = a.checked_elapsed(b).unwrap();
1692
1693        assert_eq!(elapsed.as_seconds(), 500_000);
1694    }
1695
1696    #[test]
1697    fn test_unix_seconds_roundtrip() {
1698        let unix = 1_600_000_000; // 2020-09-13
1699        let utc = Time::<Utc>::from_unix_seconds(unix).unwrap();
1700
1701        assert_eq!(utc.as_unix_seconds(), unix);
1702    }
1703
1704    #[test]
1705    fn test_unix_nanos_roundtrip() {
1706        let unix_ns = 1_600_000_000_123_456_789;
1707        let utc = Time::<Utc>::from_unix_nanos(unix_ns).unwrap();
1708
1709        assert_eq!(utc.as_unix_nanos(), unix_ns);
1710    }
1711
1712    #[test]
1713    fn test_gps_display_format() {
1714        let t = Time::<Gps>::from_week_tow(
1715            2345,
1716            DurationParts {
1717                seconds: 432_000,
1718                nanos: 0,
1719            },
1720        )
1721        .unwrap();
1722        assert_eq!(t.to_string(), "GPS 2345:432000.000");
1723    }
1724
1725    #[test]
1726    fn test_saturating_add_clamps_at_max() {
1727        assert_eq!(
1728            Time::<Gps>::MAX.saturating_add(Duration::from_nanos(1)),
1729            Time::<Gps>::MAX
1730        );
1731    }
1732
1733    #[test]
1734    fn test_utc_from_unix_seconds_zero_fails() {
1735        // Unix epoch (1970-01-01) is before UTC epoch (1972-01-01)
1736        assert!(matches!(
1737            Time::<Utc>::from_unix_seconds(0),
1738            Err(GnssTimeError::Overflow)
1739        ));
1740    }
1741
1742    #[test]
1743    fn test_utc_from_unix_seconds_negative_fails() {
1744        assert!(matches!(
1745            Time::<Utc>::from_unix_seconds(-1),
1746            Err(GnssTimeError::Overflow)
1747        ));
1748    }
1749
1750    #[test]
1751    fn test_utc_from_unix_seconds_just_before_utc_epoch_fails() {
1752        // One second before 1972-01-01
1753        assert!(matches!(
1754            Time::<Utc>::from_unix_seconds(63_071_999),
1755            Err(GnssTimeError::Overflow)
1756        ));
1757    }
1758
1759    #[test]
1760    fn test_utc_from_unix_seconds_at_utc_epoch_gives_epoch() {
1761        // 1972-01-01 00:00:00 UTC = unix 63_072_000
1762        let utc = Time::<Utc>::from_unix_seconds(63_072_000).unwrap();
1763        assert_eq!(utc, Time::<Utc>::EPOCH);
1764    }
1765
1766    #[test]
1767    fn test_utc_from_unix_seconds_roundtrip() {
1768        let unix_s: i64 = 1_700_000_000; // 2023-11-14
1769        let utc = Time::<Utc>::from_unix_seconds(unix_s).unwrap();
1770        assert_eq!(utc.as_unix_seconds(), unix_s);
1771    }
1772
1773    #[test]
1774    fn test_utc_from_unix_seconds_known_date() {
1775        // 2024-01-01 00:00:00 UTC = Unix 1_704_067_200
1776        let unix_s: i64 = 1_704_067_200;
1777        let utc = Time::<Utc>::from_unix_seconds(unix_s).unwrap();
1778        assert_eq!(utc.as_unix_seconds(), unix_s);
1779    }
1780
1781    #[test]
1782    fn test_utc_as_unix_seconds_at_epoch_equals_offset() {
1783        use crate::UTC_EPOCH_UNIX_OFFSET_S;
1784        assert_eq!(
1785            Time::<Utc>::EPOCH.as_unix_seconds(),
1786            UTC_EPOCH_UNIX_OFFSET_S
1787        );
1788        assert_eq!(Time::<Utc>::EPOCH.as_unix_seconds(), 63_072_000);
1789    }
1790
1791    #[test]
1792    fn test_utc_as_unix_seconds_one_second_after_epoch() {
1793        let utc = Time::<Utc>::from_nanos(1_000_000_000); // 1 s after UTC epoch
1794        assert_eq!(utc.as_unix_seconds(), 63_072_001);
1795    }
1796
1797    #[test]
1798    fn test_utc_from_unix_nanos_at_utc_epoch() {
1799        use crate::UTC_EPOCH_UNIX_OFFSET_NS;
1800        let utc = Time::<Utc>::from_unix_nanos(UTC_EPOCH_UNIX_OFFSET_NS).unwrap();
1801        assert_eq!(utc, Time::<Utc>::EPOCH);
1802    }
1803
1804    #[test]
1805    fn test_utc_from_unix_nanos_zero_fails() {
1806        assert!(matches!(
1807            Time::<Utc>::from_unix_nanos(0),
1808            Err(GnssTimeError::Overflow)
1809        ));
1810    }
1811
1812    #[test]
1813    fn test_utc_from_unix_nanos_one_ns_before_utc_epoch_fails() {
1814        assert!(matches!(
1815            Time::<Utc>::from_unix_nanos(63_072_000_000_000_000 - 1),
1816            Err(GnssTimeError::Overflow)
1817        ));
1818    }
1819
1820    #[test]
1821    fn test_utc_from_unix_nanos_roundtrip() {
1822        let unix_ns: i64 = 1_700_000_000_123_456_789;
1823        let utc = Time::<Utc>::from_unix_nanos(unix_ns).unwrap();
1824        assert_eq!(utc.as_unix_nanos(), unix_ns);
1825    }
1826
1827    #[test]
1828    fn test_utc_as_unix_nanos_at_epoch() {
1829        use crate::UTC_EPOCH_UNIX_OFFSET_NS;
1830        assert_eq!(Time::<Utc>::EPOCH.as_unix_nanos(), UTC_EPOCH_UNIX_OFFSET_NS);
1831        assert_eq!(Time::<Utc>::EPOCH.as_unix_nanos(), 63_072_000_000_000_000);
1832    }
1833
1834    #[test]
1835    fn test_utc_as_unix_nanos_one_ns_after_epoch() {
1836        let utc = Time::<Utc>::from_nanos(1);
1837        assert_eq!(utc.as_unix_nanos(), 63_072_000_000_000_001);
1838    }
1839
1840    #[test]
1841    fn test_utc_unix_seconds_and_nanos_consistent() {
1842        let unix_s: i64 = 1_600_000_000;
1843        let unix_ns: i64 = unix_s * 1_000_000_000;
1844        let from_s = Time::<Utc>::from_unix_seconds(unix_s).unwrap();
1845        let from_ns = Time::<Utc>::from_unix_nanos(unix_ns).unwrap();
1846        assert_eq!(from_s, from_ns);
1847    }
1848
1849    #[test]
1850    fn test_utc_unix_nanos_sub_second_preserved() {
1851        let unix_ns: i64 = 1_700_000_000_500_000_000; // .5 s
1852        let utc = Time::<Utc>::from_unix_nanos(unix_ns).unwrap();
1853        // seconds part
1854        assert_eq!(utc.as_unix_seconds(), 1_700_000_000);
1855        // nanoseconds round-trip
1856        assert_eq!(utc.as_unix_nanos(), unix_ns);
1857    }
1858
1859    #[test]
1860    fn test_gps_from_unix_seconds_at_gps_epoch() {
1861        let ls = LeapSeconds::builtin();
1862        // GPS epoch (1980-01-06) in Unix time = 315_964_800
1863        // At that moment GPS − UTC = 0
1864        let gps = Time::<Gps>::from_unix_seconds(315_964_800, ls).unwrap();
1865        assert_eq!(gps, Time::<Gps>::EPOCH);
1866    }
1867
1868    #[test]
1869    fn test_gps_from_unix_seconds_before_utc_epoch_fails() {
1870        let ls = LeapSeconds::builtin();
1871        // Before 1972-01-01 (UTC epoch) → error in utc step
1872        assert!(Time::<Gps>::from_unix_seconds(0, ls).is_err());
1873    }
1874
1875    #[test]
1876    fn test_gps_as_unix_seconds_at_gps_epoch() {
1877        let ls = LeapSeconds::builtin();
1878        assert_eq!(Time::<Gps>::EPOCH.as_unix_seconds(ls).unwrap(), 315_964_800);
1879    }
1880
1881    #[test]
1882    fn test_gps_unix_seconds_roundtrip() {
1883        let ls = LeapSeconds::builtin();
1884        // 2020-01-01 00:00:00 UTC = Unix 1_577_836_800
1885        let unix_s: i64 = 1_577_836_800;
1886        let gps = Time::<Gps>::from_unix_seconds(unix_s, ls).unwrap();
1887        assert_eq!(gps.as_unix_seconds(ls).unwrap(), unix_s);
1888    }
1889
1890    #[test]
1891    fn test_gps_unix_seconds_post_2017() {
1892        let ls = LeapSeconds::builtin();
1893        // 2023-01-01 00:00:00 UTC = Unix 1_672_531_200
1894        let unix_s: i64 = 1_672_531_200;
1895        let gps = Time::<Gps>::from_unix_seconds(unix_s, ls).unwrap();
1896        assert_eq!(gps.as_unix_seconds(ls).unwrap(), unix_s);
1897    }
1898
1899    #[test]
1900    fn test_gps_unix_offset_is_18s_post_2017() {
1901        let ls = LeapSeconds::builtin();
1902        // In 2023, GPS − UTC = 18 s, so GPS seconds = unix − 315_964_800 + 18
1903        let unix_s: i64 = 1_672_531_200; // 2023-01-01 UTC
1904        let gps = Time::<Gps>::from_unix_seconds(unix_s, ls).unwrap();
1905        let expected_gps_s = u64::try_from(i128::from(unix_s) - 315_964_800i128 + 18i128).unwrap();
1906        assert_eq!(gps.as_seconds(), expected_gps_s);
1907    }
1908}