celestial_time/scales/
tdb.rs1use crate::constants::UNIX_EPOCH_JD;
33use crate::julian::JulianDate;
34use crate::parsing::parse_iso8601;
35use crate::{TimeError, TimeResult};
36use celestial_core::constants::SECONDS_PER_DAY_F64;
37use std::fmt;
38use std::str::FromStr;
39
40#[derive(Debug, Clone, Copy, PartialEq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct TDB(JulianDate);
47
48impl TDB {
49 pub fn new(seconds: i64, nanos: u32) -> Self {
53 let total_seconds =
54 seconds as f64 + nanos as f64 / celestial_core::constants::NANOSECONDS_PER_SECOND_F64;
55 let jd = JulianDate::from_f64(UNIX_EPOCH_JD + total_seconds / SECONDS_PER_DAY_F64);
56 Self(jd)
57 }
58
59 pub fn from_julian_date(jd: JulianDate) -> Self {
61 Self(jd)
62 }
63
64 pub fn j2000() -> Self {
66 Self(JulianDate::j2000())
67 }
68
69 pub fn to_julian_date(&self) -> JulianDate {
71 self.0
72 }
73
74 pub fn add_seconds(&self, seconds: f64) -> Self {
76 Self(self.0.add_seconds(seconds))
77 }
78
79 pub fn add_days(&self, days: f64) -> Self {
81 Self(self.0.add_days(days))
82 }
83}
84
85impl fmt::Display for TDB {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 write!(f, "TDB {}", self.0)
88 }
89}
90
91impl From<JulianDate> for TDB {
93 fn from(jd: JulianDate) -> Self {
94 Self::from_julian_date(jd)
95 }
96}
97
98impl FromStr for TDB {
102 type Err = TimeError;
103
104 fn from_str(s: &str) -> TimeResult<Self> {
105 let parsed = parse_iso8601(s)?;
106 Ok(Self::from_julian_date(parsed.to_julian_date()))
107 }
108}
109
110pub fn tdb_from_calendar(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> TDB {
115 let jd = JulianDate::from_calendar(year, month, day, hour, minute, second);
116 TDB::from_julian_date(jd)
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::constants::UNIX_EPOCH_JD;
123 use celestial_core::constants::J2000_JD;
124
125 #[test]
126 fn test_tdb_constructors() {
127 assert_eq!(TDB::new(0, 0).to_julian_date().to_f64(), UNIX_EPOCH_JD);
128 assert_eq!(TDB::j2000().to_julian_date().to_f64(), J2000_JD);
129 assert_eq!(
130 tdb_from_calendar(2000, 1, 1, 12, 0, 0.0)
131 .to_julian_date()
132 .to_f64(),
133 J2000_JD
134 );
135 }
136
137 #[test]
138 fn test_tdb_arithmetic() {
139 let tdb = TDB::j2000();
140 assert_eq!(tdb.add_days(1.0).to_julian_date().to_f64(), J2000_JD + 1.0);
141 assert_eq!(
142 tdb.add_seconds(3600.0).to_julian_date().to_f64(),
143 J2000_JD + 1.0 / 24.0
144 );
145 }
146
147 #[test]
148 fn test_tdb_from_julian_date_trait() {
149 let jd = JulianDate::new(J2000_JD, 0.123456789);
150 let tdb_direct = TDB::from_julian_date(jd);
151 let tdb_from_trait: TDB = jd.into();
152
153 assert_eq!(
154 tdb_direct.to_julian_date().jd1(),
155 tdb_from_trait.to_julian_date().jd1()
156 );
157 assert_eq!(
158 tdb_direct.to_julian_date().jd2(),
159 tdb_from_trait.to_julian_date().jd2()
160 );
161 }
162
163 #[test]
164 fn test_tdb_display() {
165 let tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, 0.5));
166 let display_str = format!("{}", tdb);
167
168 assert!(display_str.starts_with("TDB"));
169 assert!(display_str.contains("2451545"));
170 }
171
172 #[test]
173 fn test_tdb_string_parsing() {
174 assert_eq!(
175 TDB::from_str("2000-01-01T12:00:00")
176 .unwrap()
177 .to_julian_date()
178 .to_f64(),
179 TDB::j2000().to_julian_date().to_f64()
180 );
181 assert!(TDB::from_str("invalid-date").is_err());
182
183 let result = TDB::from_str("2000-01-01T12:00:00.123").unwrap();
184 let expected_jd = J2000_JD + 0.123 / SECONDS_PER_DAY_F64;
185 let diff = (result.to_julian_date().to_f64() - expected_jd).abs();
186 assert!(diff < 1e-14, "fractional seconds diff: {:.2e}", diff);
187 }
188
189 #[cfg(feature = "serde")]
190 #[test]
191 fn test_tdb_serde_round_trip() {
192 let test_cases = [
193 TDB::j2000(),
194 TDB::new(0, 0),
195 tdb_from_calendar(2024, 6, 15, 14, 30, 45.123),
196 tdb_from_calendar(1990, 12, 31, 23, 59, 59.999999999),
197 ];
198
199 for original in test_cases {
200 let json = serde_json::to_string(&original).unwrap();
201 let deserialized: TDB = serde_json::from_str(&json).unwrap();
202
203 let jd1_diff =
204 (original.to_julian_date().jd1() - deserialized.to_julian_date().jd1()).abs();
205 let jd2_diff =
206 (original.to_julian_date().jd2() - deserialized.to_julian_date().jd2()).abs();
207 let total_diff =
208 (original.to_julian_date().to_f64() - deserialized.to_julian_date().to_f64()).abs();
209
210 assert!(jd1_diff < 1e-14, "jd1 diff: {:.2e}", jd1_diff);
211 assert!(jd2_diff < 1e-14, "jd2 diff: {:.2e}", jd2_diff);
212 assert!(total_diff < 1e-14, "total diff: {:.2e}", total_diff);
213 }
214 }
215}