Skip to main content

celestial_time/scales/
tcg.rs

1//! Geocentric Coordinate Time (TCG) time scale.
2//!
3//! TCG is the proper time of a clock at rest at the geocenter, free from Earth's
4//! gravitational potential. It runs faster than TT by approximately 22 microseconds
5//! per year due to gravitational time dilation.
6//!
7//! # Background
8//!
9//! TCG was introduced by the IAU in 1991 as the coordinate time for the Geocentric
10//! Celestial Reference System (GCRS). While TT is adjusted to match the rate of
11//! proper time on Earth's geoid, TCG ticks at the rate of a clock experiencing
12//! no gravitational potential.
13//!
14//! The relationship between TCG and TT is defined by IAU Resolution B1.9 (2000):
15//!
16//! ```text
17//! TCG - TT = L_G * (JD_TT - T_0) * 86400
18//!
19//! where:
20//!   L_G = 6.969290134e-10 (defining constant)
21//!   T_0 = 2443144.5003725 (TCG/TT coincidence epoch, 1977-01-01 00:00:32.184)
22//! ```
23//!
24//! # Usage
25//!
26//! ```
27//! use celestial_time::{JulianDate, TCG};
28//! use celestial_time::scales::tcg_from_calendar;
29//!
30//! let tcg = TCG::j2000();
31//! let jd = tcg.to_julian_date();
32//!
33//! let tcg_cal = tcg_from_calendar(2000, 1, 1, 12, 0, 0.0);
34//! ```
35//!
36//! # Precision
37//!
38//! TCG values use split Julian Date storage internally. The struct methods preserve
39//! full f64 precision through all arithmetic operations. Conversions to/from TT
40//! maintain nanosecond accuracy.
41
42use crate::constants::UNIX_EPOCH_JD;
43use crate::julian::JulianDate;
44use crate::parsing::parse_iso8601;
45use crate::{TimeError, TimeResult};
46use celestial_core::constants::SECONDS_PER_DAY_F64;
47use std::fmt;
48use std::str::FromStr;
49
50/// Geocentric Coordinate Time.
51///
52/// Wraps a `JulianDate` representing an instant in the TCG time scale.
53/// TCG is the coordinate time for the Geocentric Celestial Reference System,
54/// running ~6.97e-10 faster than TT (about 22 microseconds per year).
55#[derive(Debug, Clone, Copy, PartialEq)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub struct TCG(JulianDate);
58
59impl TCG {
60    /// Creates a TCG instant from Unix timestamp components.
61    ///
62    /// Converts seconds and nanoseconds since 1970-01-01 00:00:00 to TCG.
63    /// Note: This assumes the Unix timestamp is already in the TCG scale.
64    pub fn new(seconds: i64, nanos: u32) -> Self {
65        let total_seconds =
66            seconds as f64 + nanos as f64 / celestial_core::constants::NANOSECONDS_PER_SECOND_F64;
67        let jd = JulianDate::from_f64(UNIX_EPOCH_JD + total_seconds / SECONDS_PER_DAY_F64);
68        Self(jd)
69    }
70
71    /// Creates a TCG instant from a Julian Date.
72    pub fn from_julian_date(jd: JulianDate) -> Self {
73        Self(jd)
74    }
75
76    /// Returns the J2000.0 epoch (2000-01-01 12:00:00) in TCG.
77    pub fn j2000() -> Self {
78        Self(JulianDate::j2000())
79    }
80
81    /// Returns the underlying Julian Date.
82    pub fn to_julian_date(&self) -> JulianDate {
83        self.0
84    }
85
86    /// Adds seconds to this TCG instant, returning a new TCG.
87    pub fn add_seconds(&self, seconds: f64) -> Self {
88        Self(self.0.add_seconds(seconds))
89    }
90
91    /// Adds days to this TCG instant, returning a new TCG.
92    pub fn add_days(&self, days: f64) -> Self {
93        Self(self.0.add_days(days))
94    }
95}
96
97impl fmt::Display for TCG {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        write!(f, "TCG {}", self.0)
100    }
101}
102
103/// Conversion from JulianDate to TCG.
104impl From<JulianDate> for TCG {
105    fn from(jd: JulianDate) -> Self {
106        Self::from_julian_date(jd)
107    }
108}
109
110/// Parses ISO 8601 formatted strings into TCG.
111///
112/// Accepts standard date-time formats like "2000-01-01T12:00:00".
113/// Fractional seconds are supported.
114impl FromStr for TCG {
115    type Err = TimeError;
116
117    fn from_str(s: &str) -> TimeResult<Self> {
118        let parsed = parse_iso8601(s)?;
119        Ok(Self::from_julian_date(parsed.to_julian_date()))
120    }
121}
122
123/// Creates a TCG instant from calendar date components.
124///
125/// Uses proleptic Gregorian calendar. No leap second or time zone handling;
126/// the values are interpreted directly as TCG coordinates.
127pub fn tcg_from_calendar(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> TCG {
128    let jd = JulianDate::from_calendar(year, month, day, hour, minute, second);
129    TCG::from_julian_date(jd)
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::constants::UNIX_EPOCH_JD;
136    use celestial_core::constants::J2000_JD;
137
138    #[test]
139    fn test_tcg_constructors() {
140        assert_eq!(TCG::new(0, 0).to_julian_date().to_f64(), UNIX_EPOCH_JD);
141        assert_eq!(TCG::j2000().to_julian_date().to_f64(), J2000_JD);
142        assert_eq!(
143            tcg_from_calendar(2000, 1, 1, 12, 0, 0.0)
144                .to_julian_date()
145                .to_f64(),
146            J2000_JD
147        );
148
149        let jd = JulianDate::new(J2000_JD, 0.123456789);
150        let tcg_direct = TCG::from_julian_date(jd);
151        let tcg_from_trait: TCG = jd.into();
152        assert_eq!(
153            tcg_direct.to_julian_date().jd1(),
154            tcg_from_trait.to_julian_date().jd1()
155        );
156        assert_eq!(
157            tcg_direct.to_julian_date().jd2(),
158            tcg_from_trait.to_julian_date().jd2()
159        );
160    }
161
162    #[test]
163    fn test_tcg_arithmetic() {
164        let tcg = TCG::j2000();
165        assert_eq!(tcg.add_days(1.0).to_julian_date().to_f64(), J2000_JD + 1.0);
166        assert_eq!(
167            tcg.add_seconds(3600.0).to_julian_date().to_f64(),
168            J2000_JD + 1.0 / 24.0
169        );
170    }
171
172    #[test]
173    fn test_tcg_string_parsing() {
174        assert_eq!(
175            TCG::from_str("2000-01-01T12:00:00")
176                .unwrap()
177                .to_julian_date()
178                .to_f64(),
179            TCG::j2000().to_julian_date().to_f64()
180        );
181
182        let result = TCG::from_str("2000-01-01T12:00:00.123").unwrap();
183        let expected_jd = J2000_JD + 0.123 / SECONDS_PER_DAY_F64;
184        let diff = (result.to_julian_date().to_f64() - expected_jd).abs();
185        assert!(diff < 1e-14, "fractional seconds diff: {:.2e}", diff);
186
187        assert!(TCG::from_str("invalid-date").is_err());
188    }
189
190    #[test]
191    fn test_tcg_display() {
192        let display_str = format!("{}", TCG::from_julian_date(JulianDate::new(J2000_JD, 0.5)));
193        assert!(display_str.starts_with("TCG"));
194        assert!(display_str.contains("2451545"));
195    }
196
197    #[cfg(feature = "serde")]
198    #[test]
199    fn test_tcg_serde_round_trip() {
200        let test_cases = [
201            ("J2000", TCG::j2000()),
202            ("Unix epoch", TCG::new(0, 0)),
203            (
204                "Modern date",
205                tcg_from_calendar(2024, 6, 15, 14, 30, 45.123),
206            ),
207            (
208                "High precision",
209                tcg_from_calendar(1990, 12, 31, 23, 59, 59.999999999),
210            ),
211        ];
212
213        for (name, original) in test_cases {
214            let json = serde_json::to_string(&original).unwrap();
215            let deserialized: TCG = serde_json::from_str(&json).unwrap();
216
217            let jd1_diff =
218                (original.to_julian_date().jd1() - deserialized.to_julian_date().jd1()).abs();
219            let jd2_diff =
220                (original.to_julian_date().jd2() - deserialized.to_julian_date().jd2()).abs();
221            let total_diff =
222                (original.to_julian_date().to_f64() - deserialized.to_julian_date().to_f64()).abs();
223
224            assert!(jd1_diff < 1e-14, "{}: jd1 diff {:.2e}", name, jd1_diff);
225            assert!(jd2_diff < 1e-14, "{}: jd2 diff {:.2e}", name, jd2_diff);
226            assert!(
227                total_diff < 1e-14,
228                "{}: total diff {:.2e}",
229                name,
230                total_diff
231            );
232        }
233    }
234}