use core::ops::Sub;
use crate::{
ConvertUnit, Days, Fraction, FromLeapSecondDateTime, Hours, IntoLeapSecondDateTime,
IntoTimeScale, LeapSecondProvider, Minutes, MulFloor, Second, Seconds, TerrestrialTime,
TimePoint, TryIntoExact,
arithmetic::TryFromExact,
calendar::{Date, Month},
errors::{InvalidGlonassDateTime, InvalidTimeOfDay},
time_scale::{AbsoluteTimeScale, TimeScale},
units::{SecondsPerDay, SecondsPerHour, SecondsPerMinute},
};
pub type GlonassTime<Representation = i64, Period = Second> =
TimePoint<Glonasst, Representation, Period>;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Glonasst;
impl TimeScale for Glonasst {
const NAME: &'static str = "Glonass Time";
const ABBREVIATION: &'static str = "GLONASST";
}
impl AbsoluteTimeScale for Glonasst {
const EPOCH: Date<i32> = match Date::from_historic_date(1996, Month::January, 1) {
Ok(epoch) => epoch,
Err(_) => unreachable!(),
};
}
impl TerrestrialTime for Glonasst {
type Representation = u8;
type Period = SecondsPerHour;
const TAI_OFFSET: Hours<u8> = Hours::new(3);
}
impl FromLeapSecondDateTime for GlonassTime<i64, Second> {
type Error = InvalidGlonassDateTime;
fn from_datetime(
date: Date<i32>,
hour: u8,
minute: u8,
second: u8,
leap_second_provider: &impl LeapSecondProvider,
) -> Result<Self, Self::Error> {
if hour > 23 || minute > 59 || second > 60 {
return Err(InvalidGlonassDateTime::InvalidTimeOfDay(InvalidTimeOfDay {
hour,
minute,
second,
}));
}
let utc_date = if hour < 3 { date - Days::new(1) } else { date };
let (is_leap_second, total_leap_seconds) =
leap_second_provider.leap_seconds_on_date(utc_date);
if second == 60 && !is_leap_second {
return Err(InvalidGlonassDateTime::NonLeapSecondDateTime {
date,
hour,
minute,
second,
});
}
let days_since_scale_epoch = {
let days_since_1970 = date.time_since_epoch();
let epoch_days_since_1970 = Glonasst::EPOCH.time_since_epoch();
(days_since_1970 - epoch_days_since_1970).cast()
};
let hours = Hours::new(hour).cast();
let minutes = Minutes::new(minute).cast();
let seconds = Seconds::new(second).cast();
let time_since_epoch = days_since_scale_epoch.into_unit()
+ hours.into_unit()
+ minutes.into_unit()
+ seconds
+ total_leap_seconds.cast();
Ok(TimePoint::from_time_since_epoch(time_since_epoch))
}
}
impl<Representation> IntoLeapSecondDateTime for GlonassTime<Representation, Second>
where
Representation: Copy
+ ConvertUnit<SecondsPerMinute, Second>
+ ConvertUnit<SecondsPerHour, Second>
+ ConvertUnit<SecondsPerDay, Second>
+ MulFloor<Fraction, Output = Representation>
+ Sub<Representation, Output = Representation>
+ TryIntoExact<i32>
+ TryIntoExact<u8>
+ TryFromExact<u8>,
i64: TryFromExact<Representation>,
{
fn into_datetime(
self,
leap_second_provider: &impl LeapSecondProvider,
) -> (Date<i32>, u8, u8, u8) {
let seconds_since_scale_epoch = self.time_since_epoch();
let time_i64 = self
.try_cast()
.unwrap_or_else(|_| panic!())
.into_time_scale();
let (is_leap_second, leap_seconds) = leap_second_provider.leap_seconds_at_time(time_i64);
let leap_seconds = leap_seconds.try_into_exact().unwrap_or_else(|_| panic!());
let seconds_since_scale_epoch = seconds_since_scale_epoch - leap_seconds;
let (days_since_scale_epoch, seconds_in_day) =
seconds_since_scale_epoch.factor_out::<SecondsPerDay>();
let days_since_scale_epoch: Days<i32> = days_since_scale_epoch
.try_cast()
.unwrap_or_else(|_| panic!("Call of `datetime_from_time_point` results in days since scale epoch outside of `i32` range"));
let (hour, seconds_in_hour) = seconds_in_day.factor_out::<SecondsPerHour>();
let (minute, second) = seconds_in_hour.factor_out::<SecondsPerMinute>();
let second = second.floor::<Second>();
let days_since_universal_epoch =
Glonasst::EPOCH.time_since_epoch() + days_since_scale_epoch;
let date = Date::from_time_since_epoch(days_since_universal_epoch);
if is_leap_second {
let date = (date - Days::new(1)).try_cast().expect("Call of `datetime_from_time_point` results in date outside of representable range of `i32`");
(date, 23, 59, 60)
} else {
(
date.try_cast()
.expect("Call of `datetime_from_time_point` results in date outside of representable range of `i32`"),
hour.count().try_into_exact().unwrap_or_else(|_| panic!("Call of `datetime_from_time_point` results in hour value that cannot be expressed as `u8`")),
minute.count().try_into_exact().unwrap_or_else(|_| panic!("Call of `datetime_from_time_point` results in minute value that cannot be expressed as `u8`")),
second.count().try_into_exact().unwrap_or_else(|_| panic!("Call of `datetime_from_time_point` results in second value that cannot be expressed as `u8`")),
)
}
}
}
#[test]
fn known_timestamps() {
use crate::{IntoTimeScale, Seconds, UtcTime};
let utc = UtcTime::from_historic_datetime(1996, Month::January, 1, 0, 0, 0).unwrap();
let glonasst = GlonassTime::from_historic_datetime(1996, Month::January, 1, 3, 0, 0).unwrap();
assert_eq!(utc.into_time_scale(), glonasst);
let utc = UtcTime::from_historic_datetime(1995, Month::December, 31, 21, 0, 0).unwrap();
let glonasst = GlonassTime::from_historic_datetime(1996, Month::January, 1, 0, 0, 0).unwrap();
assert_eq!(utc, glonasst.into_time_scale());
assert_eq!(glonasst.time_since_epoch(), Seconds::new(29));
}
#[cfg(test)]
fn date_roundtrip(year: i32, month: Month, day: u8, hour: u8, minute: u8, second: u8) {
let time = GlonassTime::from_historic_datetime(year, month, day, hour, minute, second).unwrap();
let (date, hour2, minute2, second2) = time.into_gregorian_datetime();
assert_eq!(date.year(), year);
assert_eq!(date.month(), month);
assert_eq!(date.day(), day);
assert_eq!(hour2, hour);
assert_eq!(minute2, minute);
assert_eq!(second2, second);
}
#[test]
fn date_decomposition() {
date_roundtrip(1999, Month::August, 22, 0, 0, 0);
date_roundtrip(1958, Month::January, 1, 0, 0, 0);
date_roundtrip(1958, Month::January, 2, 0, 0, 0);
date_roundtrip(1960, Month::January, 1, 0, 0, 0);
date_roundtrip(1961, Month::January, 1, 0, 0, 0);
date_roundtrip(1970, Month::January, 1, 0, 0, 0);
date_roundtrip(1976, Month::January, 1, 0, 0, 0);
date_roundtrip(2025, Month::July, 16, 16, 23, 24);
date_roundtrip(2034, Month::December, 26, 8, 2, 37);
date_roundtrip(2760, Month::April, 1, 21, 59, 58);
date_roundtrip(1643, Month::January, 4, 1, 1, 33);
date_roundtrip(1996, Month::January, 1, 3, 0, 0);
}