summaryrefslogtreecommitdiff
path: root/src/tai.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/tai.rs')
-rw-r--r--src/tai.rs144
1 files changed, 144 insertions, 0 deletions
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<LeapSeconds> = const_rwlock(LeapSeconds::empty());
+
+struct LeapSeconds(Vec<DateTime<Utc>>);
+
+impl LeapSeconds {
+ // TODO docs
+
+ const fn empty() -> Self {
+ Self(Vec::new())
+ }
+
+ fn leap_seconds_before_inclusive(&self, date_time: DateTime<Utc>) -> 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<Utc>) -> 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<UtcOffset, Self::Err> {
+ // 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))
+ }
+}