hifitime 4.3.0

Ultra-precise date and time handling in Rust for scientific applications with leap second support
Documentation
/*
* Hifitime
* Copyright (C) 2017-onward Christopher Rabotin <christopher.rabotin@gmail.com> et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Documentation: https://nyxspace.com/
*/

#[cfg(feature = "python")]
use pyo3::prelude::*;

#[cfg(feature = "lts")]
use crate::HifitimeError;

#[cfg(feature = "std")]
pub use super::leap_seconds_file::LeapSecondsFile;

use core::ops::Index;

pub trait LeapSecondProvider: DoubleEndedIterator<Item = LeapSecond> + Index<usize> {}

/// A structure representing a leap second
#[cfg_attr(kani, derive(kani::Arbitrary))]
#[repr(C)]
#[cfg_attr(feature = "python", pyclass)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct LeapSecond {
    /// Timestamp in TAI seconds for this leap second, e.g. `2_272_060_800.0` for the first IERS leap second.
    pub timestamp_tai_s: f64,
    /// ΔAT is the accumulated time offset after this leap second has past.
    pub delta_at: f64,
    /// Whether or not this leap second was announced by the IERS.
    pub announced_by_iers: bool,
}

impl LeapSecond {
    pub const fn new(timestamp_tai_s: f64, delta_at: f64, announced: bool) -> Self {
        Self {
            timestamp_tai_s,
            delta_at,
            announced_by_iers: announced,
        }
    }
}

const LATEST_LEAP_SECONDS: [LeapSecond; 42] = [
    LeapSecond::new(1_893_369_600.0, 1.417818, false), // SOFA: 01 Jan 1960
    LeapSecond::new(1_924_992_000.0, 1.422818, false), // SOFA: 01 Jan 1961
    LeapSecond::new(1_943_308_800.0, 1.372818, false), // SOFA: 01 Aug 1961
    LeapSecond::new(1_956_528_000.0, 1.845858, false), // SOFA: 01 Jan 1962
    LeapSecond::new(2_014_329_600.0, 1.945858, false), // SOFA: 01 Jan 1963
    LeapSecond::new(2_019_600_000.0, 3.24013, false),  // SOFA: 01 Jan 1964
    LeapSecond::new(2_027_462_400.0, 3.34013, false),  // SOFA: 01 Apr 1964
    LeapSecond::new(2_040_681_600.0, 3.44013, false),  // SOFA: 01 Sep 1964
    LeapSecond::new(2_051_222_400.0, 3.54013, false),  // SOFA: 01 Jan 1965
    LeapSecond::new(2_056_320_000.0, 3.64013, false),  // SOFA: 01 Mar 1965
    LeapSecond::new(2_066_860_800.0, 3.74013, false),  // SOFA: 01 Jul 1965
    LeapSecond::new(2_072_217_600.0, 3.84013, false),  // SOFA: 01 Sep 1965
    LeapSecond::new(2_082_758_400.0, 4.31317, false),  // SOFA: 01 Jan 1966
    LeapSecond::new(2_148_508_800.0, 4.21317, false),  // SOFA: 01 Feb 1968
    LeapSecond::new(2_272_060_800.0, 10.0, true),      // IERS: 01 Jan 1972
    LeapSecond::new(2_287_785_600.0, 11.0, true),      // IERS: 01 Jul 1972
    LeapSecond::new(2_303_683_200.0, 12.0, true),      // IERS: 01 Jan 1973
    LeapSecond::new(2_335_219_200.0, 13.0, true),      // IERS: 01 Jan 1974
    LeapSecond::new(2_366_755_200.0, 14.0, true),      // IERS: 01 Jan 1975
    LeapSecond::new(2_398_291_200.0, 15.0, true),      // IERS: 01 Jan 1976
    LeapSecond::new(2_429_913_600.0, 16.0, true),      // IERS: 01 Jan 1977
    LeapSecond::new(2_461_449_600.0, 17.0, true),      // IERS: 01 Jan 1978
    LeapSecond::new(2_492_985_600.0, 18.0, true),      // IERS: 01 Jan 1979
    LeapSecond::new(2_524_521_600.0, 19.0, true),      // IERS: 01 Jan 1980
    LeapSecond::new(2_571_782_400.0, 20.0, true),      // IERS: 01 Jul 1981
    LeapSecond::new(2_603_318_400.0, 21.0, true),      // IERS: 01 Jul 1982
    LeapSecond::new(2_634_854_400.0, 22.0, true),      // IERS: 01 Jul 1983
    LeapSecond::new(2_698_012_800.0, 23.0, true),      // IERS: 01 Jul 1985
    LeapSecond::new(2_776_982_400.0, 24.0, true),      // IERS: 01 Jan 1988
    LeapSecond::new(2_840_140_800.0, 25.0, true),      // IERS: 01 Jan 1990
    LeapSecond::new(2_871_676_800.0, 26.0, true),      // IERS: 01 Jan 1991
    LeapSecond::new(2_918_937_600.0, 27.0, true),      // IERS: 01 Jul 1992
    LeapSecond::new(2_950_473_600.0, 28.0, true),      // IERS: 01 Jul 1993
    LeapSecond::new(2_982_009_600.0, 29.0, true),      // IERS: 01 Jul 1994
    LeapSecond::new(3_029_443_200.0, 30.0, true),      // IERS: 01 Jan 1996
    LeapSecond::new(3_076_704_000.0, 31.0, true),      // IERS: 01 Jul 1997
    LeapSecond::new(3_124_137_600.0, 32.0, true),      // IERS: 01 Jan 1999
    LeapSecond::new(3_345_062_400.0, 33.0, true),      // IERS: 01 Jan 2006
    LeapSecond::new(3_439_756_800.0, 34.0, true),      // IERS: 01 Jan 2009
    LeapSecond::new(3_550_089_600.0, 35.0, true),      // IERS: 01 Jul 2012
    LeapSecond::new(3_644_697_600.0, 36.0, true),      // IERS: 01 Jul 2015
    LeapSecond::new(3_692_217_600.0, 37.0, true),      // IERS: 01 Jan 2017
];

