deep_time/utc/leap_seconds_fns.rs
1//! [`Dt`](../struct.Dt.html) impls for leap-second lookup and UTC↔TAI conversion.
2//!
3//! Uses the built-in [`LEAP_SECS`] list or a caller-provided [`LeapSec`] slice.
4//! [`LeapInfo`] is returned by [`Dt::leap_sec`](../struct.Dt.html#method.leap_sec) and related methods.
5
6use crate::utc::leap_seconds_list::{LEAP_SECS, LeapSec};
7use crate::{Dt, Scale};
8
9#[cfg(feature = "std")]
10use std::{fs, io, path::Path};
11
12#[cfg(feature = "alloc")]
13use alloc::vec::Vec;
14
15/// Indicates whether a queried instant falls exactly on a leap second transition.
16///
17/// This is returned by [`LeapInfo::is_leap_sec`] and is only ever set to a
18/// non-`None` value when the queried timestamp is *exactly* at the moment
19/// a leap second is inserted or removed.
20///
21/// - [`IsLeapSec::Add`] is returned for the inserted leap second
22/// (e.g. `23:59:60`).
23/// - [`IsLeapSec::Sub`] is returned for a negative (subtracted) leap second.
24/// - [`IsLeapSec::None`] is returned for all normal seconds.
25#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
26pub enum IsLeapSec {
27 /// This instant is **not** a leap second.
28 #[default]
29 None,
30 /// This instant is a positive leap second (a second is being inserted).
31 ///
32 /// Example: `2015-06-30 23:59:60 UTC`.
33 Add,
34 /// This instant is a negative leap second (a second is being removed).
35 ///
36 /// Perhaps this would work by having clocks skip the 59th second of a
37 /// minute, e.g. `2015-06-30 23:59:58 UTC` → `2015-07-01 00:00:00 UTC`.
38 Sub,
39}
40
41/// Leap-second details for an instant, returned by [`Dt::leap_sec`](../struct.Dt.html#method.leap_sec)
42/// and related methods.
43///
44/// ## See also
45///
46/// - [Dt::leap_sec](../struct.Dt.html#method.leap_sec)
47/// - [Dt::leap_sec_using_list](../struct.Dt.html#method.leap_sec_using_list)
48/// - [Dt::leap_sec_using_sec64](../struct.Dt.html#method.leap_sec_using_sec64)
49/// - [Dt::leap_sec_using_sec64_and_list](../struct.Dt.html#method.leap_sec_using_sec64_and_list)
50/// - [Dt::leap_sec_list_from_str](../struct.Dt.html#method.leap_sec_list_from_str)
51/// - [Dt::leap_sec_list_from_file](../struct.Dt.html#method.leap_sec_list_from_file)
52/// - [Dt::to_tai_from_utc_using_list](../struct.Dt.html#method.to_tai_from_utc_using_list)
53/// - [Dt::to_utc_from_tai_using_list](../struct.Dt.html#method.to_utc_from_tai_using_list)
54#[derive(Copy, Clone, Debug, PartialEq, Eq)]
55pub struct LeapInfo {
56 /// TAI minus UTC offset, in whole seconds.
57 pub offset: i64,
58 /// How many leap-second list entries come at or before the instant
59 /// (`1` on 1972-01-01).
60 pub n_entries_at_or_before: usize,
61 /// Whether the queried instant is exactly at a leap second transition point.
62 ///
63 /// - [`IsLeapSec::Add`] — this is the inserted leap second (`23:59:60`).
64 /// - [`IsLeapSec::Sub`] — this is a negative (removed) leap second.
65 /// - [`IsLeapSec::None`] — normal second (most common).
66 pub is_leap_sec: IsLeapSec,
67}
68
69impl Dt {
70 /// Get [`LeapInfo`] for a particular library timestamp (seconds from 2000-01-01 noon TAI)
71 /// using a provided leap seconds list.
72 ///
73 /// - If the timestamp is currently on the UTC time scale then use **`true`** for the `is_utc`
74 /// parameter.
75 /// - If the timestamp is currently on the TAI time scale then use **`false`** for the `is_utc`
76 /// parameter.
77 /// - `sec64` should be such that it was produced using euclid division, see
78 /// [`Dt::to_sec64`](../struct.Dt.html#method.to_sec64) for more info. This only applies to
79 /// negative `sec64` values.
80 ///
81 /// ## See also
82 ///
83 /// For more information on how to make a leap seconds list, see the following functions:
84 ///
85 /// - [Dt::leap_sec_list_from_str](../struct.Dt.html#method.leap_sec_list_from_str)
86 /// - [Dt::leap_sec_list_from_file](../struct.Dt.html#method.leap_sec_list_from_file)
87 /// - [Dt::to_tai_from_utc_using_list](../struct.Dt.html#method.to_tai_from_utc_using_list)
88 /// - [Dt::to_utc_from_tai_using_list](../struct.Dt.html#method.to_utc_from_tai_using_list)
89 pub const fn leap_sec_using_sec64_and_list(
90 sec64: i64,
91 is_utc: bool,
92 list: &[LeapSec],
93 ) -> Option<LeapInfo> {
94 let len = list.len();
95 if len == 0 {
96 return None;
97 }
98
99 // Binary search for upper_bound: first index where entry_sec > sec64
100 let mut low = 0usize;
101 let mut high = len;
102 if is_utc {
103 while low < high {
104 let mid = low + (high - low) / 2;
105 if list[mid].utc_sec <= sec64 {
106 low = mid + 1;
107 } else {
108 high = mid;
109 }
110 }
111 } else {
112 while low < high {
113 let mid = low + (high - low) / 2;
114 if list[mid].tai_sec <= sec64 {
115 low = mid + 1;
116 } else {
117 high = mid;
118 }
119 }
120 }
121
122 // low == first index with entry_sec > sec64 (or len)
123 if low == 0 {
124 return None;
125 }
126
127 let idx = low - 1;
128 let entry = &list[idx];
129 let is_leap = {
130 if sec64 != if is_utc { entry.utc_sec } else { entry.tai_sec } {
131 IsLeapSec::None
132 } else if idx != 0 {
133 let prev_leap_sec_after = list[idx - 1].leap_sec_after;
134 if entry.leap_sec_after > prev_leap_sec_after {
135 IsLeapSec::Add
136 } else if entry.leap_sec_after < prev_leap_sec_after {
137 IsLeapSec::Sub
138 } else {
139 IsLeapSec::None
140 }
141 } else if entry.leap_sec_after > 0 {
142 IsLeapSec::Add
143 } else if entry.leap_sec_after < 0 {
144 IsLeapSec::Sub
145 } else {
146 IsLeapSec::None
147 }
148 };
149
150 Some(LeapInfo {
151 offset: entry.leap_sec_after,
152 n_entries_at_or_before: low,
153 is_leap_sec: is_leap,
154 })
155 }
156
157 /// Get [`LeapInfo`] for a particular library timestamp (seconds from 2000-01-01 noon TAI)
158 /// using the library's in-built leap seconds list.
159 ///
160 /// - If the timestamp is currently on the UTC time scale then use **`true`** for the `is_utc`
161 /// parameter.
162 /// - If the timestamp is currently on the TAI time scale then use **`false`** for the `is_utc`
163 /// parameter.
164 /// - `sec64` should be such that it was produced using euclid division, see
165 /// [`Dt::to_sec64`](../struct.Dt.html#method.to_sec64) for more info. This only applies to
166 /// negative `sec64` values.
167 #[inline(always)]
168 pub const fn leap_sec_using_sec64(sec64: i64, is_utc: bool) -> Option<LeapInfo> {
169 Self::leap_sec_using_sec64_and_list(sec64, is_utc, LEAP_SECS)
170 }
171
172 /// Get the leap seconds info for this instant.
173 ///
174 /// Uses the library's in-built leap seconds list.
175 #[inline(always)]
176 pub const fn leap_sec(&self, is_utc: bool) -> Option<LeapInfo> {
177 Self::leap_sec_using_sec64_and_list(self.to_sec64(), is_utc, LEAP_SECS)
178 }
179
180 /// Get the leap seconds info for this instant with a given list.
181 #[inline(always)]
182 pub const fn leap_sec_using_list(&self, is_utc: bool, list: &[LeapSec]) -> Option<LeapInfo> {
183 Self::leap_sec_using_sec64_and_list(self.to_sec64(), is_utc, list)
184 }
185
186 #[inline(always)]
187 pub(crate) const fn utc_to_tai_using_list(&self, list: &[LeapSec]) -> Option<Dt> {
188 match self.leap_sec_using_list(true, list) {
189 Some(info) => Some(self.add_sec(info.offset as i128)),
190 None => None,
191 }
192 }
193
194 #[inline(always)]
195 pub(crate) const fn tai_to_utc_using_list(&self, list: &[LeapSec]) -> Option<Dt> {
196 match self.leap_sec_using_list(false, list) {
197 Some(info) => Some(self.add_sec(-info.offset as i128)),
198 None => None,
199 }
200 }
201
202 /// Converts **UTC -> TAI** using a provided Leap seconds list.
203 ///
204 /// - If the
205 /// [`Dt`](../struct.Dt.html) is before the provided leap second list's
206 /// first entry then the library's own conversion is used to convert to
207 /// [`Scale::TAI`](../enum.Scale.html#variant.TAI)
208 ///
209 /// ## Examples
210 ///
211 /// ```rust
212 /// # #[cfg(feature = "std")] {
213 /// use deep_time::utc::{IsLeapSec, LEAP_SECS};
214 /// use deep_time::{Dt, Scale};
215 ///
216 /// let leap_seconds_list =
217 /// Dt::leap_sec_list_from_file("tests/assets/leap-seconds.list.txt").unwrap();
218 /// assert_eq!(leap_seconds_list[1], LEAP_SECS[1]);
219 ///
220 /// let x = Dt::from_ymd(2015, 6, 30, Scale::UTC, 23, 59, 60, 0);
221 /// let leap_sec = x.leap_sec_using_list(false, &leap_seconds_list).unwrap();
222 /// assert!(leap_sec.is_leap_sec == IsLeapSec::Add);
223 ///
224 /// let dt = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
225 ///
226 /// let utc1 = dt.to(Scale::UTC);
227 /// let utc2 = dt.to_utc_from_tai_using_list(Scale::UTC, &leap_seconds_list);
228 /// assert_eq!(utc1, utc2);
229 ///
230 /// let tai1 = utc1.to_tai();
231 /// let tai2 = utc2.to_tai_from_utc_using_list(&leap_seconds_list);
232 /// assert_eq!(tai1, tai2);
233 /// # }
234 /// ```
235 ///
236 /// ## See also
237 ///
238 /// - [Dt::leap_sec_list_from_str](../struct.Dt.html#method.leap_sec_list_from_str)
239 /// - [Dt::leap_sec_list_from_file](../struct.Dt.html#method.leap_sec_list_from_file)
240 /// - [Dt::to_utc_from_tai_using_list](../struct.Dt.html#method.to_utc_from_tai_using_list)
241 pub const fn to_tai_from_utc_using_list(&self, list: &[LeapSec]) -> Dt {
242 match self.scale {
243 // we're going utc -> tai, check if it's
244 // post start of list using the provided leap seconds list
245 Scale::UTC | Scale::UtcHist | Scale::UtcSpice => {
246 match self.utc_to_tai_using_list(list) {
247 // leap seconds list returned an offset, so use that
248 Some(dt) => dt.with(Scale::TAI),
249 // leap seconds list returned None so it must be pre 1972
250 None => match self.scale {
251 Scale::UtcHist => match self.historical_utc_offset() {
252 Some(offset) => self.add(Dt::span_f(offset)).with(Scale::TAI),
253 None => self.with(Scale::TAI),
254 },
255 Scale::UtcSpice => self.add_sec(9).with(Scale::TAI),
256 _ => self.with(Scale::TAI),
257 },
258 }
259 }
260 // defer to library conversion function
261 _ => self.to(Scale::TAI),
262 }
263 }
264
265 /// Converts **TAI -> UTC** using a provided Leap seconds list.
266 ///
267 /// - If `new` is
268 /// [`Scale::UtcHist`](../enum.Scale.html#variant.UtcHist) or
269 /// [`Scale::UtcSpice`](../enum.Scale.html#variant.UtcSpice) and the
270 /// [`Dt`](../struct.Dt.html) is before the provided leap second list's
271 /// first entry then the library's own conversion is used to convert to
272 /// `new`.
273 /// - If `new` is not one of the scales that uses leap seconds then the library's
274 /// own conversion is used to convert to `new`.
275 ///
276 /// ## Examples
277 ///
278 /// ```rust
279 /// # #[cfg(feature = "std")] {
280 /// use deep_time::utc::{IsLeapSec, LEAP_SECS};
281 /// use deep_time::{Dt, Scale};
282 ///
283 /// let leap_seconds_list =
284 /// Dt::leap_sec_list_from_file("tests/assets/leap-seconds.list.txt").unwrap();
285 /// assert_eq!(leap_seconds_list[1], LEAP_SECS[1]);
286 ///
287 /// let x = Dt::from_ymd(2015, 6, 30, Scale::UTC, 23, 59, 60, 0);
288 /// let leap_sec = x.leap_sec_using_list(false, &leap_seconds_list).unwrap();
289 /// assert!(leap_sec.is_leap_sec == IsLeapSec::Add);
290 ///
291 /// let dt = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
292 ///
293 /// let utc1 = dt.to(Scale::UTC);
294 /// let utc2 = dt.to_utc_from_tai_using_list(Scale::UTC, &leap_seconds_list);
295 /// assert_eq!(utc1, utc2);
296 ///
297 /// let tai1 = utc1.to_tai();
298 /// let tai2 = utc2.to_tai_from_utc_using_list(&leap_seconds_list);
299 /// assert_eq!(tai1, tai2);
300 /// # }
301 /// ```
302 ///
303 /// ## See also
304 ///
305 /// - [Dt::leap_sec_list_from_str](../struct.Dt.html#method.leap_sec_list_from_str)
306 /// - [Dt::leap_sec_list_from_file](../struct.Dt.html#method.leap_sec_list_from_file)
307 /// - [Dt::to_tai_from_utc_using_list](../struct.Dt.html#method.to_tai_from_utc_using_list)
308 pub const fn to_utc_from_tai_using_list(&self, new: Scale, list: &[LeapSec]) -> Dt {
309 match new {
310 Scale::UTC | Scale::UtcHist | Scale::UtcSpice => {
311 match self.tai_to_utc_using_list(list) {
312 // leap seconds list returned an offset, so use that
313 Some(dt) => dt.with(new),
314 // leap seconds list returned None so it must be pre 1972
315 None => match new {
316 Scale::UtcHist => match self.historical_utc_offset() {
317 Some(offset) => self.sub(Dt::span_f(offset)).with(new),
318 None => self.with(new),
319 },
320 Scale::UtcSpice => self.add_sec(-9).with(new),
321 _ => self.with(new),
322 },
323 }
324 }
325 // defer to library conversion function
326 _ => self.to(new),
327 }
328 }
329}
330
331#[cfg(feature = "alloc")]
332impl Dt {
333 /// Load directly from a str (e.g. the official IANA `leap-seconds.list`).
334 ///
335 /// Format should be the same as the file available at:
336 /// <https://data.iana.org/time-zones/data/leap-seconds.list>
337 ///
338 /// For rows that don't start with # (the list rows) the first column
339 /// should be the NTP timestamp, the second column (separated by whitespace)
340 /// should be the offset against TAI in seconds (the number of leap seconds at
341 /// that point).
342 ///
343 /// e.g.
344 ///
345 /// | #NTP Time | DTAI |
346 /// |------------|----------|
347 /// | # | |
348 /// | 2272060800 | 10 |
349 /// | 2287785600 | 11 |
350 /// | 2303683200 | 12 |
351 ///
352 /// ## See also
353 ///
354 /// - [Dt::leap_sec_list_from_file](../struct.Dt.html#method.leap_sec_list_from_file)
355 /// - [Dt::to_utc_from_tai_using_list](../struct.Dt.html#method.to_utc_from_tai_using_list)
356 /// - [Dt::to_tai_from_utc_using_list](../struct.Dt.html#method.to_tai_from_utc_using_list)
357 pub fn leap_sec_list_from_str(s: &str) -> Vec<LeapSec> {
358 use crate::Scale;
359
360 let mut list = Vec::new();
361 let mut prev_leap_sec_after: i64 = 0;
362 let mut entries_pushed: usize = 0;
363
364 for line in s.lines() {
365 let trimmed = line.trim();
366
367 if trimmed.is_empty() || trimmed.starts_with("#") {
368 continue;
369 }
370
371 let mut parts = trimmed.split_whitespace();
372
373 let ntp_timestamp = if let Some(num) = parts.next() {
374 match num.parse::<i64>() {
375 Ok(n) => n,
376 Err(_) => continue,
377 }
378 } else {
379 continue;
380 };
381 let leap_sec_after = if let Some(num) = parts.next() {
382 match num.parse::<i64>() {
383 Ok(n) => n,
384 Err(_) => continue,
385 }
386 } else {
387 continue;
388 };
389
390 // don't use current: UTC because it would use the internal leap list
391 let utc_sec = Dt::from_ntp(Dt::from_sec(ntp_timestamp as i128, Scale::TAI)).to_sec64();
392
393 let tai_sec = if entries_pushed == 0 {
394 if leap_sec_after > 0 {
395 utc_sec + leap_sec_after - 1
396 } else {
397 // hypothetical negative first entry
398 utc_sec + leap_sec_after
399 }
400 } else {
401 utc_sec + prev_leap_sec_after
402 };
403
404 list.push(LeapSec {
405 ntp_timestamp,
406 leap_sec_after,
407 utc_sec,
408 tai_sec,
409 });
410
411 prev_leap_sec_after = leap_sec_after;
412 entries_pushed += 1;
413 }
414
415 list
416 }
417}
418
419#[cfg(feature = "std")]
420impl Dt {
421 /// Load directly from a file (e.g. the official IANA `leap-seconds.list`).
422 ///
423 /// Format should be the same as the file available at:
424 /// <https://data.iana.org/time-zones/data/leap-seconds.list>
425 ///
426 /// For rows that don't start with # (the list rows) the first column
427 /// should be the NTP timestamp, the second column (separated by whitespace)
428 /// should be the offset against TAI in seconds (the number of leap seconds at
429 /// that point).
430 ///
431 /// e.g.
432 ///
433 /// | #NTP Time | DTAI |
434 /// |------------|----------|
435 /// | # | |
436 /// | 2272060800 | 10 |
437 /// | 2287785600 | 11 |
438 /// | 2303683200 | 12 |
439 ///
440 /// ## Examples
441 ///
442 /// ```rust
443 /// # #[cfg(feature = "std")] {
444 /// use deep_time::utc::{IsLeapSec, LEAP_SECS};
445 /// use deep_time::{Dt, Scale};
446 ///
447 /// let leap_seconds_list =
448 /// Dt::leap_sec_list_from_file("tests/assets/leap-seconds.list.txt").unwrap();
449 /// assert_eq!(leap_seconds_list[1], LEAP_SECS[1]);
450 ///
451 /// let x = Dt::from_ymd(2015, 6, 30, Scale::UTC, 23, 59, 60, 0);
452 /// let leap_sec = x.leap_sec_using_list(false, &leap_seconds_list).unwrap();
453 /// assert!(leap_sec.is_leap_sec == IsLeapSec::Add);
454 ///
455 /// let dt = Dt::from_ymd(2000, 1, 1, Scale::TAI, 12, 0, 0, 0);
456 ///
457 /// let utc1 = dt.to(Scale::UTC);
458 /// let utc2 = dt.to_utc_from_tai_using_list(Scale::UTC, &leap_seconds_list);
459 /// assert_eq!(utc1, utc2);
460 ///
461 /// let tai1 = utc1.to_tai();
462 /// let tai2 = utc2.to_tai_from_utc_using_list(&leap_seconds_list);
463 /// assert_eq!(tai1, tai2);
464 /// # }
465 /// ```
466 ///
467 /// ## See also
468 ///
469 /// - [Dt::leap_sec_list_from_str](../struct.Dt.html#method.leap_sec_list_from_str)
470 /// - [Dt::to_utc_from_tai_using_list](../struct.Dt.html#method.to_utc_from_tai_using_list)
471 /// - [Dt::to_tai_from_utc_using_list](../struct.Dt.html#method.to_tai_from_utc_using_list)
472 #[inline]
473 pub fn leap_sec_list_from_file<P: AsRef<Path>>(path: P) -> io::Result<Vec<LeapSec>> {
474 let content = fs::read_to_string(path)?;
475 Ok(Self::leap_sec_list_from_str(&content))
476 }
477}