use crate::{Errors, J1900_OFFSET, MJD_OFFSET, SECONDS_PER_DAY};
const LEAP_SECONDS: [f64; 28] = [
2_272_060_800.0,
2_287_785_600.0,
2_303_683_200.0,
2_335_219_200.0,
2_366_755_200.0,
2_398_291_200.0,
2_429_913_600.0,
2_461_449_600.0,
2_492_985_600.0,
2_524_521_600.0,
2_571_782_400.0,
2_603_318_400.0,
2_634_854_400.0,
2_698_012_800.0,
2_776_982_400.0,
2_840_140_800.0,
2_871_676_800.0,
2_918_937_600.0,
2_950_473_600.0,
2_982_009_600.0,
3_029_443_200.0,
3_076_704_000.0,
3_124_137_600.0,
3_345_062_400.0,
3_439_756_800.0,
3_550_089_600.0,
3_644_697_600.0,
3_692_217_600.0,
];
const JANUARY_YEARS: [i32; 17] = [
1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 1980, 1988, 1990, 1991, 1996, 1999, 2006, 2009,
2017,
];
const JULY_YEARS: [i32; 11] = [
1972, 1981, 1982, 1983, 1985, 1992, 1993, 1994, 1997, 2012, 2015,
];
const USUAL_DAYS_PER_MONTH: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const USUAL_DAYS_PER_YEAR: f64 = 365.0;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Epoch(f64);
impl Epoch {
pub fn from_tai_seconds(seconds: f64) -> Self {
Self { 0: seconds }
}
pub fn from_tai_days(days: f64) -> Self {
Self {
0: days * SECONDS_PER_DAY,
}
}
pub fn from_mjd_tai(days: f64) -> Self {
Self {
0: (days - J1900_OFFSET) * SECONDS_PER_DAY,
}
}
pub fn from_jde_tai(days: f64) -> Self {
Self {
0: (days - J1900_OFFSET - MJD_OFFSET) * SECONDS_PER_DAY,
}
}
pub fn maybe_from_gregorian_tai(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanos: u32,
) -> Result<Self, Errors> {
if !is_gregorian_valid(year, month, day, hour, minute, second, nanos) {
return Err(Errors::Carry);
}
let mut seconds_wrt_1900: f64 =
f64::from((year - 1900).abs()) * SECONDS_PER_DAY * USUAL_DAYS_PER_YEAR;
for year in 1900..year {
if is_leap_year(year) {
seconds_wrt_1900 += SECONDS_PER_DAY;
}
}
for month in 0..month - 1 {
seconds_wrt_1900 += SECONDS_PER_DAY * f64::from(USUAL_DAYS_PER_MONTH[(month) as usize]);
}
if is_leap_year(year) && month > 2 {
seconds_wrt_1900 += SECONDS_PER_DAY;
}
seconds_wrt_1900 += f64::from(day - 1) * SECONDS_PER_DAY
+ f64::from(hour) * 3600.0
+ f64::from(minute) * 60.0
+ f64::from(second);
if second == 60 {
seconds_wrt_1900 -= 1.0;
}
Ok(Self {
0: seconds_wrt_1900,
})
}
pub fn from_gregorian_tai(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanos: u32,
) -> Self {
Self::maybe_from_gregorian_tai(year, month, day, hour, minute, second, nanos)
.expect("invalid Gregorian date")
}
pub fn from_gregorian_tai_at_midnight(year: i32, month: u8, day: u8) -> Self {
Self::maybe_from_gregorian_tai(year, month, day, 0, 0, 0, 0)
.expect("invalid Gregorian date")
}
pub fn from_gregorian_tai_at_noon(year: i32, month: u8, day: u8) -> Self {
Self::maybe_from_gregorian_tai(year, month, day, 12, 0, 0, 0)
.expect("invalid Gregorian date")
}
pub fn from_gregorian_tai_hms(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
) -> Self {
Self::maybe_from_gregorian_tai(year, month, day, hour, minute, second, 0)
.expect("invalid Gregorian date")
}
pub fn maybe_from_gregorian_utc(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanos: u32,
) -> Result<Self, Errors> {
let mut if_tai =
Self::maybe_from_gregorian_tai(year, month, day, hour, minute, second, nanos)?;
let mut cnt = 0;
for tai_ts in LEAP_SECONDS.iter() {
if &if_tai.0 >= tai_ts {
if cnt == 0 {
cnt = 10;
} else {
cnt += 1;
}
}
}
if_tai.0 -= f64::from(cnt);
Ok(if_tai)
}
pub fn from_gregorian_utc(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanos: u32,
) -> Self {
Self::maybe_from_gregorian_utc(year, month, day, hour, minute, second, nanos)
.expect("invalid Gregorian date")
}
pub fn from_gregorian_utc_at_midnight(year: i32, month: u8, day: u8) -> Self {
Self::maybe_from_gregorian_utc(year, month, day, 0, 0, 0, 0)
.expect("invalid Gregorian date")
}
pub fn from_gregorian_utc_at_noon(year: i32, month: u8, day: u8) -> Self {
Self::maybe_from_gregorian_utc(year, month, day, 12, 0, 0, 0)
.expect("invalid Gregorian date")
}
pub fn from_gregorian_utc_hms(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
) -> Self {
Self::maybe_from_gregorian_utc(year, month, day, hour, minute, second, 0)
.expect("invalid Gregorian date")
}
pub fn as_tai_seconds(self) -> f64 {
self.0
}
pub fn as_tai_days(self) -> f64 {
self.0 / SECONDS_PER_DAY
}
pub fn as_utc_seconds(self) -> f64 {
let mut cnt = 0;
for tai_ts in LEAP_SECONDS.iter() {
if &self.0 >= tai_ts {
if cnt == 0 {
cnt = 10;
} else {
cnt += 1;
}
}
}
self.0 + f64::from(cnt)
}
pub fn as_utc_days(self) -> f64 {
self.as_utc_seconds() / SECONDS_PER_DAY
}
pub fn as_mjd_tai_days(self) -> f64 {
self.as_tai_days() + J1900_OFFSET
}
pub fn as_mjd_utc_days(self) -> f64 {
self.as_utc_days() + J1900_OFFSET
}
pub fn as_mjd_tai_seconds(self) -> f64 {
self.as_mjd_tai_days() * SECONDS_PER_DAY
}
pub fn as_mjd_utc_seconds(self) -> f64 {
self.as_mjd_utc_days() * SECONDS_PER_DAY
}
pub fn as_jde_tai_days(self) -> f64 {
self.as_mjd_tai_days() + MJD_OFFSET
}
pub fn as_jde_tai_seconds(self) -> f64 {
self.as_jde_tai_days() * SECONDS_PER_DAY
}
pub fn as_jde_utc_days(self) -> f64 {
self.as_mjd_utc_days() + MJD_OFFSET
}
pub fn as_jde_utc_seconds(self) -> f64 {
self.as_jde_utc_days() * SECONDS_PER_DAY
}
pub fn mut_add_days(&mut self, days: f64) {
self.0 += days * SECONDS_PER_DAY
}
pub fn mut_add_secs(&mut self, seconds: f64) {
self.0 += seconds
}
pub fn mut_sub_days(&mut self, days: f64) {
self.0 -= days * SECONDS_PER_DAY
}
pub fn mut_sub_secs(&mut self, seconds: f64) {
self.0 -= seconds
}
}
pub fn is_gregorian_valid(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanos: u32,
) -> bool {
let max_seconds = if (month == 12 || month == 6)
&& day == USUAL_DAYS_PER_MONTH[month as usize - 1]
&& hour == 23
&& minute == 59
&& ((month == 6 && JULY_YEARS.contains(&year))
|| (month == 12 && JANUARY_YEARS.contains(&(year + 1))))
{
60
} else {
59
};
if month == 0
|| month > 12
|| day == 0
|| day > 31
|| hour > 24
|| minute > 59
|| second > max_seconds
|| f64::from(nanos) > 1e9
{
return false;
}
if day > USUAL_DAYS_PER_MONTH[month as usize - 1] && (month != 2 || !is_leap_year(year)) {
return false;
}
true
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
#[test]
fn utc_epochs() {
use std::f64::EPSILON;
assert!(Epoch::from_mjd_tai(J1900_OFFSET).as_tai_seconds() < EPSILON);
assert!((Epoch::from_mjd_tai(J1900_OFFSET).as_mjd_tai_days() - J1900_OFFSET).abs() < EPSILON);
let this_epoch = Epoch::from_tai_seconds(1_199_333_568.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1938, 1, 3, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_204_156_800.0);
let epoch_utc =
Epoch::maybe_from_gregorian_utc(1938, 2, 28, 00, 00, 00, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_204_243_199.0);
let epoch_utc =
Epoch::maybe_from_gregorian_utc(1938, 2, 28, 23, 59, 59, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_204_243_200.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1938, 3, 1, 00, 00, 00, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_206_850_368.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1938, 3, 31, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_214_194_368.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1938, 6, 24, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_220_069_568.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1938, 8, 31, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_230_610_368.0);
let epoch_utc =
Epoch::maybe_from_gregorian_utc(1938, 12, 31, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_230_696_768.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1939, 1, 1, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_235_794_368.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1939, 3, 1, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_267_416_768.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1940, 3, 1, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_233_375_168.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1939, 2, 1, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_264_911_168.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1940, 2, 1, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_267_243_968.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1940, 2, 28, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(1_267_330_368.0);
let epoch_utc = Epoch::maybe_from_gregorian_utc(1940, 2, 29, 4, 12, 48, 0).expect("init epoch");
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(2_272_060_790.0);
let epoch_utc = Epoch::from_gregorian_utc_at_midnight(1972, 1, 1);
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(2_272_060_800.0);
let epoch_utc = Epoch::from_gregorian_utc_hms(1972, 1, 1, 0, 0, 10);
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let this_epoch = Epoch::from_tai_seconds(3_692_217_599.0);
let epoch_utc = Epoch::from_gregorian_utc_hms(2017, 1, 1, 0, 0, 36);
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
let mut this_epoch = Epoch::from_tai_seconds(3_692_217_600.0);
let epoch_utc = Epoch::from_gregorian_utc_hms(2017, 1, 1, 0, 0, 37);
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch");
this_epoch.mut_add_secs(3600.0);
assert_eq!(
this_epoch,
Epoch::from_gregorian_utc_hms(2017, 1, 1, 1, 0, 37),
"Incorrect epoch after add"
);
this_epoch.mut_sub_secs(3600.0);
assert_eq!(epoch_utc, this_epoch, "Incorrect epoch after sub");
}
#[test]
fn julian_epoch() {
let nist_j1900 = Epoch::from_tai_days(0.0);
assert!((nist_j1900.as_mjd_tai_days() - 15_020.0).abs() < std::f64::EPSILON);
assert!((nist_j1900.as_jde_tai_days() - 2_415_020.5).abs() < std::f64::EPSILON);
let mjd = Epoch::from_gregorian_utc_at_midnight(1900, 1, 1);
assert!((mjd.as_mjd_tai_days() - 15_020.0).abs() < std::f64::EPSILON);
let j1900 = Epoch::from_tai_days(0.5);
assert!((j1900.as_mjd_tai_days() - 15_020.5).abs() < std::f64::EPSILON);
assert!((j1900.as_jde_tai_days() - 2_415_021.0).abs() < std::f64::EPSILON);
let mjd = Epoch::from_gregorian_utc_at_noon(1900, 1, 1);
assert!((mjd.as_mjd_tai_days() - 15_020.5).abs() < std::f64::EPSILON);
let mjd = Epoch::from_gregorian_utc_at_midnight(1900, 1, 8);
assert!((mjd.as_mjd_tai_days() - 15_027.0).abs() < std::f64::EPSILON);
assert!((mjd.as_jde_tai_days() - 2_415_027.5).abs() < std::f64::EPSILON);
let gps_std_epoch = Epoch::from_gregorian_tai_at_midnight(1980, 1, 6);
assert!((gps_std_epoch.as_mjd_tai_days() - 44_244.0).abs() < std::f64::EPSILON);
assert!((gps_std_epoch.as_jde_tai_days() - 2_444_244.5).abs() < std::f64::EPSILON);
let j2000 = Epoch::from_gregorian_tai_at_midnight(2000, 1, 1);
assert!((j2000.as_mjd_tai_days() - 51_544.0).abs() < std::f64::EPSILON);
assert!((j2000.as_jde_tai_days() - 2_451_544.5).abs() < std::f64::EPSILON);
let j2000 = Epoch::from_gregorian_utc_at_midnight(2000, 1, 1);
assert!((j2000.as_mjd_utc_days() - 51_544.0).abs() < std::f64::EPSILON);
assert!((j2000.as_jde_utc_days() - 2_451_544.5).abs() < std::f64::EPSILON);
let jd020207 = Epoch::from_gregorian_tai_at_midnight(2002, 2, 7);
assert!((jd020207.as_mjd_tai_days() - 52_312.0).abs() < std::f64::EPSILON);
assert!((jd020207.as_jde_tai_days() - 2_452_312.5).abs() < std::f64::EPSILON);
assert!(
(Epoch::from_gregorian_tai_hms(2015, 6, 30, 23, 59, 59).as_mjd_tai_days()
- 57_203.999_988_425_92)
.abs()
< std::f64::EPSILON,
"Incorrect July 2015 leap second MJD computed"
);
assert!(
(Epoch::from_gregorian_tai_hms(2015, 6, 30, 23, 59, 60).as_mjd_tai_days()
- 57_203.999_988_425_92)
.abs()
< std::f64::EPSILON,
"Incorrect July 2015 leap second MJD computed"
);
assert!(
(Epoch::from_gregorian_tai_at_midnight(2015, 7, 1).as_mjd_tai_days() - 57_204.0).abs()
< std::f64::EPSILON,
"Incorrect Post July 2015 leap second MJD computed"
);
}
#[test]
fn leap_year() {
assert!(!is_leap_year(2019));
assert!(!is_leap_year(2001));
assert!(!is_leap_year(1000));
let leap_years: [i32; 146] = [
1804, 1808, 1812, 1816, 1820, 1824, 1828, 1832, 1836, 1840, 1844, 1848, 1852, 1856, 1860,
1864, 1868, 1872, 1876, 1880, 1884, 1888, 1892, 1896, 1904, 1908, 1912, 1916, 1920, 1924,
1928, 1932, 1936, 1940, 1944, 1948, 1952, 1956, 1960, 1964, 1968, 1972, 1976, 1980, 1984,
1988, 1992, 1996, 2000, 2004, 2008, 2012, 2016, 2020, 2024, 2028, 2032, 2036, 2040, 2044,
2048, 2052, 2056, 2060, 2064, 2068, 2072, 2076, 2080, 2084, 2088, 2092, 2096, 2104, 2108,
2112, 2116, 2120, 2124, 2128, 2132, 2136, 2140, 2144, 2148, 2152, 2156, 2160, 2164, 2168,
2172, 2176, 2180, 2184, 2188, 2192, 2196, 2204, 2208, 2212, 2216, 2220, 2224, 2228, 2232,
2236, 2240, 2244, 2248, 2252, 2256, 2260, 2264, 2268, 2272, 2276, 2280, 2284, 2288, 2292,
2296, 2304, 2308, 2312, 2316, 2320, 2324, 2328, 2332, 2336, 2340, 2344, 2348, 2352, 2356,
2360, 2364, 2368, 2372, 2376, 2380, 2384, 2388, 2392, 2396, 2400,
];
for year in leap_years.iter() {
assert!(is_leap_year(*year));
}
}
#[test]
fn datetime_invalid_dates() {
assert!(!is_gregorian_valid(2001, 2, 29, 22, 8, 47, 0));
assert!(!is_gregorian_valid(2016, 12, 31, 23, 59, 61, 0));
assert!(!is_gregorian_valid(2015, 6, 30, 23, 59, 61, 0));
}