use crate::epoch::SECONDS_PER_DAY;
pub const UTC_EPOCH_TAI_TJT: f64 = 11_544.499_257_129_63;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CalendarDate {
pub year: i32,
pub month: i32,
pub day: i32,
pub hour: i32,
pub minute: i32,
pub second: f64,
}
impl CalendarDate {
pub fn new(year: i32, month: i32, day: i32, hour: i32, minute: i32, second: f64) -> Self {
Self {
year,
month,
day,
hour,
minute,
second,
}
}
}
pub fn calendar_to_tjt(cal: &CalendarDate) -> f64 {
assert!(
(1..=12).contains(&cal.month),
"month must be 1..=12, got {}",
cal.month
);
assert!(
(1..=31).contains(&cal.day),
"day must be 1..=31, got {}",
cal.day
);
assert!(
(0..=23).contains(&cal.hour),
"hour must be 0..=23, got {}",
cal.hour
);
assert!(
(0..=59).contains(&cal.minute),
"minute must be 0..=59, got {}",
cal.minute
);
assert!(
cal.second >= 0.0 && cal.second < 61.0,
"second must be in [0, 61), got {}",
cal.second
);
assert!(
cal.year > 1600 || (cal.year == 1600 && cal.month >= 3),
"date must be 1600-03-01 or later, got {}-{:02}-{:02}",
cal.year,
cal.month,
cal.day
);
let y = cal.year - 1601 + (cal.month + 9) / 12;
let n_400 = y / 400;
let y_400 = y - 400 * n_400;
let n_100 = y_400 / 100;
let y_100 = y_400 - 100 * n_100;
let n_4 = y_100 / 4;
let n_1 = y_100 - 4 * n_4;
let m = cal.month - 2 + 12 * (1 - ((cal.month + 9) / 12) + n_1);
(cal.day as f64) + ((30.585 * m as f64).floor()) - (2 * n_1) as f64 - 31.0
+ (1461 * n_4) as f64
+ (36524 * n_100) as f64
+ (146097 * n_400) as f64
- 134493.0
+ (cal.hour as f64 / 24.0)
+ (cal.minute as f64 / 1440.0)
+ (cal.second / SECONDS_PER_DAY)
}
pub fn tjt_to_calendar(tjt: f64) -> CalendarDate {
let mut julian_day = tjt.floor() as i32;
let temp = 1440.0 * (tjt - julian_day as f64);
let mut calendar_minute = temp as i32;
let mut calendar_second = 60.0 * (temp - calendar_minute as f64);
let clock_resolution = 1e-6;
if calendar_second > 60.0 - clock_resolution {
calendar_minute += 1;
if calendar_minute >= 1440 {
julian_day += 1;
calendar_minute -= 1440;
}
calendar_second = 0.0;
}
let calendar_hour = calendar_minute / 60;
calendar_minute -= calendar_hour * 60;
let jd = julian_day + 134493; let mut n_400 = jd / 146097;
if jd < 0 {
n_400 -= 1;
}
let r_400 = jd - 146097 * n_400 + 1;
let n_100 = (r_400 as f64 / 36524.3) as i32;
let r_100 = r_400 - 36524 * n_100 - 1;
let n_4 = r_100 / 1461;
let r_4 = r_100 - 1461 * n_4 + 1;
let n_1 = (r_4 as f64 / 365.3) as i32;
let r_4a = r_4 + 30 + 2 * n_1;
let m = (r_4a as f64 / 30.585) as i32;
let calendar_day = r_4a - (30.585 * m as f64) as i32;
let calendar_month = m + 2 - 12 * ((m + 1) / 12);
let calendar_year = 1600 + 400 * n_400 + 100 * n_100 + 4 * n_4 + ((m + 1) / 12);
CalendarDate {
year: calendar_year,
month: calendar_month,
day: calendar_day,
hour: calendar_hour,
minute: calendar_minute,
second: calendar_second,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calendar_to_tjt_j2000() {
let cal = CalendarDate::new(2000, 1, 1, 12, 0, 0.0);
let tjt = calendar_to_tjt(&cal);
assert!(
(tjt - 11544.5).abs() < 1e-10,
"J2000 TJT: expected 11544.5, got {}",
tjt
);
}
#[test]
fn calendar_to_tjt_round_trip() {
let dates = [
CalendarDate::new(1972, 1, 1, 0, 0, 0.0),
CalendarDate::new(2000, 1, 1, 12, 0, 0.0),
CalendarDate::new(2024, 6, 15, 14, 30, 45.123),
CalendarDate::new(1980, 1, 6, 0, 0, 0.0), ];
for cal in &dates {
let tjt = calendar_to_tjt(cal);
let back = tjt_to_calendar(tjt);
assert_eq!(back.year, cal.year, "year mismatch for {:?}", cal);
assert_eq!(back.month, cal.month, "month mismatch for {:?}", cal);
assert_eq!(back.day, cal.day, "day mismatch for {:?}", cal);
assert_eq!(back.hour, cal.hour, "hour mismatch for {:?}", cal);
assert_eq!(back.minute, cal.minute, "minute mismatch for {:?}", cal);
assert!(
(back.second - cal.second).abs() < 1e-6,
"second mismatch for {:?}: got {}",
cal,
back.second
);
}
}
#[test]
fn tjt_to_calendar_j2000() {
let cal = tjt_to_calendar(11544.5);
assert_eq!(cal.year, 2000);
assert_eq!(cal.month, 1);
assert_eq!(cal.day, 1);
assert_eq!(cal.hour, 12);
assert_eq!(cal.minute, 0);
assert!(cal.second.abs() < 1e-6);
}
#[test]
fn leap_year_feb_29() {
let cal = CalendarDate::new(2000, 2, 29, 0, 0, 0.0);
let tjt = calendar_to_tjt(&cal);
let back = tjt_to_calendar(tjt);
assert_eq!(back.year, 2000);
assert_eq!(back.month, 2);
assert_eq!(back.day, 29);
}
#[test]
fn dec_31_to_jan_1_transition() {
let dec31 = CalendarDate::new(2016, 12, 31, 23, 59, 59.0);
let tjt_dec31 = calendar_to_tjt(&dec31);
let tjt_next = tjt_dec31 + 2.0 / SECONDS_PER_DAY;
let cal = tjt_to_calendar(tjt_next);
assert_eq!(cal.year, 2017);
assert_eq!(cal.month, 1);
assert_eq!(cal.day, 1);
assert_eq!(cal.hour, 0);
assert_eq!(cal.minute, 0);
assert!((cal.second - 1.0).abs() < 1e-6);
}
}