celestial_time/scales/conversions/
tai_tt.rs1use super::{ToTAI, ToTCG, ToTT};
49use crate::constants::TT_TAI_OFFSET;
50use crate::scales::{TAI, TCG, TT};
51use crate::TimeResult;
52use celestial_core::constants::SECONDS_PER_DAY_F64;
53
54impl ToTAI for TAI {
56 fn to_tai(&self) -> TimeResult<TAI> {
57 Ok(*self)
58 }
59}
60
61impl ToTT for TT {
63 fn to_tt(&self) -> TimeResult<TT> {
64 Ok(*self)
65 }
66}
67
68impl ToTT for TAI {
73 fn to_tt(&self) -> TimeResult<TT> {
74 let tai_jd = self.to_julian_date();
75 let dtat = TT_TAI_OFFSET / SECONDS_PER_DAY_F64;
76
77 let jd1_raw = tai_jd.jd1().to_bits();
78 let jd2_raw = tai_jd.jd2().to_bits();
79 let jd1_magnitude = jd1_raw & 0x7FFFFFFFFFFFFFFF;
80 let jd2_magnitude = jd2_raw & 0x7FFFFFFFFFFFFFFF;
81 let (tt_jd1, tt_jd2) = if jd1_magnitude > jd2_magnitude {
82 (tai_jd.jd1(), tai_jd.jd2() + dtat)
83 } else {
84 (tai_jd.jd1() + dtat, tai_jd.jd2())
85 };
86
87 Ok(TT::from_julian_date_raw(tt_jd1, tt_jd2))
88 }
89}
90
91impl ToTAI for TT {
96 fn to_tai(&self) -> TimeResult<TAI> {
97 let tt_jd = self.to_julian_date();
98 let dtat = TT_TAI_OFFSET / SECONDS_PER_DAY_F64;
99
100 let jd1_raw = tt_jd.jd1().to_bits();
101 let jd2_raw = tt_jd.jd2().to_bits();
102 let jd1_magnitude = jd1_raw & 0x7FFFFFFFFFFFFFFF;
103 let jd2_magnitude = jd2_raw & 0x7FFFFFFFFFFFFFFF;
104 let (tai_jd1, tai_jd2) = if jd1_magnitude > jd2_magnitude {
105 (tt_jd.jd1(), tt_jd.jd2() - dtat)
106 } else {
107 (tt_jd.jd1() - dtat, tt_jd.jd2())
108 };
109
110 Ok(TAI::from_julian_date_raw(tai_jd1, tai_jd2))
111 }
112}
113
114impl ToTCG for TAI {
118 fn to_tcg(&self) -> TimeResult<TCG> {
119 self.to_tt()?.to_tcg()
120 }
121}
122
123impl ToTAI for TCG {
127 fn to_tai(&self) -> TimeResult<TAI> {
128 self.to_tt()?.to_tai()
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::JulianDate;
136 use celestial_core::constants::J2000_JD;
137
138 #[test]
139 fn test_identity_conversions() {
140 let tai = TAI::from_julian_date(JulianDate::new(J2000_JD, 0.999999999999999));
141 let identity_tai = tai.to_tai().unwrap();
142
143 assert_eq!(
144 tai.to_julian_date().jd1(),
145 identity_tai.to_julian_date().jd1(),
146 "TAI identity conversion should preserve JD1"
147 );
148 assert_eq!(
149 tai.to_julian_date().jd2(),
150 identity_tai.to_julian_date().jd2(),
151 "TAI identity conversion should preserve JD2"
152 );
153
154 let tt = TT::from_julian_date(JulianDate::new(J2000_JD, 0.999999999999999));
155 let identity_tt = tt.to_tt().unwrap();
156
157 assert_eq!(
158 tt.to_julian_date().jd1(),
159 identity_tt.to_julian_date().jd1(),
160 "TT identity conversion should preserve JD1"
161 );
162 assert_eq!(
163 tt.to_julian_date().jd2(),
164 identity_tt.to_julian_date().jd2(),
165 "TT identity conversion should preserve JD2"
166 );
167 }
168
169 #[test]
170 fn test_tai_tt_offset_32_184_seconds() {
171 let test_dates = [
172 (J2000_JD, "J2000.0"),
173 (2455197.5, "2010-01-01"),
174 (2459580.5, "2022-01-01"),
175 (2440587.5, "1970-01-01 Unix epoch"),
176 ];
177
178 for (jd, description) in test_dates {
179 let tai = TAI::from_julian_date(JulianDate::new(jd, 0.0));
180 let tt = tai.to_tt().unwrap();
181
182 let tai_jd = tai.to_julian_date();
183 let tt_jd = tt.to_julian_date();
184
185 let offset_days = (tt_jd.jd1() - tai_jd.jd1()) + (tt_jd.jd2() - tai_jd.jd2());
186 let offset_seconds = offset_days * SECONDS_PER_DAY_F64;
187
188 assert_eq!(
189 offset_seconds, 32.184,
190 "{}: TAI->TT offset must be exactly 32.184 seconds",
191 description
192 );
193
194 let tt = TT::from_julian_date(JulianDate::new(jd, 0.0));
195 let tai = tt.to_tai().unwrap();
196
197 let tt_jd = tt.to_julian_date();
198 let tai_jd = tai.to_julian_date();
199
200 let offset_days = (tt_jd.jd1() - tai_jd.jd1()) + (tt_jd.jd2() - tai_jd.jd2());
201 let offset_seconds = offset_days * SECONDS_PER_DAY_F64;
202
203 assert_eq!(
204 offset_seconds, 32.184,
205 "{}: TT->TAI means TT is 32.184 seconds ahead",
206 description
207 );
208 }
209 }
210
211 #[test]
212 fn test_tai_tt_round_trip_precision() {
213 let test_jd2_values = [0.0, 0.5, 0.123456789012345, -0.123456789012345, 0.987654321];
214
215 for jd2 in test_jd2_values {
216 let original_tai = TAI::from_julian_date(JulianDate::new(J2000_JD, jd2));
217 let tt = original_tai.to_tt().unwrap();
218 let round_trip_tai = tt.to_tai().unwrap();
219
220 assert_eq!(
221 original_tai.to_julian_date().jd1(),
222 round_trip_tai.to_julian_date().jd1(),
223 "TAI->TT->TAI JD1 must be exact for jd2={}",
224 jd2
225 );
226 assert_eq!(
227 original_tai.to_julian_date().jd2(),
228 round_trip_tai.to_julian_date().jd2(),
229 "TAI->TT->TAI JD2 must be exact for jd2={}",
230 jd2
231 );
232
233 let original_tt = TT::from_julian_date(JulianDate::new(J2000_JD, jd2));
234 let tai = original_tt.to_tai().unwrap();
235 let round_trip_tt = tai.to_tt().unwrap();
236
237 assert_eq!(
238 original_tt.to_julian_date().jd1(),
239 round_trip_tt.to_julian_date().jd1(),
240 "TT->TAI->TT JD1 must be exact for jd2={}",
241 jd2
242 );
243 assert_eq!(
244 original_tt.to_julian_date().jd2(),
245 round_trip_tt.to_julian_date().jd2(),
246 "TT->TAI->TT JD2 must be exact for jd2={}",
247 jd2
248 );
249 }
250
251 let alt_tai = TAI::from_julian_date(JulianDate::new(0.5, J2000_JD));
252 let alt_tt = alt_tai.to_tt().unwrap();
253 let alt_round_trip = alt_tt.to_tai().unwrap();
254
255 assert_eq!(
256 alt_tai.to_julian_date().jd1(),
257 alt_round_trip.to_julian_date().jd1(),
258 "Alternate split TAI->TT->TAI JD1 must be exact"
259 );
260 assert_eq!(
261 alt_tai.to_julian_date().jd2(),
262 alt_round_trip.to_julian_date().jd2(),
263 "Alternate split TAI->TT->TAI JD2 must be exact"
264 );
265 }
266
267 #[test]
268 fn test_tai_tcg_chain_round_trip() {
269 const TOLERANCE_DAYS: f64 = 1e-14; let test_jd2_values = [0.0, 0.123456789, 0.5, -0.25];
275
276 for jd2 in test_jd2_values {
277 let original_tai = TAI::from_julian_date(JulianDate::new(J2000_JD, jd2));
278 let tcg = original_tai.to_tcg().unwrap();
279 let round_trip_tai = tcg.to_tai().unwrap();
280
281 assert_eq!(
282 original_tai.to_julian_date().jd1(),
283 round_trip_tai.to_julian_date().jd1(),
284 "TAI->TCG->TAI JD1 must be exact for jd2={}",
285 jd2
286 );
287 let jd2_diff =
288 (original_tai.to_julian_date().jd2() - round_trip_tai.to_julian_date().jd2()).abs();
289 assert!(
290 jd2_diff <= TOLERANCE_DAYS,
291 "TAI->TCG->TAI JD2 difference {} exceeds tolerance {} for jd2={}",
292 jd2_diff,
293 TOLERANCE_DAYS,
294 jd2
295 );
296
297 let original_tcg = TCG::from_julian_date(JulianDate::new(J2000_JD, jd2));
298 let tai = original_tcg.to_tai().unwrap();
299 let round_trip_tcg = tai.to_tcg().unwrap();
300
301 assert_eq!(
302 original_tcg.to_julian_date().jd1(),
303 round_trip_tcg.to_julian_date().jd1(),
304 "TCG->TAI->TCG JD1 must be exact for jd2={}",
305 jd2
306 );
307 let jd2_diff =
308 (original_tcg.to_julian_date().jd2() - round_trip_tcg.to_julian_date().jd2()).abs();
309 assert!(
310 jd2_diff <= TOLERANCE_DAYS,
311 "TCG->TAI->TCG JD2 difference {} exceeds tolerance {} for jd2={}",
312 jd2_diff,
313 TOLERANCE_DAYS,
314 jd2
315 );
316 }
317 }
318}