Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/time/leap_second.rs

16.3 KiB, 81 runs

created by r1870400018:8564, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
2//! Anthropic Claude
3
4use oxedyne_fe2o3_core::prelude::*;
5
6use std::cmp::Ordering;
7
8/// Leap seconds are announced by the IERS and are not carried in timezone data,
9/// so the table is kept separately from the zone rules.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct LeapSecondEntry {
12 pub utc_timestamp: i64, // seconds since the Unix epoch
13 pub tai_utc_offset: i32, // seconds, after this leap second
14 pub description: String,
15}
16
17impl PartialOrd for LeapSecondEntry {
18 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
19 Some(self.cmp(other))
20 }
21}
22
23impl Ord for LeapSecondEntry {
24 fn cmp(&self, other: &Self) -> Ordering {
25 self.utc_timestamp.cmp(&other.utc_timestamp)
26 }
27}
28
29/// Every leap second from 1972 onwards, which is what a UTC to TAI conversion
30/// needs.
31///
32/// ```ignore
33/// use oxedyne_fe2o3_datime::time::LeapSecondTable;
34///
35/// let table = LeapSecondTable::standard();
36/// let utc_timestamp = 1609459200; // 2021-01-01 00:00:00 UTC
37/// let tai_utc_offset = table.tai_utc_offset_at(utc_timestamp);
38/// println!("TAI-UTC offset: {} seconds", tai_utc_offset);
39/// ```
40#[derive(Clone, Debug)]
41pub struct LeapSecondTable {
42 entries: Vec<LeapSecondEntry>, // ascending by utc_timestamp
43 enabled: bool,
44}
45
46impl LeapSecondTable {
47 pub fn new() -> Self {
48 Self {
49 entries: Vec::new(),
50 enabled: true,
51 }
52 }
53
54 /// Every leap second from 1972 to 2017, which is all of them so far. The table
55 /// needs extending whenever the IERS announces another.
56 pub fn standard() -> Self {
57 let mut table = Self::new();
58
59 // Historical leap seconds from 1972-2017
60 // Source: IERS Bulletin C and NIST leap second data
61 table.add_entry(78796800, 10, "1972-07-01: First leap second");
62 table.add_entry(94694400, 11, "1973-01-01: +1 second");
63 table.add_entry(126230400, 12, "1974-01-01: +1 second");
64 table.add_entry(157766400, 13, "1975-01-01: +1 second");
65 table.add_entry(189302400, 14, "1976-01-01: +1 second");
66 table.add_entry(220924800, 15, "1977-01-01: +1 second");
67 table.add_entry(252460800, 16, "1978-01-01: +1 second");
68 table.add_entry(283996800, 17, "1979-01-01: +1 second");
69 table.add_entry(315532800, 18, "1980-01-01: +1 second");
70 table.add_entry(362793600, 19, "1981-07-01: +1 second");
71 table.add_entry(394329600, 20, "1982-07-01: +1 second");
72 table.add_entry(425865600, 21, "1983-07-01: +1 second");
73 table.add_entry(489024000, 22, "1985-07-01: +1 second");
74 table.add_entry(567993600, 23, "1988-01-01: +1 second");
75 table.add_entry(631152000, 24, "1990-01-01: +1 second");
76 table.add_entry(662688000, 25, "1991-01-01: +1 second");
77 table.add_entry(709948800, 26, "1992-07-01: +1 second");
78 table.add_entry(741484800, 27, "1993-07-01: +1 second");
79 table.add_entry(773020800, 28, "1994-07-01: +1 second");
80 table.add_entry(820454400, 29, "1996-01-01: +1 second");
81 table.add_entry(867715200, 30, "1997-07-01: +1 second");
82 table.add_entry(915148800, 31, "1999-01-01: +1 second");
83 table.add_entry(1136073600, 32, "2006-01-01: +1 second");
84 table.add_entry(1230768000, 33, "2009-01-01: +1 second");
85 table.add_entry(1341100800, 34, "2012-07-01: +1 second");
86 table.add_entry(1435708800, 35, "2015-07-01: +1 second");
87 table.add_entry(1483228800, 36, "2017-01-01: +1 second (most recent)");
88
89 table
90 }
91
92 /// Every TAI-UTC offset then reads zero, so the two scales coincide.
93 pub fn disabled() -> Self {
94 Self {
95 entries: Vec::new(),
96 enabled: false,
97 }
98 }
99
100 pub fn add_entry(&mut self, utc_timestamp: i64, tai_utc_offset: i32, description: &str) {
101 self.entries.push(LeapSecondEntry {
102 utc_timestamp,
103 tai_utc_offset,
104 description: description.to_string(),
105 });
106
107 // Keep entries sorted by timestamp
108 self.entries.sort();
109 }
110
111 /// Seconds to add to UTC to reach TAI. Zero before 1972, or when handling is off.
112 pub fn tai_utc_offset_at(&self, utc_timestamp: i64) -> i32 {
113 if !self.enabled {
114 return 0;
115 }
116
117 // Before 1972, there were no leap seconds (TAI-UTC was 10 seconds)
118 if utc_timestamp < 78796800 { // 1972-07-01
119 return 10; // Initial TAI-UTC offset before leap seconds began
120 }
121
122 // Find the most recent leap second entry before or at this timestamp
123 match self.entries.binary_search_by_key(&utc_timestamp, |entry| entry.utc_timestamp) {
124 Ok(index) => self.entries[index].tai_utc_offset,
125 Err(index) => {
126 if index == 0 {
127 10 // Before first leap second
128 } else {
129 self.entries[index - 1].tai_utc_offset
130 }
131 }
132 }
133 }
134
135 pub fn utc_to_tai(&self, utc_timestamp: i64) -> i64 {
136 utc_timestamp + self.tai_utc_offset_at(utc_timestamp) as i64
137 }
138
139 /// Harder than the other direction: during a positive leap second, one UTC time
140 /// answers to two TAI times.
141 pub fn tai_to_utc(&self, tai_timestamp: i64) -> Outcome<i64> {
142 if !self.enabled {
143 return Ok(tai_timestamp);
144 }
145
146 // Approximate UTC time (ignoring leap seconds)
147 let approx_utc = tai_timestamp - 36; // Rough estimate using latest offset
148
149 // Find the correct offset by checking around the approximate time
150 let offset = self.tai_utc_offset_at(approx_utc);
151 let precise_utc = tai_timestamp - offset as i64;
152
153 // Verify our calculation is correct
154 let verification_tai = self.utc_to_tai(precise_utc);
155 if verification_tai == tai_timestamp {
156 Ok(precise_utc)
157 } else {
158 // Handle edge cases around leap second boundaries
159 self.handle_leap_second_boundary(tai_timestamp, precise_utc)
160 }
161 }
162
163 fn handle_leap_second_boundary(&self, tai_timestamp: i64, initial_utc: i64) -> Outcome<i64> {
164 // Check timestamps within ±2 seconds to handle leap second ambiguity
165 for offset in -2..=2 {
166 let test_utc = initial_utc + offset;
167 if self.utc_to_tai(test_utc) == tai_timestamp {
168 return Ok(test_utc);
169 }
170 }
171
172 Err(err!("Cannot convert TAI timestamp {} to UTC: ambiguous leap second boundary", tai_timestamp; Invalid, Input))
173 }
174
175 pub fn is_enabled(&self) -> bool {
176 self.enabled
177 }
178
179 pub fn set_enabled(&mut self, enabled: bool) {
180 self.enabled = enabled;
181 }
182
183 pub fn leap_second_count(&self) -> usize {
184 self.entries.len()
185 }
186
187 pub fn latest_leap_second(&self) -> Option<&LeapSecondEntry> {
188 self.entries.last()
189 }
190
191 /// In chronological order.
192 pub fn all_leap_seconds(&self) -> &[LeapSecondEntry] {
193 &self.entries
194 }
195
196 pub fn is_leap_second(&self, utc_timestamp: i64) -> bool {
197 if !self.enabled {
198 return false;
199 }
200
201 self.entries.iter().any(|entry| entry.utc_timestamp == utc_timestamp)
202 }
203
204 pub fn leap_second_at(&self, utc_timestamp: i64) -> Option<&LeapSecondEntry> {
205 if !self.enabled {
206 return None;
207 }
208
209 self.entries.iter().find(|entry| entry.utc_timestamp == utc_timestamp)
210 }
211
212 pub fn validate_leap_second(&self, year: i32, month: u8, day: u8, hour: u8, minute: u8) -> bool {
213 if !self.enabled || hour != 23 || minute != 59 {
214 return false; // Leap seconds only occur at 23:59:60 UTC
215 }
216
217 // Convert date to UTC timestamp using proper calendar arithmetic
218 let utc_timestamp = match self.date_to_utc_timestamp(year, month, day, hour, minute, 60) {
219 Ok(ts) => ts,
220 Err(_) => return false,
221 };
222
223 self.is_leap_second(utc_timestamp)
224 }
225
226 fn date_to_utc_timestamp(&self, year: i32, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Result<i64, &'static str> {
227 // Validate input parameters
228 if month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 60 {
229 return Err("Invalid date/time parameters");
230 }
231
232 // Calculate days since Unix epoch using proper calendar algorithm
233 let days_since_epoch = ok!(self.calculate_days_since_epoch(year, month, day));
234
235 // Convert to seconds and add time components
236 let timestamp = days_since_epoch as i64 * 86400 +
237 hour as i64 * 3600 +
238 minute as i64 * 60 +
239 second as i64;
240
241 Ok(timestamp)
242 }
243
244 fn calculate_days_since_epoch(&self, year: i32, month: u8, day: u8) -> Result<i32, &'static str> {
245 // Validate month and day ranges
246 if month < 1 || month > 12 || day < 1 {
247 return Err("Invalid month or day");
248 }
249
250 // Check day validity for the given month/year
251 let days_in_month = self.days_in_month(year, month);
252 if day > days_in_month {
253 return Err("Day out of range for month");
254 }
255
256 // Use Julian day number algorithm for accurate calculation
257 // Convert to Julian day number first
258 let jdn = self.gregorian_to_jdn(year, month, day);
259
260 // Unix epoch is January 1, 1970 = JDN 2440588
261 let unix_epoch_jdn = 2440588i64;
262 let days_since_epoch = jdn - unix_epoch_jdn;
263
264 // Check for reasonable range (avoid overflow)
265 if days_since_epoch < i32::MIN as i64 || days_since_epoch > i32::MAX as i64 {
266 return Err("Date too far from Unix epoch");
267 }
268
269 Ok(days_since_epoch as i32)
270 }
271
272 fn gregorian_to_jdn(&self, year: i32, month: u8, day: u8) -> i64 {
273 let (y, m) = if month <= 2 {
274 (year - 1, month as i32 + 12)
275 } else {
276 (year, month as i32)
277 };
278
279 let a = y / 100;
280 let b = 2 - a + a / 4;
281
282 let jdn = (365.25 * (y + 4716) as f64) as i64 +
283 (30.6001 * (m + 1) as f64) as i64 +
284 day as i64 + b as i64 - 1524;
285
286 jdn
287 }
288
289 fn days_in_month(&self, year: i32, month: u8) -> u8 {
290 match month {
291 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
292 4 | 6 | 9 | 11 => 30,
293 2 => if self.is_leap_year(year) { 29 } else { 28 },
294 _ => 0, // Invalid month
295 }
296 }
297
298 fn is_leap_year(&self, year: i32) -> bool {
299 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
300 }
301
302 pub fn statistics(&self) -> LeapSecondStatistics {
303 LeapSecondStatistics {
304 total_leap_seconds: self.entries.len(),
305 enabled: self.enabled,
306 first_leap_second: self.entries.first().map(|e| e.utc_timestamp),
307 latest_leap_second: self.entries.last().map(|e| e.utc_timestamp),
308 current_tai_utc_offset: self.entries.last().map(|e| e.tai_utc_offset).unwrap_or(10),
309 }
310 }
311}
312
313impl Default for LeapSecondTable {
314 fn default() -> Self {
315 Self::standard()
316 }
317}
318
319#[derive(Clone, Debug)]
320pub struct LeapSecondStatistics {
321 pub total_leap_seconds: usize,
322 pub enabled: bool,
323 pub first_leap_second: Option<i64>,
324 pub latest_leap_second: Option<i64>,
325 pub current_tai_utc_offset: i32, // seconds
326}
327
328#[derive(Clone, Debug)]
329pub struct LeapSecondConfig {
330 pub enabled: bool,
331 pub allow_leap_second_parsing: bool,
332 pub validate_leap_seconds: bool,
333 pub custom_table: Option<LeapSecondTable>, // None uses the standard table
334}
335
336impl Default for LeapSecondConfig {
337 fn default() -> Self {
338 Self {
339 enabled: true,
340 allow_leap_second_parsing: true,
341 validate_leap_seconds: true,
342 custom_table: None,
343 }
344 }
345}
346
347impl LeapSecondConfig {
348 pub fn disabled() -> Self {
349 Self {
350 enabled: false,
351 allow_leap_second_parsing: false,
352 validate_leap_seconds: false,
353 custom_table: None,
354 }
355 }
356
357 /// Second 60 parses, but is not checked against the table.
358 pub fn permissive() -> Self {
359 Self {
360 enabled: true,
361 allow_leap_second_parsing: true,
362 validate_leap_seconds: false,
363 custom_table: None,
364 }
365 }
366
367 pub fn get_table(&self) -> LeapSecondTable {
368 if !self.enabled {
369 LeapSecondTable::disabled()
370 } else if let Some(ref table) = self.custom_table {
371 table.clone()
372 } else {
373 LeapSecondTable::standard()
374 }
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_leap_second_table_creation() {
384 let table = LeapSecondTable::standard();
385 assert!(table.is_enabled());
386 assert!(table.leap_second_count() > 0);
387
388 let disabled = LeapSecondTable::disabled();
389 assert!(!disabled.is_enabled());
390 assert_eq!(disabled.leap_second_count(), 0);
391 }
392
393 #[test]
394 fn test_tai_utc_offset() {
395 let table = LeapSecondTable::standard();
396
397 // Before first leap second (1972-07-01)
398 assert_eq!(table.tai_utc_offset_at(0), 10);
399
400 // After first leap second
401 assert_eq!(table.tai_utc_offset_at(78796800), 10); // Exactly at first leap second
402 assert_eq!(table.tai_utc_offset_at(78796801), 10); // Just after first leap second
403
404 // After most recent leap second (2017-01-01)
405 assert_eq!(table.tai_utc_offset_at(1483228800), 36); // 2017-01-01
406 assert_eq!(table.tai_utc_offset_at(1600000000), 36); // 2020 (no leap seconds since 2017)
407 }
408
409 #[test]
410 fn test_utc_tai_conversion() -> Outcome<()> {
411 let table = LeapSecondTable::standard();
412
413 let utc_timestamp = 1483228800; // 2017-01-01 00:00:00 UTC
414 let tai_timestamp = table.utc_to_tai(utc_timestamp);
415 assert_eq!(tai_timestamp, utc_timestamp + 36);
416
417 // Round trip conversion
418 let converted_utc = res!(table.tai_to_utc(tai_timestamp));
419 assert_eq!(converted_utc, utc_timestamp);
420 Ok(())
421 }
422
423 #[test]
424 fn test_leap_second_detection() {
425 let table = LeapSecondTable::standard();
426
427 // 2017-01-01 leap second
428 assert!(table.is_leap_second(1483228800));
429
430 // Not a leap second
431 assert!(!table.is_leap_second(1483228801));
432
433 // The leap second the table records as "2017-01-01" was inserted at the
434 // end of the preceding day, as 2016-12-31T23:59:60Z, which is the instant
435 // 1483228800 the entry holds. The date carrying the sixtieth second is
436 // therefore 2016-12-31, not 2017-01-01.
437 assert!(table.validate_leap_second(2016, 12, 31, 23, 59));
438 assert!(!table.validate_leap_second(2017, 1, 1, 23, 59)); // a day too late
439 assert!(!table.validate_leap_second(2017, 1, 2, 23, 59)); // not a leap second date
440 assert!(!table.validate_leap_second(2016, 12, 31, 12, 0)); // wrong time
441 }
442
443 #[test]
444 fn test_leap_second_config() {
445 let config = LeapSecondConfig::default();
446 assert!(config.enabled);
447 assert!(config.allow_leap_second_parsing);
448
449 let disabled = LeapSecondConfig::disabled();
450 assert!(!disabled.enabled);
451 assert!(!disabled.allow_leap_second_parsing);
452
453 let table = disabled.get_table();
454 assert!(!table.is_enabled());
455 }
456
457 #[test]
458 fn test_leap_second_statistics() {
459 let table = LeapSecondTable::standard();
460 let stats = table.statistics();
461
462 assert!(stats.total_leap_seconds > 0);
463 assert!(stats.enabled);
464 assert!(stats.first_leap_second.is_some());
465 assert!(stats.latest_leap_second.is_some());
466 assert_eq!(stats.current_tai_utc_offset, 36); // As of 2017
467 }
468
469 #[test]
470 fn test_leap_second_boundary_handling() -> Outcome<()> {
471 let table = LeapSecondTable::standard();
472
473 // Test conversion around leap second boundaries
474 let leap_second_utc = 1483228800; // 2017-01-01 leap second
475 let tai_at_leap = table.utc_to_tai(leap_second_utc);
476
477 // Converting back should work
478 let converted_back = res!(table.tai_to_utc(tai_at_leap));
479 assert_eq!(converted_back, leap_second_utc);
480 Ok(())
481 }
482}