Skip to main content

celestial_time/scales/
tt.rs

1//! Terrestrial Time (TT) time scale.
2//!
3//! TT is the modern successor to Ephemeris Time (ET) and Terrestrial Dynamical Time (TDT).
4//! It provides a uniform time scale for geocentric ephemerides and is the basis for
5//! planetary position calculations referenced to Earth's center.
6//!
7//! # Relationship to TAI
8//!
9//! TT differs from TAI by a fixed offset:
10//!
11//! ```text
12//! TT = TAI + 32.184 seconds
13//! ```
14//!
15//! The 32.184s offset was chosen to maintain continuity with ET at the 1977 epoch.
16//!
17//! # Usage
18//!
19//! ```
20//! use celestial_time::{JulianDate, TT};
21//!
22//! // Create TT at J2000.0 epoch
23//! let tt = TT::j2000();
24//!
25//! // From calendar date
26//! use celestial_time::scales::tt::tt_from_calendar;
27//! let tt = tt_from_calendar(2000, 1, 1, 12, 0, 0.0);
28//!
29//! // Parse from ISO 8601
30//! let tt: TT = "2000-01-01T12:00:00".parse().unwrap();
31//!
32//! // Julian centuries since J2000.0 (for precession/nutation)
33//! let centuries = tt.centuries_since_j2000();
34//! ```
35//!
36//! # Precision
37//!
38//! TT uses split Julian Date storage internally, preserving microsecond accuracy
39//! across the full date range. The `centuries_since_j2000()` method provides
40//! the T parameter used in IAU precession and nutation models.
41
42use crate::constants::UNIX_EPOCH_JD;
43use crate::julian::JulianDate;
44use crate::parsing::parse_iso8601;
45use crate::{TimeError, TimeResult};
46use celestial_core::constants::{J2000_JD, SECONDS_PER_DAY_F64};
47use std::fmt;
48use std::str::FromStr;
49
50/// Terrestrial Time representation.
51///
52/// Wraps a split Julian Date for high-precision time storage.
53/// TT is the primary time scale for geocentric ephemeris calculations.
54#[derive(Debug, Clone, Copy, PartialEq)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
56pub struct TT(JulianDate);
57
58impl TT {
59    /// Creates TT from Unix timestamp components.
60    ///
61    /// Converts seconds and nanoseconds since Unix epoch (1970-01-01T00:00:00)
62    /// to TT Julian Date representation.
63    pub fn new(seconds: i64, nanos: u32) -> Self {
64        let total_seconds =
65            seconds as f64 + nanos as f64 / celestial_core::constants::NANOSECONDS_PER_SECOND_F64;
66        let jd = JulianDate::from_f64(UNIX_EPOCH_JD + total_seconds / SECONDS_PER_DAY_F64);
67        Self(jd)
68    }
69
70    /// Creates TT from a split Julian Date.
71    pub fn from_julian_date(jd: JulianDate) -> Self {
72        Self(jd)
73    }
74
75    /// Creates TT from raw Julian Date components.
76    ///
77    /// Use when you have separate jd1/jd2 values and want to avoid
78    /// intermediate JulianDate construction.
79    pub fn from_julian_date_raw(jd1: f64, jd2: f64) -> Self {
80        Self(JulianDate::new(jd1, jd2))
81    }
82
83    /// Returns TT at J2000.0 epoch (2000-01-01T12:00:00 TT).
84    ///
85    /// This is the fundamental epoch for modern astronomical calculations.
86    /// JD = 2451545.0.
87    pub fn j2000() -> Self {
88        Self(JulianDate::j2000())
89    }
90
91    /// Returns the underlying Julian Date.
92    pub fn to_julian_date(&self) -> JulianDate {
93        self.0
94    }
95
96    /// Returns a new TT offset by the given number of seconds.
97    pub fn add_seconds(&self, seconds: f64) -> Self {
98        Self(self.0.add_seconds(seconds))
99    }
100
101    /// Returns a new TT offset by the given number of days.
102    pub fn add_days(&self, days: f64) -> Self {
103        Self(self.0.add_days(days))
104    }
105
106    /// Creates TT from a single-value Julian Date.
107    ///
108    /// For high-precision work, prefer `from_julian_date` with split values.
109    pub fn from_jd(jd: f64) -> TimeResult<Self> {
110        Ok(Self(JulianDate::from_f64(jd)))
111    }
112
113    /// Returns the Julian year corresponding to this TT instant.
114    ///
115    /// Julian year = 2000.0 + (JD - J2000_JD) / 365.25
116    pub fn julian_year(&self) -> f64 {
117        2000.0 + (self.0.to_f64() - J2000_JD) / 365.25
118    }
119
120    /// Returns Julian centuries since J2000.0 (the T parameter).
121    ///
122    /// This is the time argument used in IAU precession and nutation series.
123    /// One Julian century = 36525 days.
124    pub fn centuries_since_j2000(&self) -> f64 {
125        (self.0.to_f64() - J2000_JD) / celestial_core::constants::DAYS_PER_JULIAN_CENTURY
126    }
127}
128
129/// Formats as "TT {julian_date}".
130impl fmt::Display for TT {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "TT {}", self.0)
133    }
134}
135
136/// Converts JulianDate to TT directly.
137impl From<JulianDate> for TT {
138    fn from(jd: JulianDate) -> Self {
139        Self::from_julian_date(jd)
140    }
141}
142
143/// Parses TT from ISO 8601 format (e.g., "2000-01-01T12:00:00").
144///
145/// Assumes the input string represents a TT instant directly.
146impl FromStr for TT {
147    type Err = TimeError;
148
149    fn from_str(s: &str) -> TimeResult<Self> {
150        let parsed = parse_iso8601(s)?;
151        Ok(Self::from_julian_date(parsed.to_julian_date()))
152    }
153}
154
155/// Creates TT from calendar components.
156///
157/// Converts Gregorian calendar date and time to TT. The input is interpreted
158/// directly as TT with no UTC or leap second corrections applied.
159///
160/// # Arguments
161///
162/// * `year` - Gregorian year (negative for BCE)
163/// * `month` - Month (1-12)
164/// * `day` - Day of month (1-31)
165/// * `hour` - Hour (0-23)
166/// * `minute` - Minute (0-59)
167/// * `second` - Second with fractional part (0.0 to <61.0)
168pub fn tt_from_calendar(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> TT {
169    let jd = JulianDate::from_calendar(year, month, day, hour, minute, second);
170    TT::from_julian_date(jd)
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::constants::UNIX_EPOCH_JD;
177
178    #[test]
179    fn test_tt_constructors() {
180        assert_eq!(TT::new(0, 0).to_julian_date().to_f64(), UNIX_EPOCH_JD);
181        assert_eq!(TT::j2000().to_julian_date().to_f64(), J2000_JD);
182        assert_eq!(
183            tt_from_calendar(2000, 1, 1, 12, 0, 0.0)
184                .to_julian_date()
185                .to_f64(),
186            J2000_JD
187        );
188        assert_eq!(
189            TT::from_jd(J2000_JD).unwrap().to_julian_date().to_f64(),
190            J2000_JD
191        );
192    }
193
194    #[test]
195    fn test_tt_from_julian_date_raw() {
196        let tt = TT::from_julian_date_raw(J2000_JD, 0.5);
197        assert_eq!(tt.to_julian_date().jd1(), J2000_JD);
198        assert_eq!(tt.to_julian_date().jd2(), 0.5);
199    }
200
201    #[test]
202    fn test_tt_arithmetic() {
203        let tt = TT::j2000();
204        assert_eq!(tt.add_days(1.0).to_julian_date().to_f64(), J2000_JD + 1.0);
205        assert_eq!(
206            tt.add_seconds(3600.0).to_julian_date().to_f64(),
207            J2000_JD + 1.0 / 24.0
208        );
209    }
210
211    #[test]
212    fn test_tt_julian_year_and_centuries() {
213        let tt = TT::j2000();
214        assert_eq!(tt.julian_year(), 2000.0);
215        assert_eq!(tt.centuries_since_j2000(), 0.0);
216
217        let tt_plus_century = tt.add_days(celestial_core::constants::DAYS_PER_JULIAN_CENTURY);
218        assert_eq!(tt_plus_century.centuries_since_j2000(), 1.0);
219    }
220
221    #[test]
222    fn test_tt_from_julian_date_trait() {
223        let jd = JulianDate::new(J2000_JD, 0.123456789);
224        let tt_direct = TT::from_julian_date(jd);
225        let tt_from_trait: TT = jd.into();
226
227        assert_eq!(
228            tt_direct.to_julian_date().jd1(),
229            tt_from_trait.to_julian_date().jd1()
230        );
231        assert_eq!(
232            tt_direct.to_julian_date().jd2(),
233            tt_from_trait.to_julian_date().jd2()
234        );
235    }
236
237    #[test]
238    fn test_tt_display() {
239        let tt = TT::from_julian_date(JulianDate::new(J2000_JD, 0.5));
240        let display_str = format!("{}", tt);
241        assert!(display_str.starts_with("TT"));
242        assert!(display_str.contains("2451545"));
243    }
244
245    #[test]
246    fn test_tt_string_parsing() {
247        assert_eq!(
248            TT::from_str("2000-01-01T12:00:00")
249                .unwrap()
250                .to_julian_date()
251                .to_f64(),
252            TT::j2000().to_julian_date().to_f64()
253        );
254        assert!(TT::from_str("invalid-date").is_err());
255    }
256
257    #[test]
258    fn test_tt_string_parsing_fractional_seconds() {
259        let result = TT::from_str("2000-01-01T12:00:00.123").unwrap();
260        let expected = tt_from_calendar(2000, 1, 1, 12, 0, 0.123);
261        assert_eq!(
262            result.to_julian_date().to_f64(),
263            expected.to_julian_date().to_f64()
264        );
265    }
266
267    #[cfg(feature = "serde")]
268    #[test]
269    fn test_tt_serde_round_trip() {
270        let test_cases = [
271            TT::j2000(),
272            TT::new(0, 0),
273            tt_from_calendar(2024, 6, 15, 14, 30, 45.123),
274            tt_from_calendar(1990, 12, 31, 23, 59, 59.999999999),
275        ];
276
277        for original in test_cases {
278            let json = serde_json::to_string(&original).unwrap();
279            let deserialized: TT = serde_json::from_str(&json).unwrap();
280
281            let total_diff =
282                (original.to_julian_date().to_f64() - deserialized.to_julian_date().to_f64()).abs();
283            assert!(
284                total_diff < 1e-14,
285                "serde precision loss: {:.2e}",
286                total_diff
287            );
288        }
289    }
290}