celestial_time/scales/
tt.rs1use 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#[derive(Debug, Clone, Copy, PartialEq)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
56pub struct TT(JulianDate);
57
58impl TT {
59 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 pub fn from_julian_date(jd: JulianDate) -> Self {
72 Self(jd)
73 }
74
75 pub fn from_julian_date_raw(jd1: f64, jd2: f64) -> Self {
80 Self(JulianDate::new(jd1, jd2))
81 }
82
83 pub fn j2000() -> Self {
88 Self(JulianDate::j2000())
89 }
90
91 pub fn to_julian_date(&self) -> JulianDate {
93 self.0
94 }
95
96 pub fn add_seconds(&self, seconds: f64) -> Self {
98 Self(self.0.add_seconds(seconds))
99 }
100
101 pub fn add_days(&self, days: f64) -> Self {
103 Self(self.0.add_days(days))
104 }
105
106 pub fn from_jd(jd: f64) -> TimeResult<Self> {
110 Ok(Self(JulianDate::from_f64(jd)))
111 }
112
113 pub fn julian_year(&self) -> f64 {
117 2000.0 + (self.0.to_f64() - J2000_JD) / 365.25
118 }
119
120 pub fn centuries_since_j2000(&self) -> f64 {
125 (self.0.to_f64() - J2000_JD) / celestial_core::constants::DAYS_PER_JULIAN_CENTURY
126 }
127}
128
129impl fmt::Display for TT {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 write!(f, "TT {}", self.0)
133 }
134}
135
136impl From<JulianDate> for TT {
138 fn from(jd: JulianDate) -> Self {
139 Self::from_julian_date(jd)
140 }
141}
142
143impl 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
155pub 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}