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