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}