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::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}