Skip to main content

celestial_time/scales/
tdb.rs

1//! Barycentric Dynamical Time (TDB) scale.
2//!
3//! TDB is the independent time argument for barycentric ephemerides of the solar system.
4//! It differs from TT by small periodic terms (max ~1.7 ms) due to relativistic effects
5//! from Earth's orbital motion around the solar system barycenter.
6//!
7//! # Background
8//!
9//! TDB runs at a different rate than TT due to gravitational time dilation and velocity
10//! effects. The difference TDB-TT is dominated by a ~1.7 ms amplitude term with a period
11//! of one year, plus smaller terms. For most applications, TDB ≈ TT to within 2 ms.
12//!
13//! The IAU recommends using TCB (Barycentric Coordinate Time) for rigorous relativistic
14//! work. TDB is defined as a linear transformation of TCB that keeps TDB-TT bounded.
15//!
16//! # Usage
17//!
18//! ```
19//! use celestial_time::{JulianDate, TDB};
20//!
21//! let tdb = TDB::j2000();
22//! let tdb_plus_day = tdb.add_days(1.0);
23//!
24//! let tdb_from_cal = celestial_time::scales::tdb::tdb_from_calendar(2000, 1, 1, 12, 0, 0.0);
25//! ```
26//!
27//! # Precision
28//!
29//! Internally stores time as a split Julian Date for sub-microsecond precision.
30//! Arithmetic operations preserve precision by operating on the underlying JulianDate.
31
32use crate::constants::UNIX_EPOCH_JD;
33use crate::julian::JulianDate;
34use crate::parsing::parse_iso8601;
35use crate::{TimeError, TimeResult};
36use celestial_core::constants::SECONDS_PER_DAY_F64;
37use std::fmt;
38use std::str::FromStr;
39
40/// Barycentric Dynamical Time.
41///
42/// A time scale for solar system barycentric ephemerides. Wraps a split Julian Date
43/// for high-precision arithmetic. TDB tracks TT to within ~2 ms over centuries.
44#[derive(Debug, Clone, Copy, PartialEq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct TDB(JulianDate);
47
48impl TDB {
49    /// Creates TDB from Unix timestamp components.
50    ///
51    /// Converts seconds and nanoseconds since 1970-01-01 to TDB Julian Date.
52    pub fn new(seconds: i64, nanos: u32) -> Self {
53        let total_seconds =
54            seconds as f64 + nanos as f64 / celestial_core::constants::NANOSECONDS_PER_SECOND_F64;
55        let jd = JulianDate::from_f64(UNIX_EPOCH_JD + total_seconds / SECONDS_PER_DAY_F64);
56        Self(jd)
57    }
58
59    /// Creates TDB from a Julian Date.
60    pub fn from_julian_date(jd: JulianDate) -> Self {
61        Self(jd)
62    }
63
64    /// Returns TDB at the J2000.0 epoch (2000-01-01T12:00:00 TDB).
65    pub fn j2000() -> Self {
66        Self(JulianDate::j2000())
67    }
68
69    /// Returns the underlying Julian Date.
70    pub fn to_julian_date(&self) -> JulianDate {
71        self.0
72    }
73
74    /// Adds seconds to this TDB instant, returning a new TDB.
75    pub fn add_seconds(&self, seconds: f64) -> Self {
76        Self(self.0.add_seconds(seconds))
77    }
78
79    /// Adds days to this TDB instant, returning a new TDB.
80    pub fn add_days(&self, days: f64) -> Self {
81        Self(self.0.add_days(days))
82    }
83}
84
85impl fmt::Display for TDB {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "TDB {}", self.0)
88    }
89}
90
91/// Conversion from JulianDate. No transformation applied.
92impl From<JulianDate> for TDB {
93    fn from(jd: JulianDate) -> Self {
94        Self::from_julian_date(jd)
95    }
96}
97
98/// Parses ISO 8601 string as TDB.
99///
100/// The string is interpreted directly as TDB without any scale conversion.
101impl FromStr for TDB {
102    type Err = TimeError;
103
104    fn from_str(s: &str) -> TimeResult<Self> {
105        let parsed = parse_iso8601(s)?;
106        Ok(Self::from_julian_date(parsed.to_julian_date()))
107    }
108}
109
110/// Creates TDB from calendar components.
111///
112/// Interprets the calendar date directly as TDB. For high-precision work,
113/// prefer constructing from a Julian Date directly.
114pub fn tdb_from_calendar(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> TDB {
115    let jd = JulianDate::from_calendar(year, month, day, hour, minute, second);
116    TDB::from_julian_date(jd)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::constants::UNIX_EPOCH_JD;
123    use celestial_core::constants::J2000_JD;
124
125    #[test]
126    fn test_tdb_constructors() {
127        assert_eq!(TDB::new(0, 0).to_julian_date().to_f64(), UNIX_EPOCH_JD);
128        assert_eq!(TDB::j2000().to_julian_date().to_f64(), J2000_JD);
129        assert_eq!(
130            tdb_from_calendar(2000, 1, 1, 12, 0, 0.0)
131                .to_julian_date()
132                .to_f64(),
133            J2000_JD
134        );
135    }
136
137    #[test]
138    fn test_tdb_arithmetic() {
139        let tdb = TDB::j2000();
140        assert_eq!(tdb.add_days(1.0).to_julian_date().to_f64(), J2000_JD + 1.0);
141        assert_eq!(
142            tdb.add_seconds(3600.0).to_julian_date().to_f64(),
143            J2000_JD + 1.0 / 24.0
144        );
145    }
146
147    #[test]
148    fn test_tdb_from_julian_date_trait() {
149        let jd = JulianDate::new(J2000_JD, 0.123456789);
150        let tdb_direct = TDB::from_julian_date(jd);
151        let tdb_from_trait: TDB = jd.into();
152
153        assert_eq!(
154            tdb_direct.to_julian_date().jd1(),
155            tdb_from_trait.to_julian_date().jd1()
156        );
157        assert_eq!(
158            tdb_direct.to_julian_date().jd2(),
159            tdb_from_trait.to_julian_date().jd2()
160        );
161    }
162
163    #[test]
164    fn test_tdb_display() {
165        let tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, 0.5));
166        let display_str = format!("{}", tdb);
167
168        assert!(display_str.starts_with("TDB"));
169        assert!(display_str.contains("2451545"));
170    }
171
172    #[test]
173    fn test_tdb_string_parsing() {
174        assert_eq!(
175            TDB::from_str("2000-01-01T12:00:00")
176                .unwrap()
177                .to_julian_date()
178                .to_f64(),
179            TDB::j2000().to_julian_date().to_f64()
180        );
181        assert!(TDB::from_str("invalid-date").is_err());
182
183        let result = TDB::from_str("2000-01-01T12:00:00.123").unwrap();
184        let expected_jd = J2000_JD + 0.123 / SECONDS_PER_DAY_F64;
185        let diff = (result.to_julian_date().to_f64() - expected_jd).abs();
186        assert!(diff < 1e-14, "fractional seconds diff: {:.2e}", diff);
187    }
188
189    #[cfg(feature = "serde")]
190    #[test]
191    fn test_tdb_serde_round_trip() {
192        let test_cases = [
193            TDB::j2000(),
194            TDB::new(0, 0),
195            tdb_from_calendar(2024, 6, 15, 14, 30, 45.123),
196            tdb_from_calendar(1990, 12, 31, 23, 59, 59.999999999),
197        ];
198
199        for original in test_cases {
200            let json = serde_json::to_string(&original).unwrap();
201            let deserialized: TDB = serde_json::from_str(&json).unwrap();
202
203            let jd1_diff =
204                (original.to_julian_date().jd1() - deserialized.to_julian_date().jd1()).abs();
205            let jd2_diff =
206                (original.to_julian_date().jd2() - deserialized.to_julian_date().jd2()).abs();
207            let total_diff =
208                (original.to_julian_date().to_f64() - deserialized.to_julian_date().to_f64()).abs();
209
210            assert!(jd1_diff < 1e-14, "jd1 diff: {:.2e}", jd1_diff);
211            assert!(jd2_diff < 1e-14, "jd2 diff: {:.2e}", jd2_diff);
212            assert!(total_diff < 1e-14, "total diff: {:.2e}", total_diff);
213        }
214    }
215}