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