/// List of leap seconds from <https://data.iana.org/time-zones/data/leap-seconds.list>.
/// This list corresponds the number of seconds in TAI to the UTC offset and to whether it was an announced leap second or not.
/// The unannoucned leap seconds come from dat.c in the SOFA library.
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(kani, derive(kani::Arbitrary))]
#[derive(Clone, Debug)]
pub struct LatestLeapSeconds {
    data: [LeapSecond; 42],
    iter_pos: usize,
}

#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl LatestLeapSeconds {
    #[new]
    pub fn __new__() -> Self {
        Self::default()
    }

    fn __repr__(&self) -> String {
        format!("{self:?} @ {self:p}")
    }
}

#[cfg(feature = "lts")]
#[cfg_attr(feature = "python", pymethods)]
impl LatestLeapSeconds {
    /// Downloads the latest leap second list from IANA, and returns whether the embedded leap seconds are still up to date
    ///
    /// ```
    /// use hifitime::leap_seconds::LatestLeapSeconds;
    ///
    /// assert!(LatestLeapSeconds::default().is_up_to_date().unwrap(), "Hifitime needs to update its leap seconds list!");
    /// ```
    ///
    /// :rtype: bool
    pub fn is_up_to_date(&self) -> Result<bool, HifitimeError> {
        let latest_iana = LeapSecondsFile::from_iana()?;

        let ls_diff = self
            .data
            .iter()
            .filter(|leap| leap.announced_by_iers) // Keep only the announced leap seconds
            .zip(&latest_iana.data)
            .filter_map(
                |(known, announced)| {
                    if known != announced {
                        Some(())
                    } else {
                        None
                    }
                },
            )
            .collect::<Vec<()>>();

        Ok(ls_diff.is_empty())
    }
}

impl Default for LatestLeapSeconds {
    fn default() -> Self {
        Self {
            data: LATEST_LEAP_SECONDS,
            iter_pos: 0,
        }
    }
}

impl Iterator for LatestLeapSeconds {
    type Item = LeapSecond;

    fn next(&mut self) -> Option<Self::Item> {
        self.iter_pos += 1;
        self.data.get(self.iter_pos - 1).copied()
    }
}

impl DoubleEndedIterator for LatestLeapSeconds {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.iter_pos == self.data.len() {
            None
        } else {
            self.iter_pos += 1;
            self.data.get(self.data.len() - self.iter_pos).copied()
        }
    }
}

impl Index<usize> for LatestLeapSeconds {
    type Output = LeapSecond;

    fn index(&self, index: usize) -> &Self::Output {
        self.data.index(index)
    }
}

impl LeapSecondProvider for LatestLeapSeconds {}

#[test]
fn leap_second_fetch() {
    let leap_seconds = LatestLeapSeconds::default();

    assert_eq!(
        leap_seconds[0],
        LeapSecond::new(1_893_369_600.0, 1.417818, false),
    );
    assert_eq!(
        leap_seconds[41],
        LeapSecond::new(3_692_217_600.0, 37.0, true)
    );
}