celestial_time/scales/
tai.rs1use crate::constants::UNIX_EPOCH_JD;
45use crate::julian::JulianDate;
46use crate::parsing::parse_iso8601;
47use crate::{TimeError, TimeResult};
48use celestial_core::constants::SECONDS_PER_DAY_F64;
49use std::fmt;
50use std::str::FromStr;
51
52#[derive(Debug, Clone, Copy, PartialEq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct TAI(JulianDate);
59
60impl TAI {
61 pub fn new(seconds: i64, nanos: u32) -> Self {
66 let total_seconds =
67 seconds as f64 + nanos as f64 / celestial_core::constants::NANOSECONDS_PER_SECOND_F64;
68 let jd = JulianDate::from_f64(UNIX_EPOCH_JD + total_seconds / SECONDS_PER_DAY_F64);
69 Self(jd)
70 }
71
72 pub fn from_julian_date(jd: JulianDate) -> Self {
74 Self(jd)
75 }
76
77 pub fn from_julian_date_raw(jd1: f64, jd2: f64) -> Self {
82 Self(JulianDate::new(jd1, jd2))
83 }
84
85 pub fn j2000() -> Self {
89 Self(JulianDate::j2000())
90 }
91
92 pub fn to_julian_date(&self) -> JulianDate {
94 self.0
95 }
96
97 pub fn add_seconds(&self, seconds: f64) -> Self {
99 Self(self.0.add_seconds(seconds))
100 }
101
102 pub fn add_days(&self, days: f64) -> Self {
104 Self(self.0.add_days(days))
105 }
106}
107
108impl fmt::Display for TAI {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 write!(f, "TAI {}", self.0)
112 }
113}
114
115impl From<JulianDate> for TAI {
116 fn from(jd: JulianDate) -> Self {
117 Self::from_julian_date(jd)
118 }
119}
120
121impl FromStr for TAI {
122 type Err = TimeError;
123
124 fn from_str(s: &str) -> TimeResult<Self> {
129 let parsed = parse_iso8601(s)?;
130 Ok(Self::from_julian_date(parsed.to_julian_date()))
131 }
132}
133
134pub fn tai_from_calendar(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> TAI {
148 let jd = JulianDate::from_calendar(year, month, day, hour, minute, second);
149 TAI::from_julian_date(jd)
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::constants::UNIX_EPOCH_JD;
156 use celestial_core::constants::J2000_JD;
157
158 #[test]
159 fn test_tai_constructors() {
160 assert_eq!(TAI::new(0, 0).to_julian_date().to_f64(), UNIX_EPOCH_JD);
161 assert_eq!(TAI::j2000().to_julian_date().to_f64(), J2000_JD);
162 assert_eq!(
163 tai_from_calendar(2000, 1, 1, 12, 0, 0.0)
164 .to_julian_date()
165 .to_f64(),
166 J2000_JD
167 );
168
169 let jd = JulianDate::new(J2000_JD, 0.123456789);
170 let tai_direct = TAI::from_julian_date(jd);
171 let tai_from_trait: TAI = jd.into();
172 assert_eq!(
173 tai_direct.to_julian_date().jd1(),
174 tai_from_trait.to_julian_date().jd1()
175 );
176 assert_eq!(
177 tai_direct.to_julian_date().jd2(),
178 tai_from_trait.to_julian_date().jd2()
179 );
180 }
181
182 #[test]
183 fn test_tai_arithmetic() {
184 let tai = TAI::j2000();
185 assert_eq!(tai.add_days(1.0).to_julian_date().to_f64(), J2000_JD + 1.0);
186 assert_eq!(
187 tai.add_seconds(3600.0).to_julian_date().to_f64(),
188 J2000_JD + 1.0 / 24.0
189 );
190 }
191
192 #[test]
193 fn test_tai_display() {
194 let display_str = format!("{}", TAI::from_julian_date(JulianDate::new(J2000_JD, 0.5)));
195 assert!(display_str.starts_with("TAI"));
196 assert!(display_str.contains("2451545"));
197 }
198
199 #[test]
200 fn test_tai_string_parsing() {
201 assert_eq!(
202 TAI::from_str("2000-01-01T12:00:00")
203 .unwrap()
204 .to_julian_date()
205 .to_f64(),
206 TAI::j2000().to_julian_date().to_f64()
207 );
208
209 let result = TAI::from_str("2000-01-01T12:00:00.123").unwrap();
210 let expected_jd = J2000_JD + 0.123 / SECONDS_PER_DAY_F64;
211 let diff = (result.to_julian_date().to_f64() - expected_jd).abs();
212 assert!(diff < 1e-14, "fractional seconds diff: {:.2e}", diff);
213
214 assert!(TAI::from_str("invalid-date").is_err());
215 }
216
217 #[cfg(feature = "serde")]
218 #[test]
219 fn test_tai_serde_round_trip() {
220 let test_cases = [
221 TAI::j2000(),
222 TAI::new(0, 0),
223 tai_from_calendar(2024, 6, 15, 14, 30, 45.123),
224 tai_from_calendar(1990, 12, 31, 23, 59, 59.999999999),
225 ];
226
227 for original in test_cases {
228 let json = serde_json::to_string(&original).unwrap();
229 let deserialized: TAI = serde_json::from_str(&json).unwrap();
230
231 let total_diff =
232 (original.to_julian_date().to_f64() - deserialized.to_julian_date().to_f64()).abs();
233 assert!(
234 total_diff < 1e-14,
235 "serde precision loss: {:.2e}",
236 total_diff
237 );
238 }
239 }
240}