celestial_time/scales/
tcg.rs1use crate::constants::UNIX_EPOCH_JD;
43use crate::julian::JulianDate;
44use crate::parsing::parse_iso8601;
45use crate::{TimeError, TimeResult};
46use celestial_core::constants::SECONDS_PER_DAY_F64;
47use std::fmt;
48use std::str::FromStr;
49
50#[derive(Debug, Clone, Copy, PartialEq)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub struct TCG(JulianDate);
58
59impl TCG {
60 pub fn new(seconds: i64, nanos: u32) -> Self {
65 let total_seconds =
66 seconds as f64 + nanos as f64 / celestial_core::constants::NANOSECONDS_PER_SECOND_F64;
67 let jd = JulianDate::from_f64(UNIX_EPOCH_JD + total_seconds / SECONDS_PER_DAY_F64);
68 Self(jd)
69 }
70
71 pub fn from_julian_date(jd: JulianDate) -> Self {
73 Self(jd)
74 }
75
76 pub fn j2000() -> Self {
78 Self(JulianDate::j2000())
79 }
80
81 pub fn to_julian_date(&self) -> JulianDate {
83 self.0
84 }
85
86 pub fn add_seconds(&self, seconds: f64) -> Self {
88 Self(self.0.add_seconds(seconds))
89 }
90
91 pub fn add_days(&self, days: f64) -> Self {
93 Self(self.0.add_days(days))
94 }
95}
96
97impl fmt::Display for TCG {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 write!(f, "TCG {}", self.0)
100 }
101}
102
103impl From<JulianDate> for TCG {
105 fn from(jd: JulianDate) -> Self {
106 Self::from_julian_date(jd)
107 }
108}
109
110impl FromStr for TCG {
115 type Err = TimeError;
116
117 fn from_str(s: &str) -> TimeResult<Self> {
118 let parsed = parse_iso8601(s)?;
119 Ok(Self::from_julian_date(parsed.to_julian_date()))
120 }
121}
122
123pub fn tcg_from_calendar(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> TCG {
128 let jd = JulianDate::from_calendar(year, month, day, hour, minute, second);
129 TCG::from_julian_date(jd)
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::constants::UNIX_EPOCH_JD;
136 use celestial_core::constants::J2000_JD;
137
138 #[test]
139 fn test_tcg_constructors() {
140 assert_eq!(TCG::new(0, 0).to_julian_date().to_f64(), UNIX_EPOCH_JD);
141 assert_eq!(TCG::j2000().to_julian_date().to_f64(), J2000_JD);
142 assert_eq!(
143 tcg_from_calendar(2000, 1, 1, 12, 0, 0.0)
144 .to_julian_date()
145 .to_f64(),
146 J2000_JD
147 );
148
149 let jd = JulianDate::new(J2000_JD, 0.123456789);
150 let tcg_direct = TCG::from_julian_date(jd);
151 let tcg_from_trait: TCG = jd.into();
152 assert_eq!(
153 tcg_direct.to_julian_date().jd1(),
154 tcg_from_trait.to_julian_date().jd1()
155 );
156 assert_eq!(
157 tcg_direct.to_julian_date().jd2(),
158 tcg_from_trait.to_julian_date().jd2()
159 );
160 }
161
162 #[test]
163 fn test_tcg_arithmetic() {
164 let tcg = TCG::j2000();
165 assert_eq!(tcg.add_days(1.0).to_julian_date().to_f64(), J2000_JD + 1.0);
166 assert_eq!(
167 tcg.add_seconds(3600.0).to_julian_date().to_f64(),
168 J2000_JD + 1.0 / 24.0
169 );
170 }
171
172 #[test]
173 fn test_tcg_string_parsing() {
174 assert_eq!(
175 TCG::from_str("2000-01-01T12:00:00")
176 .unwrap()
177 .to_julian_date()
178 .to_f64(),
179 TCG::j2000().to_julian_date().to_f64()
180 );
181
182 let result = TCG::from_str("2000-01-01T12:00:00.123").unwrap();
183 let expected_jd = J2000_JD + 0.123 / SECONDS_PER_DAY_F64;
184 let diff = (result.to_julian_date().to_f64() - expected_jd).abs();
185 assert!(diff < 1e-14, "fractional seconds diff: {:.2e}", diff);
186
187 assert!(TCG::from_str("invalid-date").is_err());
188 }
189
190 #[test]
191 fn test_tcg_display() {
192 let display_str = format!("{}", TCG::from_julian_date(JulianDate::new(J2000_JD, 0.5)));
193 assert!(display_str.starts_with("TCG"));
194 assert!(display_str.contains("2451545"));
195 }
196
197 #[cfg(feature = "serde")]
198 #[test]
199 fn test_tcg_serde_round_trip() {
200 let test_cases = [
201 ("J2000", TCG::j2000()),
202 ("Unix epoch", TCG::new(0, 0)),
203 (
204 "Modern date",
205 tcg_from_calendar(2024, 6, 15, 14, 30, 45.123),
206 ),
207 (
208 "High precision",
209 tcg_from_calendar(1990, 12, 31, 23, 59, 59.999999999),
210 ),
211 ];
212
213 for (name, original) in test_cases {
214 let json = serde_json::to_string(&original).unwrap();
215 let deserialized: TCG = serde_json::from_str(&json).unwrap();
216
217 let jd1_diff =
218 (original.to_julian_date().jd1() - deserialized.to_julian_date().jd1()).abs();
219 let jd2_diff =
220 (original.to_julian_date().jd2() - deserialized.to_julian_date().jd2()).abs();
221 let total_diff =
222 (original.to_julian_date().to_f64() - deserialized.to_julian_date().to_f64()).abs();
223
224 assert!(jd1_diff < 1e-14, "{}: jd1 diff {:.2e}", name, jd1_diff);
225 assert!(jd2_diff < 1e-14, "{}: jd2 diff {:.2e}", name, jd2_diff);
226 assert!(
227 total_diff < 1e-14,
228 "{}: total diff {:.2e}",
229 name,
230 total_diff
231 );
232 }
233 }
234}