Skip to main content

leap_sec/
types.rs

1//! Newtype wrappers for timestamps in different time scales.
2//!
3//! Each type carries an explicit scale label so the compiler prevents mixing
4//! UTC, TAI, and GPST values at call sites.
5//!
6//! # Seconds types
7//!
8//! | Type | Scale | Inner |
9//! |------|-------|-------|
10//! | [`UtcUnixSeconds`] | UTC | `i64` |
11//! | [`TaiSeconds`] | TAI | `i64` |
12//! | [`GpstSeconds`] | GPST | `i64` |
13//!
14//! # Nanosecond types
15//!
16//! | Type | Scale | Inner |
17//! |------|-------|-------|
18//! | [`UtcUnixNanos`] | UTC | `i128` |
19//! | [`TaiNanos`] | TAI | `i128` |
20//! | [`GpstNanos`] | GPST | `i128` |
21//!
22//! Use [`From`] to promote seconds to nanoseconds (lossless), and
23//! [`to_seconds_floor()`](UtcUnixNanos::to_seconds_floor) to truncate back.
24
25use core::fmt;
26
27/// Unix-like seconds count in the UTC scale.
28///
29/// This is the standard POSIX timestamp — seconds since 1970-01-01T00:00:00 UTC,
30/// with leap seconds folded (the 61st second `23:59:60` shares the same POSIX
31/// value as the following `00:00:00`).
32///
33/// Use [`LeapSeconds::utc_to_tai`](crate::LeapSeconds::utc_to_tai) to convert
34/// to TAI, or promote to [`UtcUnixNanos`] via `.into()`.
35///
36/// # Example
37///
38/// ```
39/// use leap_sec::UtcUnixSeconds;
40///
41/// let utc = UtcUnixSeconds(1_700_000_000); // 2023-11-14 22:13:20 UTC
42/// assert_eq!(format!("{utc}"), "1700000000 UTC");
43/// ```
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45pub struct UtcUnixSeconds(pub i64);
46
47/// Continuous seconds count in the TAI scale (no leap seconds).
48///
49/// TAI is ahead of UTC by a varying number of whole seconds (37 as of 2017-01-01).
50/// The inner `i64` counts seconds since the Unix epoch in TAI.
51///
52/// Use [`LeapSeconds::tai_to_utc`](crate::LeapSeconds::tai_to_utc) to convert
53/// back to UTC, or [`tai_to_gpst`](crate::tai_to_gpst) to convert to GPST.
54///
55/// # Example
56///
57/// ```
58/// use leap_sec::TaiSeconds;
59///
60/// let tai = TaiSeconds(1_700_000_037);
61/// assert_eq!(format!("{tai}"), "1700000037 TAI");
62/// ```
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
64pub struct TaiSeconds(pub i64);
65
66/// Continuous seconds count in the GPS time scale.
67///
68/// GPST is offset from TAI by exactly 19 seconds: `GPST = TAI − 19`.
69/// The inner `i64` counts seconds since the Unix epoch in GPST.
70///
71/// Convert to/from TAI with [`tai_to_gpst`](crate::tai_to_gpst) and
72/// [`gpst_to_tai`](crate::gpst_to_tai). Convert to/from UTC via
73/// [`LeapSeconds::utc_to_gpst`](crate::LeapSeconds::utc_to_gpst).
74///
75/// # Example
76///
77/// ```
78/// use leap_sec::GpstSeconds;
79///
80/// let gpst = GpstSeconds(1_700_000_018);
81/// assert_eq!(format!("{gpst}"), "1700000018 GPST");
82/// ```
83#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub struct GpstSeconds(pub i64);
85
86/// Unix-epoch nanoseconds in the UTC scale.
87///
88/// Uses `i128` to hold the full range of Unix nanoseconds without overflow.
89/// Promote from [`UtcUnixSeconds`] via `.into()`, truncate back with
90/// [`to_seconds_floor`](Self::to_seconds_floor).
91///
92/// # Example
93///
94/// ```
95/// use leap_sec::{UtcUnixSeconds, UtcUnixNanos};
96///
97/// let ns = UtcUnixNanos(1_700_000_000_500_000_000);
98/// assert_eq!(format!("{ns}"), "1700000000500000000 UTC");
99///
100/// // Promote from seconds
101/// let promoted: UtcUnixNanos = UtcUnixSeconds(1_700_000_000).into();
102/// assert_eq!(promoted, UtcUnixNanos(1_700_000_000_000_000_000));
103/// ```
104#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
105pub struct UtcUnixNanos(pub i128);
106
107/// Continuous nanoseconds in the TAI scale.
108///
109/// Uses `i128` to hold the full range of nanoseconds without overflow.
110/// Promote from [`TaiSeconds`] via `.into()`, truncate back with
111/// [`to_seconds_floor`](Self::to_seconds_floor).
112///
113/// # Example
114///
115/// ```
116/// use leap_sec::{TaiSeconds, TaiNanos};
117///
118/// let ns = TaiNanos(1_700_000_037_500_000_000);
119/// assert_eq!(format!("{ns}"), "1700000037500000000 TAI");
120///
121/// let promoted: TaiNanos = TaiSeconds(1_700_000_037).into();
122/// assert_eq!(promoted, TaiNanos(1_700_000_037_000_000_000));
123/// ```
124#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
125pub struct TaiNanos(pub i128);
126
127/// Continuous nanoseconds in the GPS time scale.
128///
129/// Uses `i128` to hold the full range of nanoseconds without overflow.
130/// Promote from [`GpstSeconds`] via `.into()`, truncate back with
131/// [`to_seconds_floor`](Self::to_seconds_floor).
132///
133/// # Example
134///
135/// ```
136/// use leap_sec::{GpstSeconds, GpstNanos};
137///
138/// let ns = GpstNanos(1_700_000_018_500_000_000);
139/// assert_eq!(format!("{ns}"), "1700000018500000000 GPST");
140///
141/// let promoted: GpstNanos = GpstSeconds(1_700_000_018).into();
142/// assert_eq!(promoted, GpstNanos(1_700_000_018_000_000_000));
143/// ```
144#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
145pub struct GpstNanos(pub i128);
146
147const NANOS_PER_SECOND: i128 = 1_000_000_000;
148
149// ---------------------------------------------------------------------------
150// Display — always shows the scale label
151// ---------------------------------------------------------------------------
152
153/// Displays as `"{seconds} UTC"` (e.g., `"1700000000 UTC"`).
154impl fmt::Display for UtcUnixSeconds {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(f, "{} UTC", self.0)
157    }
158}
159
160/// Displays as `"{seconds} TAI"` (e.g., `"1700000037 TAI"`).
161impl fmt::Display for TaiSeconds {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        write!(f, "{} TAI", self.0)
164    }
165}
166
167/// Displays as `"{seconds} GPST"` (e.g., `"1700000018 GPST"`).
168impl fmt::Display for GpstSeconds {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        write!(f, "{} GPST", self.0)
171    }
172}
173
174/// Displays as `"{nanoseconds} UTC"` (e.g., `"1700000000500000000 UTC"`).
175impl fmt::Display for UtcUnixNanos {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        write!(f, "{} UTC", self.0)
178    }
179}
180
181/// Displays as `"{nanoseconds} TAI"` (e.g., `"1700000037500000000 TAI"`).
182impl fmt::Display for TaiNanos {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        write!(f, "{} TAI", self.0)
185    }
186}
187
188/// Displays as `"{nanoseconds} GPST"` (e.g., `"1700000018500000000 GPST"`).
189impl fmt::Display for GpstNanos {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        write!(f, "{} GPST", self.0)
192    }
193}
194
195// ---------------------------------------------------------------------------
196// From<Seconds> for Nanos — lossless promotion
197// ---------------------------------------------------------------------------
198
199/// Lossless promotion: multiplies by 1,000,000,000.
200impl From<UtcUnixSeconds> for UtcUnixNanos {
201    fn from(s: UtcUnixSeconds) -> Self {
202        Self(i128::from(s.0) * NANOS_PER_SECOND)
203    }
204}
205
206/// Lossless promotion: multiplies by 1,000,000,000.
207impl From<TaiSeconds> for TaiNanos {
208    fn from(s: TaiSeconds) -> Self {
209        Self(i128::from(s.0) * NANOS_PER_SECOND)
210    }
211}
212
213/// Lossless promotion: multiplies by 1,000,000,000.
214impl From<GpstSeconds> for GpstNanos {
215    fn from(s: GpstSeconds) -> Self {
216        Self(i128::from(s.0) * NANOS_PER_SECOND)
217    }
218}
219
220// ---------------------------------------------------------------------------
221// to_seconds_floor — truncate nanos to whole seconds (floor division)
222// ---------------------------------------------------------------------------
223
224impl UtcUnixNanos {
225    /// Truncate to whole seconds, rounding toward negative infinity.
226    ///
227    /// # Example
228    ///
229    /// ```
230    /// use leap_sec::{UtcUnixSeconds, UtcUnixNanos};
231    ///
232    /// let ns = UtcUnixNanos(1_700_000_000_999_999_999);
233    /// assert_eq!(ns.to_seconds_floor(), UtcUnixSeconds(1_700_000_000));
234    ///
235    /// // Negative values floor correctly
236    /// assert_eq!(UtcUnixNanos(-1).to_seconds_floor(), UtcUnixSeconds(-1));
237    /// ```
238    #[allow(clippy::cast_possible_truncation)]
239    pub const fn to_seconds_floor(self) -> UtcUnixSeconds {
240        UtcUnixSeconds(floor_div_i128(self.0, NANOS_PER_SECOND) as i64)
241    }
242}
243
244impl TaiNanos {
245    /// Truncate to whole seconds, rounding toward negative infinity.
246    ///
247    /// # Example
248    ///
249    /// ```
250    /// use leap_sec::{TaiSeconds, TaiNanos};
251    ///
252    /// let ns = TaiNanos(1_700_000_037_500_000_000);
253    /// assert_eq!(ns.to_seconds_floor(), TaiSeconds(1_700_000_037));
254    /// ```
255    #[allow(clippy::cast_possible_truncation)]
256    pub const fn to_seconds_floor(self) -> TaiSeconds {
257        TaiSeconds(floor_div_i128(self.0, NANOS_PER_SECOND) as i64)
258    }
259}
260
261impl GpstNanos {
262    /// Truncate to whole seconds, rounding toward negative infinity.
263    ///
264    /// # Example
265    ///
266    /// ```
267    /// use leap_sec::{GpstSeconds, GpstNanos};
268    ///
269    /// let ns = GpstNanos(1_700_000_018_500_000_000);
270    /// assert_eq!(ns.to_seconds_floor(), GpstSeconds(1_700_000_018));
271    /// ```
272    #[allow(clippy::cast_possible_truncation)]
273    pub const fn to_seconds_floor(self) -> GpstSeconds {
274        GpstSeconds(floor_div_i128(self.0, NANOS_PER_SECOND) as i64)
275    }
276}
277
278/// Floor division for i128 — rounds toward negative infinity.
279const fn floor_div_i128(a: i128, b: i128) -> i128 {
280    let d = a / b;
281    let r = a % b;
282    if (r != 0) && ((r ^ b) < 0) {
283        d - 1
284    } else {
285        d
286    }
287}