Skip to main content

deep_time/
leap_seconds.rs

1//! Leap seconds table from the official IANA
2//! [leap-seconds.list](https://data.iana.org/time-zones/data/leap-seconds.list)
3//! Updated through IERS Bulletin C as of April 2026.
4//! Last leap second: 2017-01-01 (TAI-UTC = 37 s)
5//! File expires: 28 December 2026
6
7use crate::Dt;
8
9#[cfg(feature = "std")]
10use std::{fs, io, path::Path};
11
12#[cfg(feature = "alloc")]
13use alloc::vec::Vec;
14
15#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
16pub struct LeapSec {
17    pub ntp_timestamp: i64,
18    pub leap_sec_after: i64,
19    // pub utc_sec: i64,
20    pub tai_sec: i64,
21}
22
23pub const LEAP_SECS: &[LeapSec] = &[
24    LeapSec {
25        ntp_timestamp: 2272060800,
26        leap_sec_after: 10,
27        // utc_sec: -883656000,
28        tai_sec: -883655991, // was -883655990
29    }, // 1 Jan 1972 (start of modern UTC definition)
30    LeapSec {
31        ntp_timestamp: 2287785600,
32        leap_sec_after: 11,
33        // utc_sec: -867931200,
34        tai_sec: -867931190, // was -867931189
35    }, // 1 Jul 1972
36    LeapSec {
37        ntp_timestamp: 2303683200,
38        leap_sec_after: 12,
39        // utc_sec: -852033600,
40        tai_sec: -852033589, // was -852033588
41    }, // 1 Jan 1973
42    LeapSec {
43        ntp_timestamp: 2335219200,
44        leap_sec_after: 13,
45        // utc_sec: -820497600,
46        tai_sec: -820497588, // was -820497587
47    }, // 1 Jan 1974
48    LeapSec {
49        ntp_timestamp: 2366755200,
50        leap_sec_after: 14,
51        // utc_sec: -788961600,
52        tai_sec: -788961587, // was -788961586
53    }, // 1 Jan 1975
54    LeapSec {
55        ntp_timestamp: 2398291200,
56        leap_sec_after: 15,
57        // utc_sec: -757425600,
58        tai_sec: -757425586, // was -757425585
59    }, // 1 Jan 1976
60    LeapSec {
61        ntp_timestamp: 2429913600,
62        leap_sec_after: 16,
63        // utc_sec: -725803200,
64        tai_sec: -725803185, // was -725803184
65    }, // 1 Jan 1977
66    LeapSec {
67        ntp_timestamp: 2461449600,
68        leap_sec_after: 17,
69        // utc_sec: -694267200,
70        tai_sec: -694267184, // was -694267183
71    }, // 1 Jan 1978
72    LeapSec {
73        ntp_timestamp: 2492985600,
74        leap_sec_after: 18,
75        // utc_sec: -662731200,
76        tai_sec: -662731183, // was -662731182
77    }, // 1 Jan 1979
78    LeapSec {
79        ntp_timestamp: 2524521600,
80        leap_sec_after: 19,
81        // utc_sec: -631195200,
82        tai_sec: -631195182, // was -631195181
83    }, // 1 Jan 1980
84    LeapSec {
85        ntp_timestamp: 2571782400,
86        leap_sec_after: 20,
87        // utc_sec: -583934400,
88        tai_sec: -583934381, // was -583934380
89    }, // 1 Jul 1981
90    LeapSec {
91        ntp_timestamp: 2603318400,
92        leap_sec_after: 21,
93        // utc_sec: -552398400,
94        tai_sec: -552398380, // was -552398379
95    }, // 1 Jul 1982
96    LeapSec {
97        ntp_timestamp: 2634854400,
98        leap_sec_after: 22,
99        // utc_sec: -520862400,
100        tai_sec: -520862379, // was -520862378
101    }, // 1 Jul 1983
102    LeapSec {
103        ntp_timestamp: 2698012800,
104        leap_sec_after: 23,
105        // utc_sec: -457704000,
106        tai_sec: -457703978, // was -457703977
107    }, // 1 Jul 1985
108    LeapSec {
109        ntp_timestamp: 2776982400,
110        leap_sec_after: 24,
111        // utc_sec: -378734400,
112        tai_sec: -378734377, // was -378734376
113    }, // 1 Jan 1988
114    LeapSec {
115        ntp_timestamp: 2840140800,
116        leap_sec_after: 25,
117        // utc_sec: -315576000,
118        tai_sec: -315575976, // was -315575975
119    }, // 1 Jan 1990
120    LeapSec {
121        ntp_timestamp: 2871676800,
122        leap_sec_after: 26,
123        // utc_sec: -284040000,
124        tai_sec: -284039975, // was -284039974
125    }, // 1 Jan 1991
126    LeapSec {
127        ntp_timestamp: 2918937600,
128        leap_sec_after: 27,
129        // utc_sec: -236779200,
130        tai_sec: -236779174, // was -236779173
131    }, // 1 Jul 1992
132    LeapSec {
133        ntp_timestamp: 2950473600,
134        leap_sec_after: 28,
135        // utc_sec: -205243200,
136        tai_sec: -205243173, // was -205243172
137    }, // 1 Jul 1993
138    LeapSec {
139        ntp_timestamp: 2982009600,
140        leap_sec_after: 29,
141        // utc_sec: -173707200,
142        tai_sec: -173707172, // was -173707171
143    }, // 1 Jul 1994
144    LeapSec {
145        ntp_timestamp: 3029443200,
146        leap_sec_after: 30,
147        // utc_sec: -126273600,
148        tai_sec: -126273571, // was -126273570
149    }, // 1 Jan 1996
150    LeapSec {
151        ntp_timestamp: 3076704000,
152        leap_sec_after: 31,
153        // utc_sec: -79012800,
154        tai_sec: -79012770, // was -79012769
155    }, // 1 Jul 1997
156    LeapSec {
157        ntp_timestamp: 3124137600,
158        leap_sec_after: 32,
159        // utc_sec: -31579200,
160        tai_sec: -31579169, // was -31579168
161    }, // 1 Jan 1999
162    LeapSec {
163        ntp_timestamp: 3345062400,
164        leap_sec_after: 33,
165        // utc_sec: 189345600,
166        tai_sec: 189345632, // was 189345633
167    }, // 1 Jan 2006
168    LeapSec {
169        ntp_timestamp: 3439756800,
170        leap_sec_after: 34,
171        // utc_sec: 284040000,
172        tai_sec: 284040033, // was 284040034
173    }, // 1 Jan 2009
174    LeapSec {
175        ntp_timestamp: 3550089600,
176        leap_sec_after: 35,
177        // utc_sec: 394372800,
178        tai_sec: 394372834, // was 394372835
179    }, // 1 Jul 2012
180    LeapSec {
181        ntp_timestamp: 3644697600,
182        leap_sec_after: 36,
183        // utc_sec: 488980800,
184        tai_sec: 488980835, // was 488980836
185    }, // 1 Jul 2015
186    LeapSec {
187        ntp_timestamp: 3692217600,
188        leap_sec_after: 37,
189        // utc_sec: 536500800,
190        tai_sec: 536500836, // was 536500837
191    }, // 1 Jan 2017
192];
193
194#[derive(Copy, Clone, Debug)]
195pub struct LeapInfo {
196    pub offset: i64,
197    pub leaps_inserted: i64,
198    pub is_leap_sec: bool,
199}
200
201impl Dt {
202    #[inline]
203    pub const fn leap_sec(&self, is_utc: bool) -> LeapInfo {
204        leap_sec(self, is_utc)
205    }
206
207    #[inline]
208    pub const fn leap_sec_using(&self, is_utc: bool, table: &[LeapSec]) -> LeapInfo {
209        leap_sec_using(self, is_utc, table)
210    }
211}
212
213#[inline]
214pub const fn leap_sec(dt: &Dt, is_utc: bool) -> LeapInfo {
215    leap_sec_using(dt, is_utc, LEAP_SECS)
216}
217
218pub const fn leap_sec_using(dt: &Dt, is_utc: bool, table: &[LeapSec]) -> LeapInfo {
219    let len = table.len();
220    if len == 0 {
221        return LeapInfo {
222            offset: 0,
223            leaps_inserted: 0,
224            is_leap_sec: false,
225        };
226    }
227
228    let target = dt.sec;
229
230    // Binary search for upper_bound: first index where entry_sec > target
231    let mut low = 0usize;
232    let mut high = len;
233    if is_utc {
234        while low < high {
235            let mid = low + (high - low) / 2;
236            let entry_sec = table[mid].tai_sec - table[mid].leap_sec_after + 1;
237            if entry_sec <= target {
238                low = mid + 1;
239            } else {
240                high = mid;
241            }
242        }
243    } else {
244        while low < high {
245            let mid = low + (high - low) / 2;
246            let entry_sec = table[mid].tai_sec;
247            if entry_sec <= target {
248                low = mid + 1;
249            } else {
250                high = mid;
251            }
252        }
253    }
254
255    // low == first index with entry_sec > target (or len)
256    if low == 0 {
257        return LeapInfo {
258            offset: 0,
259            leaps_inserted: 0,
260            is_leap_sec: false,
261        };
262    }
263
264    let idx = low - 1;
265    let entry = &table[idx];
266    let entry_sec = if is_utc {
267        entry.tai_sec - entry.leap_sec_after + 1
268    } else {
269        entry.tai_sec
270    };
271
272    let is_leap = target == entry_sec;
273
274    LeapInfo {
275        offset: entry.leap_sec_after,
276        leaps_inserted: (idx + 1) as i64,
277        is_leap_sec: is_leap,
278    }
279}
280
281#[cfg(feature = "std")]
282impl Dt {
283    /// Load directly from a file (e.g. the official IANA `leap-seconds.list`).
284    ///
285    /// Format should be the same as the file available at:
286    /// https://data.iana.org/time-zones/data/leap-seconds.list
287    ///
288    /// For rows that don't start with # (the data rows) the first column
289    /// should be the NTP timestamp, the second column (separated by whitespace)
290    /// should be the offset against TAI in seconds (the number of leap seconds at
291    /// that point).
292    ///
293    /// e.g.
294    ///
295    /// | #NTP Time  |    DTAI  |
296    /// |------------|----------|
297    /// | #          |          |
298    /// | 2272060800 |     10   |
299    /// | 2287785600 |     11   |
300    /// | 2303683200 |     12   |
301    pub fn leap_sec_data_from_file<P: AsRef<Path>>(path: P) -> io::Result<Vec<LeapSec>> {
302        let content = fs::read_to_string(path)?;
303        Ok(Self::leap_sec_data_from_str(&content))
304    }
305}
306
307#[cfg(feature = "alloc")]
308impl Dt {
309    /// Load directly from a str (e.g. the official IANA `leap-seconds.list`).
310    ///
311    /// Format should be the same as the file available at:
312    /// https://data.iana.org/time-zones/data/leap-seconds.list
313    ///
314    /// For rows that don't start with # (the data rows) the first column
315    /// should be the NTP timestamp, the second column (separated by whitespace)
316    /// should be the offset against TAI in seconds (the number of leap seconds at
317    /// that point).
318    ///
319    /// e.g.
320    ///
321    /// | #NTP Time  |    DTAI  |
322    /// |------------|----------|
323    /// | #          |          |
324    /// | 2272060800 |     10   |
325    /// | 2287785600 |     11   |
326    /// | 2303683200 |     12   |
327    ///
328    /// ## Example:
329    ///
330    /// ```ignore
331    /// let table = Self::leap_sec_from_str(&file_content_as_str);
332    /// ```
333    pub fn leap_sec_data_from_str(s: &str) -> Vec<LeapSec> {
334        use crate::Scale;
335
336        let mut table = Vec::new();
337        let mut prev_leap_sec_after: i64 = 0;
338
339        for line in s.lines() {
340            let trimmed = line.trim();
341
342            if trimmed.is_empty() || trimmed.starts_with("#") {
343                continue;
344            }
345
346            let parts: Vec<&str> = trimmed.split_whitespace().collect();
347
348            if parts.len() < 2 {
349                continue;
350            }
351            let Ok(ntp_timestamp) = parts[0].parse::<i64>() else {
352                continue;
353            };
354            let Ok(leap_sec_after) = parts[1].parse::<i64>() else {
355                continue;
356            };
357
358            // don't use current: UTC because it would use the internal leap table
359            let dt = Dt::from_ntp(f!(ntp_timestamp), Scale::TAI);
360            let tai_sec = if prev_leap_sec_after == 0 {
361                dt.sec + leap_sec_after - 1
362            } else {
363                dt.sec + leap_sec_after - (leap_sec_after - prev_leap_sec_after)
364            };
365            // let utc_sec = tai_sec - leap_sec_after + 1;
366
367            table.push(LeapSec {
368                ntp_timestamp,
369                leap_sec_after,
370                // utc_sec,
371                tai_sec,
372            });
373
374            prev_leap_sec_after = leap_sec_after;
375        }
376
377        table
378    }
379}