use crate::constants::UNIX_EPOCH_JD;
use crate::julian::JulianDate;
use crate::parsing::parse_iso8601;
use crate::{TimeError, TimeResult};
use celestial_core::constants::{J2000_JD, SECONDS_PER_DAY_F64};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TT(JulianDate);
impl TT {
pub fn new(seconds: i64, nanos: u32) -> Self {
let total_seconds =
seconds as f64 + nanos as f64 / celestial_core::constants::NANOSECONDS_PER_SECOND_F64;
let jd = JulianDate::from_f64(UNIX_EPOCH_JD + total_seconds / SECONDS_PER_DAY_F64);
Self(jd)
}
pub fn from_julian_date(jd: JulianDate) -> Self {
Self(jd)
}
pub fn from_julian_date_raw(jd1: f64, jd2: f64) -> Self {
Self(JulianDate::new(jd1, jd2))
}
pub fn j2000() -> Self {
Self(JulianDate::j2000())
}
pub fn to_julian_date(&self) -> JulianDate {
self.0
}
pub fn add_seconds(&self, seconds: f64) -> Self {
Self(self.0.add_seconds(seconds))
}
pub fn add_days(&self, days: f64) -> Self {
Self(self.0.add_days(days))
}
pub fn from_jd(jd: f64) -> TimeResult<Self> {
Ok(Self(JulianDate::from_f64(jd)))
}
pub fn julian_year(&self) -> f64 {
2000.0 + (self.0.to_f64() - J2000_JD) / 365.25
}
pub fn centuries_since_j2000(&self) -> f64 {
(self.0.to_f64() - J2000_JD) / celestial_core::constants::DAYS_PER_JULIAN_CENTURY
}
}
impl fmt::Display for TT {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TT {}", self.0)
}
}
impl From<JulianDate> for TT {
fn from(jd: JulianDate) -> Self {
Self::from_julian_date(jd)
}
}
impl FromStr for TT {
type Err = TimeError;
fn from_str(s: &str) -> TimeResult<Self> {
let parsed = parse_iso8601(s)?;
Ok(Self::from_julian_date(parsed.to_julian_date()))
}
}
pub fn tt_from_calendar(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> TT {
let jd = JulianDate::from_calendar(year, month, day, hour, minute, second);
TT::from_julian_date(jd)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::UNIX_EPOCH_JD;
#[test]
fn test_tt_constructors() {
assert_eq!(TT::new(0, 0).to_julian_date().to_f64(), UNIX_EPOCH_JD);
assert_eq!(TT::j2000().to_julian_date().to_f64(), J2000_JD);
assert_eq!(
tt_from_calendar(2000, 1, 1, 12, 0, 0.0)
.to_julian_date()
.to_f64(),
J2000_JD
);
assert_eq!(
TT::from_jd(J2000_JD).unwrap().to_julian_date().to_f64(),
J2000_JD
);
}
#[test]
fn test_tt_from_julian_date_raw() {
let tt = TT::from_julian_date_raw(J2000_JD, 0.5);
assert_eq!(tt.to_julian_date().jd1(), J2000_JD);
assert_eq!(tt.to_julian_date().jd2(), 0.5);
}
#[test]
fn test_tt_arithmetic() {
let tt = TT::j2000();
assert_eq!(tt.add_days(1.0).to_julian_date().to_f64(), J2000_JD + 1.0);
assert_eq!(
tt.add_seconds(3600.0).to_julian_date().to_f64(),
J2000_JD + 1.0 / 24.0
);
}
#[test]
fn test_tt_julian_year_and_centuries() {
let tt = TT::j2000();
assert_eq!(tt.julian_year(), 2000.0);
assert_eq!(tt.centuries_since_j2000(), 0.0);
let tt_plus_century = tt.add_days(celestial_core::constants::DAYS_PER_JULIAN_CENTURY);
assert_eq!(tt_plus_century.centuries_since_j2000(), 1.0);
}
#[test]
fn test_tt_from_julian_date_trait() {
let jd = JulianDate::new(J2000_JD, 0.123456789);
let tt_direct = TT::from_julian_date(jd);
let tt_from_trait: TT = jd.into();
assert_eq!(
tt_direct.to_julian_date().jd1(),
tt_from_trait.to_julian_date().jd1()
);
assert_eq!(
tt_direct.to_julian_date().jd2(),
tt_from_trait.to_julian_date().jd2()
);
}
#[test]
fn test_tt_display() {
let tt = TT::from_julian_date(JulianDate::new(J2000_JD, 0.5));
let display_str = format!("{}", tt);
assert!(display_str.starts_with("TT"));
assert!(display_str.contains("2451545"));
}
#[test]
fn test_tt_string_parsing() {
assert_eq!(
TT::from_str("2000-01-01T12:00:00")
.unwrap()
.to_julian_date()
.to_f64(),
TT::j2000().to_julian_date().to_f64()
);
assert!(TT::from_str("invalid-date").is_err());
}
#[test]
fn test_tt_string_parsing_fractional_seconds() {
let result = TT::from_str("2000-01-01T12:00:00.123").unwrap();
let expected = tt_from_calendar(2000, 1, 1, 12, 0, 0.123);
assert_eq!(
result.to_julian_date().to_f64(),
expected.to_julian_date().to_f64()
);
}
#[cfg(feature = "serde")]
#[test]
fn test_tt_serde_round_trip() {
let test_cases = [
TT::j2000(),
TT::new(0, 0),
tt_from_calendar(2024, 6, 15, 14, 30, 45.123),
tt_from_calendar(1990, 12, 31, 23, 59, 59.999999999),
];
for original in test_cases {
let json = serde_json::to_string(&original).unwrap();
let deserialized: TT = serde_json::from_str(&json).unwrap();
let total_diff =
(original.to_julian_date().to_f64() - deserialized.to_julian_date().to_f64()).abs();
assert!(
total_diff < 1e-14,
"serde precision loss: {:.2e}",
total_diff
);
}
}
}