From 0fb870509821eb6f8158a9ee3cc02e6a0f951c86 Mon Sep 17 00:00:00 2001 From: Botahamec Date: Thu, 3 Mar 2022 20:18:06 -0500 Subject: Started with leap seconds --- src/datetime.rs | 20 ++++++++ src/lib.rs | 2 + src/tai.rs | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/utc.rs | 1 + 4 files changed, 167 insertions(+) create mode 100644 src/tai.rs create mode 100644 src/utc.rs (limited to 'src') diff --git a/src/datetime.rs b/src/datetime.rs index f856e74..df4e637 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -92,6 +92,26 @@ impl NaiveDateTime { pub const fn nanosecond(self) -> u32 { self.time.nanosecond() } + + #[must_use] + pub fn add_seconds(self, seconds: isize) -> Self { + let time = self.time.add_seconds(seconds); + + Self { + date: self.date, + time, + } + } + + #[must_use] + pub fn add_nanoseconds(self, nanoseconds: isize) -> Self { + let time = self.time.add_nanoseconds(nanoseconds); + + Self { + date: self.date, + time, + } + } } impl PartialOrd for NaiveDateTime { diff --git a/src/lib.rs b/src/lib.rs index ebc132d..d83185d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,12 @@ #![doc = include_str!("../README.md")] // TODO must uses +// TODO serde support mod date; mod datetime; mod month; +mod tai; mod time; pub mod timezone; mod weekday; diff --git a/src/tai.rs b/src/tai.rs new file mode 100644 index 0000000..147b51b --- /dev/null +++ b/src/tai.rs @@ -0,0 +1,144 @@ +use core::cmp::Ordering; +use core::fmt::Display; + +use parking_lot::{const_rwlock, RwLock}; +use thiserror::Error; + +use crate::{ + timezone::{Utc, UtcOffset}, + Date, DateTime, NaiveDateTime, Time, TimeZone, +}; + +static GLOBAL_LEAP_SECONDS: RwLock = const_rwlock(LeapSeconds::empty()); + +struct LeapSeconds(Vec>); + +impl LeapSeconds { + // TODO docs + + const fn empty() -> Self { + Self(Vec::new()) + } + + fn leap_seconds_before_inclusive(&self, date_time: DateTime) -> usize { + let mut seconds = 0; + for leap_second in &self.0 { + if leap_second > &date_time { + break; + } + seconds += 1; + } + + seconds + } + + fn add_leap_second(&mut self, day: Date) { + let utc_datetime = NaiveDateTime::new(day, Time::MIDNIGHT); + let exact_time = DateTime::from_utc(utc_datetime, Utc); + + let mut i = 0; + while i < self.0.len() { + match self.0[i].cmp(&exact_time) { + Ordering::Greater => break, // insert the new leap second here + Ordering::Equal => return, // it's already here, so don't add it again + Ordering::Less => i += 1, // check the next leap second + } + } + + self.0.insert(i, exact_time); + } +} + +pub fn add_leap_second(day: Date) { + let mut leap_seconds = GLOBAL_LEAP_SECONDS.write(); + leap_seconds.add_leap_second(day); +} + +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub struct Tai; + +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Error)] +#[error( + "TAI cannot represent leap seconds, so a leap second cannot be converted to TAI. Recieved: {}", + given_dt +)] +pub struct UnexpectedLeapSecond { + given_dt: NaiveDateTime, +} + +impl Display for Tai { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "TAI") + } +} + +impl TimeZone for Tai { + type Err = UnexpectedLeapSecond; + + fn utc_offset(&self, date_time: DateTime) -> UtcOffset { + let leap_seconds = GLOBAL_LEAP_SECONDS.read(); + let past_leap_seconds = leap_seconds.leap_seconds_before_inclusive(date_time); + UtcOffset::from_seconds(-(past_leap_seconds as isize + 10)) + } + + // TODO optimize + fn offset_from_local_time(&self, date_time: NaiveDateTime) -> Result { + // TAI times cannot have leap seconds + if date_time.second() == 60 { + return Err(UnexpectedLeapSecond { + given_dt: date_time, + }); + } + + // calculate the number of seconds that have passed since date_time in UTC + let leap_seconds = GLOBAL_LEAP_SECONDS.read(); + let utc_dt = DateTime::from_utc(date_time, Utc); + let mut past_leap_seconds = leap_seconds.leap_seconds_before_inclusive(utc_dt); + let mut prev_pls = 0; // use this to see if the number of leap seconds has been updated + + // check if any leap seconds were found because of this calculation + // keep checking until there is no longer a change in the total leap seconds + while past_leap_seconds != prev_pls { + prev_pls = past_leap_seconds; + let ndt = date_time.add_seconds(past_leap_seconds as isize); + let utc_dt = DateTime::from_utc(ndt, Utc); + past_leap_seconds = leap_seconds.leap_seconds_before_inclusive(utc_dt); + } + + Ok(UtcOffset::from_seconds(-(past_leap_seconds as isize + 10))) + } +} + +#[cfg(test)] +mod tests { + use crate::{Date, Month, Time}; + + use super::*; + + #[test] + fn test_conversion_no_leap_seconds() { + let offset = unsafe { + Tai.offset_from_local_time(NaiveDateTime::new( + Date::from_ymd_unchecked(2000.into(), Month::January, 1), + Time::from_hms_unchecked(0, 0, 0), + )) + .unwrap() + }; + + assert_eq!(offset, UtcOffset::from_seconds(-10)) + } + + #[test] + fn test_conversion_one_leap_second() { + add_leap_second(unsafe { Date::from_ymd_unchecked(2000.into(), Month::January, 1) }); + let offset = unsafe { + Tai.offset_from_local_time(NaiveDateTime::new( + Date::from_ymd_unchecked(2000.into(), Month::January, 2), + Time::from_hms_unchecked(0, 0, 0), + )) + .unwrap() + }; + + assert_eq!(offset, UtcOffset::from_seconds(-11)) + } +} diff --git a/src/utc.rs b/src/utc.rs new file mode 100644 index 0000000..e2e5aad --- /dev/null +++ b/src/utc.rs @@ -0,0 +1 @@ +// TODO move stuff around -- cgit v1.2.3