Skip to main content

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}