Skip to main content

celestial_time/scales/
tcb.rs

1//! Barycentric Coordinate Time (TCB) representation.
2//!
3//! TCB is the coordinate time for the barycentric reference frame, as defined by the IAU.
4//! It ticks faster than TDB by approximately 1.55e-8 due to gravitational time dilation
5//! (Earth sits in the Sun's gravitational well).
6//!
7//! # Relationship to TDB
8//!
9//! TCB and TDB are related by a linear transformation plus periodic terms:
10//!
11//! ```text
12//! TCB - TDB = L_B * (JD_TCB - T_0) * 86400
13//! ```
14//!
15//! Where:
16//! - L_B = 1.550519768e-8 (IAU 2006 Resolution B3)
17//! - T_0 = 2443144.5003725 (TCB-TDB epoch, 1977 Jan 1.0 TAI)
18//!
19//! # Usage
20//!
21//! ```
22//! use celestial_time::{JulianDate, TCB};
23//!
24//! // Create from Julian Date
25//! let tcb = TCB::from_julian_date(JulianDate::j2000());
26//!
27//! // Create from calendar
28//! use celestial_time::scales::tcb::tcb_from_calendar;
29//! let tcb = tcb_from_calendar(2000, 1, 1, 12, 0, 0.0);
30//!
31//! // Parse from ISO 8601
32//! let tcb: TCB = "2000-01-01T12:00:00".parse().unwrap();
33//! ```
34//!
35//! # When to Use TCB
36//!
37//! TCB is the natural time coordinate for barycentric calculations (solar system dynamics,
38//! pulsar timing, VLBI). For most terrestrial applications, TDB is more practical since
39//! it stays close to TT.
40
41use crate::constants::UNIX_EPOCH_JD;
42use crate::julian::JulianDate;
43use crate::parsing::parse_iso8601;
44use crate::{TimeError, TimeResult};
45use celestial_core::constants::SECONDS_PER_DAY_F64;
46use std::fmt;
47use std::str::FromStr;
48
49/// Barycentric Coordinate Time.
50///
51/// Wraps a Julian Date interpreted in the TCB time scale. TCB is the proper time
52/// for a clock at the solar system barycenter, far from gravitational sources.
53#[derive(Debug, Clone, Copy, PartialEq)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub struct TCB(JulianDate);
56
57impl TCB {
58    /// Creates TCB from Unix timestamp components.
59    ///
60    /// Interprets the given seconds and nanoseconds as elapsed since Unix epoch
61    /// (1970-01-01T00:00:00) in the TCB scale.
62    pub fn new(seconds: i64, nanos: u32) -> Self {
63        let total_seconds =
64            seconds as f64 + nanos as f64 / celestial_core::constants::NANOSECONDS_PER_SECOND_F64;
65        let jd = JulianDate::from_f64(UNIX_EPOCH_JD + total_seconds / SECONDS_PER_DAY_F64);
66        Self(jd)
67    }
68
69    /// Creates TCB from a Julian Date.
70    pub fn from_julian_date(jd: JulianDate) -> Self {
71        Self(jd)
72    }
73
74    /// Returns TCB at the J2000.0 epoch (2000-01-01T12:00:00).
75    pub fn j2000() -> Self {
76        Self(JulianDate::j2000())
77    }
78
79    /// Returns the underlying Julian Date.
80    pub fn to_julian_date(&self) -> JulianDate {
81        self.0
82    }
83
84    /// Returns a new TCB advanced by the given seconds.
85    pub fn add_seconds(&self, seconds: f64) -> Self {
86        Self(self.0.add_seconds(seconds))
87    }
88
89    /// Returns a new TCB advanced by the given days.
90    pub fn add_days(&self, days: f64) -> Self {
91        Self(self.0.add_days(days))
92    }
93}
94
95impl fmt::Display for TCB {
96    /// Formats as "TCB <julian_date>".
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        write!(f, "TCB {}", self.0)
99    }
100}
101
102impl From<JulianDate> for TCB {
103    /// Converts a Julian Date to TCB.
104    fn from(jd: JulianDate) -> Self {
105        Self::from_julian_date(jd)
106    }
107}
108
109impl FromStr for TCB {
110    type Err = TimeError;
111
112    /// Parses an ISO 8601 datetime string as TCB.
113    ///
114    /// Accepts formats like "2000-01-01T12:00:00" or "2000-01-01T12:00:00.123".
115    fn from_str(s: &str) -> TimeResult<Self> {
116        let parsed = parse_iso8601(s)?;
117        Ok(Self::from_julian_date(parsed.to_julian_date()))
118    }
119}
120
121/// Creates TCB from calendar components.
122///
123/// Converts the given Gregorian calendar date and time to TCB. No time scale
124/// corrections are applied; the calendar date is interpreted directly as TCB.
125pub fn tcb_from_calendar(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> TCB {
126    let jd = JulianDate::from_calendar(year, month, day, hour, minute, second);
127    TCB::from_julian_date(jd)
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::constants::UNIX_EPOCH_JD;
134    use celestial_core::constants::J2000_JD;
135
136    #[test]
137    fn test_tcb_construction() {
138        let test_cases: [(&str, TCB, f64); 3] = [
139            ("new(0, 0) -> Unix epoch", TCB::new(0, 0), UNIX_EPOCH_JD),
140            ("j2000() -> J2000_JD", TCB::j2000(), J2000_JD),
141            (
142                "calendar J2000 -> J2000_JD",
143                tcb_from_calendar(2000, 1, 1, 12, 0, 0.0),
144                J2000_JD,
145            ),
146        ];
147
148        for (name, tcb, expected_jd) in test_cases {
149            assert_eq!(tcb.to_julian_date().to_f64(), expected_jd, "{}", name);
150        }
151    }
152
153    #[test]
154    fn test_tcb_arithmetic() {
155        let tcb = TCB::j2000();
156
157        assert_eq!(tcb.add_days(1.0).to_julian_date().to_f64(), J2000_JD + 1.0);
158        assert_eq!(
159            tcb.add_seconds(3600.0).to_julian_date().to_f64(),
160            J2000_JD + 1.0 / 24.0
161        );
162    }
163
164    #[cfg(feature = "serde")]
165    #[test]
166    fn test_tcb_serde_round_trip() {
167        let test_cases = [
168            TCB::j2000(),
169            TCB::new(0, 0),
170            tcb_from_calendar(2024, 6, 15, 14, 30, 45.123),
171            tcb_from_calendar(1990, 12, 31, 23, 59, 59.999999999),
172        ];
173
174        for original in test_cases {
175            let json = serde_json::to_string(&original).unwrap();
176            let deserialized: TCB = serde_json::from_str(&json).unwrap();
177
178            let diff =
179                (original.to_julian_date().to_f64() - deserialized.to_julian_date().to_f64()).abs();
180            assert!(diff < 1e-14, "serde precision loss: {:.2e}", diff);
181        }
182    }
183
184    #[test]
185    fn test_tcb_display() {
186        let tcb = TCB::from_julian_date(JulianDate::new(J2000_JD, 0.5));
187        let s = format!("{}", tcb);
188
189        assert!(s.starts_with("TCB"));
190        assert!(s.contains("2451545"));
191    }
192
193    #[test]
194    fn test_tcb_from_julian_date_trait() {
195        let jd = JulianDate::new(J2000_JD, 0.123456789);
196        let tcb_direct = TCB::from_julian_date(jd);
197        let tcb_trait: TCB = jd.into();
198
199        assert_eq!(tcb_direct.to_julian_date(), tcb_trait.to_julian_date());
200    }
201
202    #[test]
203    fn test_tcb_string_parsing() {
204        let result = TCB::from_str("2000-01-01T12:00:00").unwrap();
205        assert_eq!(result.to_julian_date().to_f64(), J2000_JD);
206
207        let result = TCB::from_str("2000-01-01T12:00:00.123").unwrap();
208        let expected_jd = J2000_JD + 0.123 / SECONDS_PER_DAY_F64;
209        let diff = (result.to_julian_date().to_f64() - expected_jd).abs();
210        assert!(diff < 1e-14, "fractional seconds diff: {:.2e}", diff);
211
212        assert!(TCB::from_str("invalid-date").is_err());
213    }
214}