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}