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