leap_seconds/
lib.rs

1//! This crate provides a means of accessing current leap second data.
2//!
3//! This is achieved through a parser that can read and provide access to the data in a
4//! `leap-seconds.list` file. A copy of this file can be obtained from various sources. To name a
5//! few:
6//!  - IERS: <https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list>
7//!  - TZDB (from IANA): <https://data.iana.org/time-zones/tzdb/leap-seconds.list>
8//!  - TZDB (from GitHub): <https://raw.githubusercontent.com/eggert/tz/main/leap-seconds.list>
9//!  - Meinberg: <https://www.meinberg.de/download/ntp/leap-seconds.list>
10//!
11//! It is recommended that you get the file from the [IERS] as they are responsible for announcing
12//! leap seconds. Consequently, they will most likely provide the most up to date version of the
13//! file.
14//!
15//! You do not need to have any knowledge of the structure or contents of the `leap-seconds.list`
16//! file in order to use this crate. However, if you want to know more about `leap-seconds.list`,
17//! here's an article from Meinberg on the matter:
18//!
19//! <https://kb.meinbergglobal.com/kb/time_sync/ntp/configuration/ntp_leap_second_file> (last
20//! accessed 2022-11-26)
21//!
22//! # Quickstart
23//!
24//! [reqwest] is used in this example, but any other HTTP library or a local file will work just as
25//! well.
26//!
27//! ```
28//! use leap_seconds::LeapSecondsList;
29//! use std::io::BufReader;
30//!
31//! // ======= fetching & parsing the file ======= //
32//!
33//! // get the file from the IERS
34//! let file = reqwest::blocking::get("https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list")
35//!         .unwrap();
36//! // parse the file
37//! let leap_seconds_list = LeapSecondsList::new(BufReader::new(file)).unwrap();
38//!
39//! // make sure the file is up to date
40//! // you should always do this unless you don't mind working with outdated data
41//! assert!(!leap_seconds_list.is_expired());
42//!
43//! // ======= some things that are possible ======= //
44//!
45//! // get the next leap second that will be introduced
46//! let next_leap_second = leap_seconds_list.next_leap_second();
47//!
48//! // get an ordered slice of all future leap seconds currently announced
49//! let future_leap_seconds = leap_seconds_list.planned_leap_seconds();
50//!
51//! // get an ordered slice of all leap seconds that have been introduced since 1970
52//! let all_leap_seconds = leap_seconds_list.leap_seconds();
53//!
54//! // get the last time the `leap-seconds.list` file was updated
55//! let last_update = leap_seconds_list.last_update();
56//! ```
57//!
58//! [IERS]: https://www.iers.org
59//! [reqwest]: https://crates.io/crates/reqwest
60
61#![deny(clippy::all)]
62#![warn(clippy::pedantic)]
63#![warn(clippy::cargo)]
64#![warn(missing_docs)]
65
66use {
67    core::fmt::{self, Display},
68    sha1::{Digest, Sha1},
69    std::{io::BufRead, time::SystemTime},
70};
71
72pub use errors::*;
73
74pub mod errors;
75
76const SECONDS_PER_MINUTE: u8 = 60;
77const MINUTES_PER_HOUR: u8 = 60;
78const HOURS_PER_DAY: u8 = 24;
79
80const SECONDS_PER_HOUR: u64 = (MINUTES_PER_HOUR as u64) * (SECONDS_PER_MINUTE as u64);
81const SECONDS_PER_DAY: u64 = (HOURS_PER_DAY as u64) * SECONDS_PER_HOUR;
82
83const JANUARY: u8 = 1;
84const FEBRUARY: u8 = 2;
85const MARCH: u8 = 3;
86const APRIL: u8 = 4;
87const MAY: u8 = 5;
88const JUNE: u8 = 6;
89const JULY: u8 = 7;
90const AUGUST: u8 = 8;
91const SEPTEMBER: u8 = 9;
92const OCTOBER: u8 = 10;
93const NOVEMBER: u8 = 11;
94const DECEMBER: u8 = 12;
95
96const DAYS_PER_ERA: u64 = 365 * 400 + 100 - 4 + 1;
97const DAYS_BETWEEN_1900_01_01_AND_0000_03_01: u64 =
98    1900 * 365 + (1900 / 400) - (1900 / 100) + (1900 / 4) - 31 - 28;
99
100#[allow(clippy::identity_op)]
101const DAYS_BETWEEN_1900_01_01_AND_1970_01_01: u64 = 70 * 365 + (70 / 400) - (70 / 100) + (70 / 4);
102const SECONDS_BETWEEN_1900_01_01_AND_1970_01_01: u64 =
103    DAYS_BETWEEN_1900_01_01_AND_1970_01_01 * SECONDS_PER_DAY;
104
105/// A date.
106///
107/// Is limited to years `>= 0`.
108#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
109pub struct Date {
110    year: u64,
111    month: u8,
112    day: u8,
113}
114
115impl Date {
116    /// Creates a new [`Date`].
117    ///
118    /// ```
119    /// # use leap_seconds::{Date, InvalidDate};
120    /// #
121    /// let _ = Date::new(2003, 03, 30)?;
122    /// #
123    /// # Ok::<(), InvalidDate>(())
124    /// ```
125    ///
126    /// # Errors
127    ///
128    /// Fails if the given date is invalid.
129    ///
130    /// ```
131    /// # use leap_seconds::Date;
132    /// #
133    /// // The year 7_777_777 is not a leap year
134    /// let error = Date::new(7_777_777, 2, 29);
135    /// assert!(error.is_err());
136    /// ```
137    pub const fn new(year: u64, month: u8, day: u8) -> Result<Self, InvalidDate> {
138        if month > 12 || month < 1 {
139            Err(InvalidDate::MonthOutOfRange(month))
140        } else if day < 1 || day > days_in_month(month, year) {
141            Err(InvalidDate::DayOutOfRange(day))
142        } else {
143            Ok(Date { year, month, day })
144        }
145    }
146
147    /// Gets the day of this [`Date`].
148    ///
149    /// ```
150    /// # use leap_seconds::{Date, InvalidDate};
151    /// #
152    /// let date = Date::new(1977, 5, 25)?;
153    /// assert_eq!(date.day(), 25);
154    /// #
155    /// # Ok::<(), InvalidDate>(())
156    /// ```
157    #[must_use]
158    pub const fn day(self) -> u8 {
159        self.day
160    }
161
162    /// Gets the month of this [`Date`].
163    ///
164    /// ```
165    /// # use leap_seconds::{Date, InvalidDate};
166    /// #
167    /// let date = Date::new(1980, 5, 21)?;
168    /// assert_eq!(date.month(), 5);
169    /// #
170    /// # Ok::<(), InvalidDate>(())
171    /// ```
172    #[must_use]
173    pub const fn month(self) -> u8 {
174        self.month
175    }
176
177    /// Gets the year of this [`Date`].
178    ///
179    /// ```
180    /// # use leap_seconds::{Date, InvalidDate};
181    /// #
182    /// let date = Date::new(1983, 5, 25)?;
183    /// assert_eq!(date.year(), 1983);
184    /// #
185    /// # Ok::<(), InvalidDate>(())
186    /// ```
187    #[must_use]
188    pub const fn year(self) -> u64 {
189        self.year
190    }
191
192    const fn days_since_1900(self) -> u64 {
193        // Credits to Howard Hinnant for the algorithm:
194        // https://howardhinnant.github.io/date_algorithms.html (last accessed 2022-11-24)
195
196        let month = self.month as u64;
197        let day = self.day as u64;
198
199        let year = self.year - (month <= 2) as u64;
200        let era = year / 400;
201        let year_of_era = year - era * 400; // [0, 399]
202        let day_of_year = (153 * ((month + 9) % 12) + 2) / 5 + day - 1; // [0, 365]
203        let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year; // [0, 146096]
204
205        era * DAYS_PER_ERA + day_of_era - DAYS_BETWEEN_1900_01_01_AND_0000_03_01
206    }
207}
208
209/// A time.
210#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
211pub struct Time {
212    hours: u8,
213    minutes: u8,
214    seconds: u8,
215}
216
217impl Time {
218    /// Create a new [`Time`].
219    ///
220    /// ```
221    /// # use leap_seconds::{InvalidTime, Time};
222    /// #
223    /// // 15:03:42
224    /// let _ = Time::new(15, 3, 42)?;
225    /// #
226    /// # Ok::<(), InvalidTime>(())
227    /// ```
228    ///
229    /// # Errors
230    ///
231    /// Fails if the given time is invalid.
232    ///
233    /// ```
234    /// # use leap_seconds::Time;
235    /// #
236    /// let error = Time::new(23, 60, 15);
237    /// assert!(error.is_err());
238    /// ```
239    pub fn new(hours: u8, minutes: u8, seconds: u8) -> Result<Self, InvalidTime> {
240        if hours >= HOURS_PER_DAY {
241            Err(InvalidTime::HoursOutOfRange(hours))
242        } else if minutes >= MINUTES_PER_HOUR {
243            Err(InvalidTime::MinutesOutOfRange(minutes))
244        } else if seconds >= SECONDS_PER_MINUTE {
245            Err(InvalidTime::SecondsOutOfRange(seconds))
246        } else {
247            Ok(Time {
248                hours,
249                minutes,
250                seconds,
251            })
252        }
253    }
254
255    /// Gets the hours of this [`Time`].
256    ///
257    /// ```
258    /// # use leap_seconds::{InvalidTime, Time};
259    /// #
260    /// let time = Time::new(13, 17, 29)?;
261    /// assert_eq!(time.hours(), 13);
262    /// #
263    /// # Ok::<(), InvalidTime>(())
264    /// ```
265    #[must_use]
266    pub const fn hours(self) -> u8 {
267        self.hours
268    }
269
270    /// Gets the minutes of this [`Time`].
271    ///
272    /// ```
273    /// # use leap_seconds::{InvalidTime, Time};
274    /// #
275    /// let time = Time::new(13, 17, 29)?;
276    /// assert_eq!(time.minutes(), 17);
277    /// #
278    /// # Ok::<(), InvalidTime>(())
279    /// ```
280    #[must_use]
281    pub const fn minutes(self) -> u8 {
282        self.minutes
283    }
284
285    /// Gets the seconds of this [`Time`].
286    ///
287    /// ```
288    /// # use leap_seconds::{InvalidTime, Time};
289    /// #
290    /// let time = Time::new(13, 17, 29)?;
291    /// assert_eq!(time.seconds(), 29);
292    /// #
293    /// # Ok::<(), InvalidTime>(())
294    /// ```
295    #[must_use]
296    pub const fn seconds(self) -> u8 {
297        self.seconds
298    }
299
300    const fn total_seconds(self) -> u64 {
301        (self.hours as u64) * SECONDS_PER_HOUR
302            + (self.minutes as u64) * (SECONDS_PER_MINUTE as u64)
303            + (self.seconds as u64)
304    }
305}
306
307/// A [`Date`] and a [`Time`].
308///
309/// That's literally what it is, nothing more to see here.
310#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
311pub struct DateTime {
312    /// The [`Date`] of the [`DateTime`].
313    pub date: Date,
314    /// The [`Time`] of the [`DateTime`].
315    pub time: Time,
316}
317
318impl From<Timestamp> for DateTime {
319    fn from(timestamp: Timestamp) -> Self {
320        timestamp.date_time()
321    }
322}
323
324const fn is_leap_year(year: u64) -> bool {
325    (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0))
326}
327
328const fn days_in_month(month: u8, year: u64) -> u8 {
329    match month {
330        JANUARY | MARCH | MAY | JULY | AUGUST | OCTOBER | DECEMBER => 31,
331        APRIL | JUNE | SEPTEMBER | NOVEMBER => 30,
332        FEBRUARY => {
333            if is_leap_year(year) {
334                29
335            } else {
336                28
337            }
338        }
339        // This should be unreachable!, but unreachable! is not yet const, so this is the best I
340        // can do right now.
341        _ => panic!("invalid month"),
342    }
343}
344
345/// A date and time represented as seconds since 1900-01-01 00:00:00.
346///
347/// [`Timestamp`] is a simple wrapper around a [`u64`] with a couple of convenience functions.
348#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
349pub struct Timestamp {
350    value: u64,
351}
352
353impl Timestamp {
354    /// The maximum [`DateTime`] that can be represented as [`Timestamp`].
355    ///
356    /// ```
357    /// # use std::error::Error;
358    /// use leap_seconds::{Date, DateTime, Time, Timestamp};
359    ///
360    /// let alt_max_1 = DateTime {
361    ///     date: Date::new(584_554_051_153, 11, 9)?,
362    ///     time: Time::new(7, 0, 15)?,
363    /// };
364    ///
365    /// let alt_max_2 = Timestamp::from_u64(u64::MAX).date_time();
366    ///
367    /// assert_eq!(Timestamp::MAX_REPRESENTABLE_DATE_TIME, alt_max_1);
368    /// assert_eq!(Timestamp::MAX_REPRESENTABLE_DATE_TIME, alt_max_2);
369    /// #
370    /// # Ok::<(), Box<dyn Error>>(())
371    /// ```
372    pub const MAX_REPRESENTABLE_DATE_TIME: DateTime = Self::from_u64(u64::MAX).date_time();
373
374    /// The minimum [`DateTime`] that can be represented as [`Timestamp`].
375    ///
376    /// ```
377    /// # use std::error::Error;
378    /// use leap_seconds::{Date, DateTime, Time, Timestamp};
379    ///
380    /// let alt_min_1 = DateTime {
381    ///     date: Date::new(1900, 1, 1)?,
382    ///     time: Time::new(0, 0, 0)?,
383    /// };
384    ///
385    /// let alt_min_2 = Timestamp::from_u64(0).date_time();
386    ///
387    /// assert_eq!(Timestamp::MIN_REPRESENTABLE_DATE_TIME, alt_min_1);
388    /// assert_eq!(Timestamp::MIN_REPRESENTABLE_DATE_TIME, alt_min_2);
389    /// #
390    /// # Ok::<(), Box<dyn Error>>(())
391    /// ```
392    pub const MIN_REPRESENTABLE_DATE_TIME: DateTime = Self::from_u64(0).date_time();
393
394    /// Creates a new [`Timestamp`] from a [`DateTime`].
395    ///
396    /// # Errors
397    ///
398    /// Fails if the [`DateTime`] is not representable as a [`Timestamp`].
399    ///
400    /// ```
401    /// use leap_seconds::{Date, DateTime, Time, Timestamp};
402    ///
403    /// let error = Timestamp::from_date_time(DateTime {
404    ///     date: Date::new(1899, 1, 1).expect("valid date"),
405    ///     time: Time::new(12, 0, 0).expect("valid time"),
406    /// });
407    ///
408    /// assert!(error.is_err());
409    /// ```
410    pub fn from_date_time(date_time: DateTime) -> Result<Self, DateTimeNotRepresentable> {
411        if (date_time >= Self::MIN_REPRESENTABLE_DATE_TIME)
412            && (date_time <= Self::MAX_REPRESENTABLE_DATE_TIME)
413        {
414            Ok(Timestamp::from_u64(
415                date_time.date.days_since_1900() * SECONDS_PER_DAY + date_time.time.total_seconds(),
416            ))
417        } else {
418            Err(DateTimeNotRepresentable { date_time })
419        }
420    }
421
422    /// Creates a new [`Timestamp`] from a [`u64`].
423    #[must_use]
424    pub const fn from_u64(value: u64) -> Self {
425        Self { value }
426    }
427
428    /// Returns the current time.
429    #[must_use]
430    pub fn now() -> Self {
431        let secs_since_unix_epoch = SystemTime::now()
432            .duration_since(SystemTime::UNIX_EPOCH)
433            .expect("now is later than the unix epoch")
434            .as_secs();
435        let secs_since_1900_01_01 =
436            secs_since_unix_epoch + SECONDS_BETWEEN_1900_01_01_AND_1970_01_01;
437
438        Timestamp::from_u64(secs_since_1900_01_01)
439    }
440
441    /// Gets the integer representation of this [`Timestamp`].
442    #[must_use]
443    pub const fn as_u64(self) -> u64 {
444        self.value
445    }
446
447    /// Gets the date and time of this [`Timestamp`].
448    #[must_use]
449    pub const fn date_time(self) -> DateTime {
450        DateTime {
451            date: self.date(),
452            time: self.time(),
453        }
454    }
455
456    /// Gets the time of this [`Timestamp`].
457    #[must_use]
458    pub const fn time(self) -> Time {
459        Time {
460            hours: self.hours(),
461            minutes: self.minutes(),
462            seconds: self.seconds(),
463        }
464    }
465
466    /// Gets the date of this [`Timestamp`].
467    #[must_use]
468    pub const fn date(self) -> Date {
469        // Credits to Howard Hinnant for the algorithm:
470        // https://howardhinnant.github.io/date_algorithms.html (last accessed 2022-11-24)
471
472        let days_since_1900_01_01 = self.total_days();
473        let days_since_0000_03_01 = days_since_1900_01_01 + DAYS_BETWEEN_1900_01_01_AND_0000_03_01;
474        let era = days_since_0000_03_01 / DAYS_PER_ERA;
475        let day_of_era = days_since_0000_03_01 % DAYS_PER_ERA; // [0, 146096]
476        let year_of_era =
477            (day_of_era - day_of_era / 1460 + day_of_era / 36524 - day_of_era / 146_096) / 365; // [0, 399]
478        let year = year_of_era + era * 400;
479        let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100); // [0, 365]
480
481        let mp = (5 * day_of_year + 2) / 153; // [0, 11]
482        #[allow(clippy::cast_possible_truncation)]
483        let day = (day_of_year - (153 * mp + 2) / 5 + 1) as u8; // [1, 31]
484        let month = ((mp + 2) % 12) as u8 + 1; // [1, 12]
485        let year = year + (month <= 2) as u64;
486
487        Date { year, month, day }
488    }
489
490    const fn hours(self) -> u8 {
491        #[allow(clippy::cast_possible_truncation)]
492        {
493            (self.total_hours() % (HOURS_PER_DAY as u64)) as u8
494        }
495    }
496
497    const fn minutes(self) -> u8 {
498        #[allow(clippy::cast_possible_truncation)]
499        {
500            (self.total_minutes() % (MINUTES_PER_HOUR as u64)) as u8
501        }
502    }
503
504    const fn seconds(self) -> u8 {
505        #[allow(clippy::cast_possible_truncation)]
506        {
507            (self.total_seconds() % (SECONDS_PER_MINUTE as u64)) as u8
508        }
509    }
510
511    const fn total_seconds(self) -> u64 {
512        self.value
513    }
514
515    const fn total_minutes(self) -> u64 {
516        self.value / (SECONDS_PER_MINUTE as u64)
517    }
518
519    const fn total_hours(self) -> u64 {
520        self.value / SECONDS_PER_HOUR
521    }
522
523    const fn total_days(self) -> u64 {
524        self.value / SECONDS_PER_DAY
525    }
526}
527
528impl Display for Timestamp {
529    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
530        write!(f, "{}", self.value)
531    }
532}
533
534impl From<u64> for Timestamp {
535    fn from(timestamp: u64) -> Timestamp {
536        Self::from_u64(timestamp)
537    }
538}
539
540impl From<Timestamp> for u64 {
541    fn from(timestamp: Timestamp) -> u64 {
542        timestamp.as_u64()
543    }
544}
545
546impl TryFrom<DateTime> for Timestamp {
547    type Error = DateTimeNotRepresentable;
548    fn try_from(date_time: DateTime) -> Result<Self, Self::Error> {
549        Self::from_date_time(date_time)
550    }
551}
552
553/// A leap second as read from a `leap-seconds.list` file.
554///
555/// Consists of...
556///  - ... a [`Timestamp`] of the date the leap second is introduced. The leap second is added to
557///    (or removed from) the end of the previous day.
558///
559///    Here is some more information on leap seconds: <https://en.wikipedia.org/wiki/leap_second>.
560///
561///  - ... a [`u16`] indicating the updated time difference (in seconds) between
562///    [UTC] and [TAI].
563///
564/// [UTC]: https://en.wikipedia.org/wiki/Coordinated_Universal_Time
565/// [TAI]: https://en.wikipedia.org/wiki/International_Atomic_Time
566#[derive(Clone, Copy, Debug, Eq, PartialEq)]
567pub struct LeapSecond {
568    timestamp: Timestamp,
569    tai_diff: u16,
570}
571
572impl LeapSecond {
573    /// Gets the [`Timestamp`] of the date this leap second was introduced.
574    #[must_use]
575    pub const fn timestamp(self) -> Timestamp {
576        self.timestamp
577    }
578
579    /// Gets the difference between [UTC] and [TAI] as of the introduction of this leap second.
580    ///
581    /// [UTC]: https://en.wikipedia.org/wiki/Coordinated_Universal_Time
582    /// [TAI]: https://en.wikipedia.org/wiki/International_Atomic_Time
583    #[must_use]
584    pub const fn tai_diff(self) -> u16 {
585        self.tai_diff
586    }
587}
588
589#[derive(Clone, Debug)]
590struct Line {
591    content: String,
592    number: usize,
593}
594
595impl Line {
596    fn kind(&self) -> LineType {
597        if self.content.starts_with('#') {
598            match self.content[1..].chars().next() {
599                Some('$') => LineType::LastUpdate,
600                Some('@') => LineType::ExpirationDate,
601                Some('h') => LineType::Hash,
602                _ => LineType::Comment,
603            }
604        } else {
605            LineType::LeapSecond
606        }
607    }
608}
609
610#[derive(Clone, Copy, Debug, Eq, PartialEq)]
611enum LineType {
612    Comment,
613    LastUpdate,
614    ExpirationDate,
615    LeapSecond,
616    Hash,
617}
618
619#[derive(Clone, Copy, Debug)]
620struct LineBorrow<'a> {
621    content: &'a str,
622    number: usize,
623}
624
625fn extract_content(line: &Line) -> LineBorrow<'_> {
626    LineBorrow {
627        content: line.content[2..].trim(),
628        number: line.number,
629    }
630}
631
632fn parse_timestamp(timestamp: LineBorrow<'_>) -> Result<Timestamp, ParseLineError> {
633    let timestamp = timestamp
634        .content
635        .parse::<u64>()
636        .map_err(|_| ParseLineError {
637            cause: ParseLineErrorKind::InvalidTimestamp,
638            line: timestamp.content.to_owned(),
639            line_number: timestamp.number,
640        })?;
641
642    Ok(Timestamp::from_u64(timestamp))
643}
644
645/// A SHA-1 hash.
646#[derive(Clone, Debug, Eq, PartialEq)]
647pub struct Sha1Hash {
648    bytes: [u8; 20],
649}
650
651impl Sha1Hash {
652    const fn from_bytes(array: [u8; 20]) -> Self {
653        Self { bytes: array }
654    }
655
656    /// Converts a [`Sha1Hash`] to a byte array.
657    #[must_use]
658    pub const fn as_bytes(&self) -> &[u8; 20] {
659        &self.bytes
660    }
661
662    /// Converts a [`Sha1Hash`] to a byte array.
663    #[must_use]
664    pub const fn into_bytes(self) -> [u8; 20] {
665        self.bytes
666    }
667}
668
669impl From<Sha1Hash> for [u8; 20] {
670    fn from(hash: Sha1Hash) -> Self {
671        hash.into_bytes()
672    }
673}
674
675impl Display for Sha1Hash {
676    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
677        let to_string = self
678            .bytes
679            .iter()
680            .map(|byte| format!("{byte:0>2x}"))
681            .collect::<String>();
682        write!(f, "{to_string}")
683    }
684}
685
686fn parse_hash(hash: LineBorrow) -> Result<Sha1Hash, ParseLineError> {
687    let hash_vec = hash
688        .content
689        .split_ascii_whitespace()
690        .map(|word| {
691            u32::from_str_radix(word, 16).map_err(|_| ParseLineError {
692                cause: ParseLineErrorKind::InvalidHash,
693                line: hash.content.to_owned(),
694                line_number: hash.number,
695            })
696        })
697        .collect::<Result<Vec<_>, _>>()?
698        .into_iter()
699        .flat_map(u32::to_be_bytes)
700        .collect::<Vec<_>>();
701
702    let hash = TryInto::<[u8; 20]>::try_into(hash_vec).map_err(|_| ParseLineError {
703        cause: ParseLineErrorKind::InvalidHash,
704        line: hash.content.to_owned(),
705        line_number: hash.number,
706    })?;
707
708    Ok(Sha1Hash::from_bytes(hash))
709}
710
711fn parse_leap_second_lines(
712    lines: &[Line],
713) -> Result<Vec<(LineBorrow<'_>, LineBorrow<'_>)>, ParseLineError> {
714    lines
715        .iter()
716        .map(|line| {
717            let mut leap_second = line.content.as_str();
718            if let Some(start_of_comment) = leap_second.find('#') {
719                leap_second = &leap_second[..start_of_comment];
720            }
721            let leap_second = leap_second.trim();
722
723            let leap_second = leap_second
724                .split(|c: char| c.is_ascii_whitespace())
725                .filter(|s| !s.is_empty())
726                .collect::<Vec<_>>();
727
728            if leap_second.len() == 2 {
729                Ok((
730                    LineBorrow {
731                        content: leap_second[0],
732                        number: line.number,
733                    },
734                    LineBorrow {
735                        content: leap_second[1],
736                        number: line.number,
737                    },
738                ))
739            } else {
740                Err(ParseLineError {
741                    cause: ParseLineErrorKind::InvalidLeapSecondLine,
742                    line: line.content.clone(),
743                    line_number: line.number,
744                })
745            }
746        })
747        .collect::<Result<Vec<_>, _>>()
748}
749
750fn calculate_hash<'a>(
751    last_update: LineBorrow<'a>,
752    expiration_date: LineBorrow<'a>,
753    leap_seconds: &'a [(LineBorrow<'a>, LineBorrow<'a>)],
754) -> Sha1Hash {
755    let mut hasher = Sha1::new();
756
757    hasher.update(last_update.content.as_bytes());
758    hasher.update(expiration_date.content.as_bytes());
759
760    for chunk in leap_seconds.iter().flat_map(|(s1, s2)| [s1, s2]) {
761        hasher.update(chunk.content.as_bytes());
762    }
763
764    Sha1Hash::from_bytes(hasher.finalize().into())
765}
766
767fn parse_tai_diff(tai_diff: LineBorrow<'_>) -> Result<u16, ParseLineError> {
768    tai_diff.content.parse::<u16>().map_err(|_| ParseLineError {
769        cause: ParseLineErrorKind::InvalidTaiDiff,
770        line: tai_diff.content.to_owned(),
771        line_number: tai_diff.number,
772    })
773}
774
775fn parse_leap_seconds<'a>(
776    leap_second_lines: &[(LineBorrow<'a>, LineBorrow<'a>)],
777) -> Result<Vec<LeapSecond>, ParseLineError> {
778    let mut leap_seconds = leap_second_lines
779        .iter()
780        .map(|(timestamp, tai_diff)| {
781            Ok(LeapSecond {
782                timestamp: parse_timestamp(*timestamp)?,
783                tai_diff: parse_tai_diff(*tai_diff)?,
784            })
785        })
786        .collect::<Result<Vec<_>, _>>()?;
787
788    leap_seconds.sort_by(|t1, t2| t1.timestamp.cmp(&t2.timestamp));
789
790    Ok(leap_seconds)
791}
792
793fn set_option(
794    option: &Option<Line>,
795    to: Line,
796    data_component: DataComponent,
797) -> Result<Line, ParseFileError> {
798    if let Some(line) = option {
799        Err(ParseFileError::DuplicateData {
800            data_component,
801            line1: line.number,
802            line2: to.number,
803        })
804    } else {
805        Ok(to)
806    }
807}
808
809fn extract_content_lines<R: BufRead>(file: R) -> Result<ContentLines, ParseFileError> {
810    let mut last_update = None;
811    let mut expiration_date = None;
812    let mut leap_seconds = Vec::new();
813    let mut hash = None;
814
815    let lines = file
816        .lines()
817        .enumerate()
818        .map(|(number, line)| line.map(|content| Line { content, number }));
819
820    for line in lines {
821        let line = line?;
822        match line.kind() {
823            LineType::Comment => continue,
824            LineType::LeapSecond => leap_seconds.push(line),
825            LineType::LastUpdate => {
826                last_update = Some(set_option(&last_update, line, DataComponent::LastUpdate)?);
827            }
828            LineType::ExpirationDate => {
829                expiration_date = Some(set_option(
830                    &expiration_date,
831                    line,
832                    DataComponent::ExpirationDate,
833                )?);
834            }
835            LineType::Hash => {
836                hash = Some(set_option(&hash, line, DataComponent::Hash)?);
837            }
838        }
839    }
840
841    let last_update = last_update.ok_or(ParseFileError::MissingData(DataComponent::LastUpdate))?;
842    let expiration_date =
843        expiration_date.ok_or(ParseFileError::MissingData(DataComponent::ExpirationDate))?;
844    let hash = hash.ok_or(ParseFileError::MissingData(DataComponent::Hash))?;
845
846    Ok(ContentLines {
847        last_update,
848        expiration_date,
849        hash,
850        leap_seconds,
851    })
852}
853
854#[derive(Clone, Debug)]
855struct ContentLines {
856    last_update: Line,
857    expiration_date: Line,
858    hash: Line,
859    leap_seconds: Vec<Line>,
860}
861
862impl ContentLines {
863    fn last_update(&self) -> LineBorrow<'_> {
864        extract_content(&self.last_update)
865    }
866
867    fn expiration_date(&self) -> LineBorrow<'_> {
868        extract_content(&self.expiration_date)
869    }
870
871    fn hash(&self) -> LineBorrow<'_> {
872        extract_content(&self.hash)
873    }
874}
875
876/// Provides access to the data in `leap-seconds.list`.
877///
878/// **IMPORTANT:** If you don't want to use outdated data, check [`LeapSecondsList::is_expired()`] before using a
879/// [`LeapSecondsList`].
880///
881/// For examples see the [crate-level documentation](crate).
882#[derive(Clone, Debug, Eq, PartialEq)]
883pub struct LeapSecondsList {
884    last_update: Timestamp,
885    expiration_date: Timestamp,
886    leap_seconds: Vec<LeapSecond>,
887}
888
889impl LeapSecondsList {
890    /// Parse a `leap-seconds.list` file.
891    ///
892    /// # Errors
893    ///
894    /// If the given `leap-seconds.list` could not be parsed successfully.
895    ///
896    /// See [`ParseFileError`] for more information on what each error variant means.
897    pub fn new<R: BufRead>(file: R) -> Result<Self, ParseFileError> {
898        let content_lines = extract_content_lines(file)?;
899
900        let last_update = content_lines.last_update();
901        let expiration_date = content_lines.expiration_date();
902        let hash = content_lines.hash();
903
904        let leap_second_lines = parse_leap_second_lines(&content_lines.leap_seconds)?;
905
906        let calculated_hash = calculate_hash(last_update, expiration_date, &leap_second_lines);
907
908        let last_update = parse_timestamp(last_update)?;
909        let expiration_date = parse_timestamp(expiration_date)?;
910        let hash_from_file = parse_hash(hash)?;
911
912        let leap_seconds = parse_leap_seconds(&leap_second_lines)?;
913
914        if calculated_hash != hash_from_file {
915            return Err(ParseFileError::InvalidHash {
916                calculated: calculated_hash,
917                found: hash_from_file,
918            });
919        }
920
921        Ok(LeapSecondsList {
922            last_update,
923            expiration_date,
924            leap_seconds,
925        })
926    }
927
928    /// Returns whether this [`LeapSecondsList`] is expired.
929    #[must_use]
930    pub fn is_expired(&self) -> bool {
931        self.expiration_date() <= Timestamp::now()
932    }
933
934    /// Gets the last time the file was updated.
935    #[must_use]
936    pub const fn last_update(&self) -> Timestamp {
937        self.last_update
938    }
939
940    /// Gets the expiration date of the file.
941    #[must_use]
942    pub const fn expiration_date(&self) -> Timestamp {
943        self.expiration_date
944    }
945
946    /// Gets the leap second list from the file, ordered by introduction timestamp.
947    #[must_use]
948    pub fn leap_seconds(&self) -> &[LeapSecond] {
949        &self.leap_seconds
950    }
951
952    /// Gets the leap second list from the file, ordered by introduction timestamp.
953    #[must_use]
954    pub fn into_leap_seconds(self) -> Vec<LeapSecond> {
955        self.leap_seconds
956    }
957
958    /// Gets all the leap seconds introduced after a given [`Timestamp`].
959    #[must_use]
960    pub fn leap_seconds_after(&self, timestamp: Timestamp) -> &[LeapSecond] {
961        // this is possible because the self.leap_seconds is sorted by timestamp
962        let start_index = self
963            .leap_seconds()
964            .iter()
965            .enumerate()
966            .find(|(_, leap_second)| leap_second.timestamp() > timestamp)
967            .map_or_else(|| self.leap_seconds().len(), |(index, _)| index);
968
969        &self.leap_seconds()[start_index..]
970    }
971
972    /// Gets all the leap seconds that are planned in the future.
973    #[must_use]
974    pub fn planned_leap_seconds(&self) -> &[LeapSecond] {
975        self.leap_seconds_after(Timestamp::now())
976    }
977
978    /// Gets the next [`LeapSecond`] after a given [`Timestamp`].
979    ///
980    /// Returns [`None`] if the list doesn't contain any leap seconds after the [`Timestamp`].
981    #[must_use]
982    pub fn next_leap_second_after(&self, timestamp: Timestamp) -> Option<LeapSecond> {
983        // this is possible because the self.leap_seconds is sorted by timestamp
984        self.leap_seconds_after(timestamp).first().copied()
985    }
986
987    /// Gets the next [`LeapSecond`] that will be introduced.
988    ///
989    /// Returns [`None`] if a next leap second been announced.
990    ///
991    /// Equivalent to calling [`LeapSecondsList::next_leap_second_after()`] with
992    /// [`Timestamp::now()`].
993    #[must_use]
994    pub fn next_leap_second(&self) -> Option<LeapSecond> {
995        self.next_leap_second_after(Timestamp::now())
996    }
997}
998
999#[cfg(test)]
1000mod proptests;
1001
1002#[cfg(test)]
1003mod tests {
1004    mod crate_doc_file_sources {
1005        use {
1006            crate::{LeapSecond, LeapSecondsList, Timestamp},
1007            std::io::BufReader,
1008        };
1009
1010        #[test]
1011        fn iers() {
1012            test_source("https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list")
1013        }
1014
1015        #[test]
1016        fn tzdb_iana() {
1017            test_source("https://data.iana.org/time-zones/tzdb/leap-seconds.list")
1018        }
1019
1020        #[test]
1021        fn tzdb_github() {
1022            test_source("https://raw.githubusercontent.com/eggert/tz/main/leap-seconds.list")
1023        }
1024
1025        #[test]
1026        fn meinberg() {
1027            test_source("https://www.meinberg.de/download/ntp/leap-seconds.list")
1028        }
1029
1030        fn test_source(url: &str) {
1031            let file = reqwest::blocking::get(url).unwrap();
1032            let leap_seconds_list =
1033                LeapSecondsList::new(BufReader::new(file)).expect("parsing should be successful");
1034
1035            // file shouldn't be expired
1036            assert!(!leap_seconds_list.is_expired());
1037
1038            // expiration date will always be >= the expiration date at the time of writing
1039            let min_expiration_date = Timestamp::from_u64(3896899200);
1040            assert!(leap_seconds_list.expiration_date() >= min_expiration_date);
1041
1042            // last update will always be >= the expiration date at the time of writing
1043            let min_last_update = Timestamp::from_u64(3676924800);
1044            assert!(leap_seconds_list.last_update() >= min_last_update);
1045
1046            // first leap second will always be the same
1047            let first_leap_second = &leap_seconds_list.leap_seconds()[0];
1048            let expected_timestamp = Timestamp::from_u64(2272060800);
1049            let expected_tai_diff = 10;
1050            assert_eq!(first_leap_second.timestamp(), expected_timestamp);
1051            assert_eq!(first_leap_second.tai_diff(), expected_tai_diff);
1052
1053            // next leap second
1054            let expected = Some(LeapSecond {
1055                timestamp: Timestamp::from_u64(2950473600),
1056                tai_diff: 28,
1057            });
1058            let actual = leap_seconds_list.next_leap_second_after(Timestamp::from_u64(2918937600));
1059            assert_eq!(actual, expected);
1060
1061            // there should be no leap seconds after the last one
1062            let last_leap_second = leap_seconds_list
1063                .leap_seconds()
1064                .last()
1065                .expect("list contains at least 1 element");
1066            assert!(leap_seconds_list
1067                .leap_seconds_after(last_leap_second.timestamp())
1068                .is_empty());
1069        }
1070    }
1071
1072    mod timestamp {
1073        use {
1074            crate::{Date, DateTime, Time, Timestamp},
1075            chrono::{offset::Utc, Datelike, Timelike},
1076        };
1077
1078        #[test]
1079        fn now() {
1080            let expected = {
1081                let now = Utc::now();
1082                let date = now.date_naive();
1083                let time = now.time();
1084
1085                DateTime {
1086                    date: Date::new(date.year() as u64, date.month() as u8, date.day() as u8)
1087                        .expect("chrono produces valid dates"),
1088                    time: Time::new(time.hour() as u8, time.minute() as u8, time.second() as u8)
1089                        .expect("chrono produces valid times"),
1090                }
1091            };
1092            let actual = Timestamp::now().date_time();
1093
1094            assert_eq!(actual, expected);
1095        }
1096
1097        #[test]
1098        fn from_and_to_date_time_0() {
1099            let timestamp = Timestamp::from_u64(0);
1100            let date_time = timestamp.date_time();
1101            let expected_date_time = DateTime {
1102                date: Date::new(1900, 1, 1).unwrap(),
1103                time: Time::new(0, 0, 0).unwrap(),
1104            };
1105            assert_eq!(date_time, expected_date_time);
1106
1107            let timestamp_again =
1108                Timestamp::from_date_time(date_time).expect("should always be valid");
1109            assert_eq!(timestamp_again, timestamp);
1110
1111            let date_time_again = timestamp_again.date_time();
1112            assert_eq!(date_time_again, date_time);
1113        }
1114
1115        #[test]
1116        fn from_and_to_date_time_1889385054048000() {
1117            let timestamp = Timestamp::from_u64(1889385054048000);
1118            let date_time = timestamp.date_time();
1119
1120            let timestamp_again =
1121                Timestamp::from_date_time(date_time).expect("should always be valid");
1122            assert_eq!(timestamp_again, timestamp);
1123
1124            let date_time_again = timestamp_again.date_time();
1125            assert_eq!(date_time_again, date_time);
1126        }
1127
1128        #[test]
1129        fn from_and_to_date_time_2004317826065173() {
1130            let timestamp = Timestamp::from_u64(2004317826065173);
1131            let date_time = timestamp.date_time();
1132
1133            let timestamp_again =
1134                Timestamp::from_date_time(date_time).expect("should always be valid");
1135            assert_eq!(timestamp_again, timestamp);
1136
1137            let date_time_again = timestamp_again.date_time();
1138            assert_eq!(date_time_again, date_time);
1139        }
1140
1141        #[test]
1142        fn from_pre_1900_date_time() {
1143            let date_time = DateTime {
1144                date: Date::new(1899, 12, 31).unwrap(),
1145                time: Time::new(23, 59, 59).unwrap(),
1146            };
1147
1148            let error = Timestamp::from_date_time(date_time);
1149
1150            assert!(error.is_err());
1151        }
1152
1153        #[test]
1154        fn from_and_as_u64() {
1155            let original = 123456780987654;
1156            let result = Timestamp::from_u64(original).as_u64();
1157
1158            assert_eq!(result, original);
1159        }
1160
1161        #[test]
1162        fn test_1900_01_01() {
1163            let expected = Timestamp::from_date_time(DateTime {
1164                date: Date::new(1900, 1, 1).unwrap(),
1165                time: Time::new(0, 0, 0).unwrap(),
1166            })
1167            .unwrap();
1168            let actual = Timestamp::from_u64(0);
1169
1170            assert_eq!(actual, expected);
1171        }
1172
1173        #[test]
1174        fn test_1901_01_07_19_45_33() {
1175            let year = 1 * 365 * 24 * 60 * 60;
1176            let day = 6 * 24 * 60 * 60;
1177            let hours = 19 * 60 * 60;
1178            let minutes = 45 * 60;
1179            let seconds = 33;
1180
1181            let expected = Timestamp::from_date_time(DateTime {
1182                date: Date::new(1901, 1, 7).unwrap(),
1183                time: Time::new(19, 45, 33).unwrap(),
1184            })
1185            .unwrap();
1186            let actual = Timestamp::from_u64(year + day + hours + minutes + seconds);
1187
1188            assert_eq!(actual, expected);
1189        }
1190
1191        #[test]
1192        fn test_1904_02_29_23_59_59() {
1193            let year = 4 * 365 * 24 * 60 * 60;
1194            let month = 31 * 24 * 60 * 60;
1195            let day = 28 * 24 * 60 * 60;
1196            let hours = 23 * 60 * 60;
1197            let minutes = 59 * 60;
1198            let seconds = 59;
1199
1200            let timestamp = Timestamp::from_u64(year + month + day + hours + minutes + seconds);
1201            let expected = Timestamp::from_date_time(DateTime {
1202                date: Date::new(1904, 2, 29).unwrap(),
1203                time: Time::new(23, 59, 59).unwrap(),
1204            })
1205            .unwrap();
1206
1207            assert_eq!(timestamp, expected);
1208
1209            let next_timestamp = Timestamp::from_u64(timestamp.as_u64() + 1);
1210            let next_expected = Timestamp::from_date_time(DateTime {
1211                date: Date::new(1904, 3, 1).unwrap(),
1212                time: Time::new(0, 0, 0).unwrap(),
1213            })
1214            .unwrap();
1215
1216            assert_eq!(next_timestamp, next_expected);
1217
1218            assert!(next_timestamp > timestamp);
1219        }
1220
1221        #[test]
1222        fn test_2023_06_28() {
1223            let expected = Timestamp::from_date_time(DateTime {
1224                date: Date::new(2023, 6, 28).unwrap(),
1225                time: Time::new(0, 0, 0).unwrap(),
1226            })
1227            .unwrap();
1228            let actual = Timestamp::from_u64(3896899200);
1229
1230            assert_eq!(actual, expected);
1231        }
1232
1233        #[test]
1234        fn test_1985_07_01() {
1235            let expected = Timestamp::from_date_time(DateTime {
1236                date: Date::new(1985, 7, 1).unwrap(),
1237                time: Time::new(0, 0, 0).unwrap(),
1238            })
1239            .unwrap();
1240            let actual = Timestamp::from_u64(2698012800);
1241
1242            assert_eq!(actual, expected);
1243        }
1244
1245        #[test]
1246        fn test_2017_01_01() {
1247            let expected = Timestamp::from_date_time(DateTime {
1248                date: Date::new(2017, 1, 1).unwrap(),
1249                time: Time::new(0, 0, 0).unwrap(),
1250            })
1251            .unwrap();
1252            let actual = Timestamp::from_u64(3692217600);
1253
1254            assert_eq!(actual, expected);
1255        }
1256    }
1257}