tempoch_core/format/
mod.rs1mod time_format;
32pub use time_format::TimeFormat;
33
34pub mod markers;
35pub use markers::{J2000s, Unix, GPS, JD, MJD};
36
37mod traits;
38pub use traits::{FormatForScale, InfallibleFormatForScale};
39
40mod impls;
41
42mod chrono;
43
44pub type JulianDate<S> = crate::model::time::Time<S, JD>;
46pub type ModifiedJulianDate<S> = crate::model::time::Time<S, MJD>;
48pub type J2000Seconds<S> = crate::model::time::Time<S, J2000s>;
50pub type UnixTime = crate::model::time::Time<crate::model::scale::UTC, Unix>;
52pub type GpsTime = crate::model::time::Time<crate::model::scale::TAI, GPS>;
54
55impl<S: crate::model::scale::Scale> From<JulianDate<S>> for crate::Time<S> {
56 #[inline]
57 fn from(value: JulianDate<S>) -> Self {
58 value.to_j2000s()
59 }
60}
61
62impl<S: crate::model::scale::Scale> From<ModifiedJulianDate<S>> for crate::Time<S> {
63 #[inline]
64 fn from(value: ModifiedJulianDate<S>) -> Self {
65 value.to_j2000s()
66 }
67}
68
69impl From<UnixTime> for crate::Time<crate::model::scale::UTC> {
70 #[inline]
71 fn from(value: UnixTime) -> Self {
72 value.to_j2000s()
73 }
74}
75
76impl From<GpsTime> for crate::Time<crate::model::scale::TAI> {
77 #[inline]
78 fn from(value: GpsTime) -> Self {
79 value.to_j2000s()
80 }
81}
82
83#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::earth::context::TimeContext;
89 use crate::model::scale::{TAI, TT, UTC};
90 use crate::model::target::ConversionTarget;
91 use qtty::{Day, Second};
92
93 #[test]
94 fn encoded_time_display_delegates_to_quantity() {
95 let jd = JulianDate::<TT>::new(2_451_545.123_456_789);
96
97 assert_eq!(format!("{jd:.9}"), "2451545.123456789 d");
98 }
99
100 #[test]
101 fn encoded_time_lower_exp_delegates_to_quantity() {
102 let seconds = J2000Seconds::<TT>::new(1_234.5);
103 let formatted = format!("{seconds:.2e}");
104
105 assert_eq!(formatted, format!("{:.2e}", seconds.raw()));
106 }
107
108 #[test]
109 fn encoded_time_upper_exp_delegates_to_quantity() {
110 let seconds = J2000Seconds::<TT>::new(1_234.5);
111 let formatted = format!("{seconds:.2E}");
112
113 assert_eq!(formatted, format!("{:.2E}", seconds.raw()));
114 }
115
116 #[test]
117 fn encoded_time_clone_matches_original() {
118 let jd = JulianDate::<TT>::new(2_451_545.0);
119 let cloned = <JulianDate<TT> as Clone>::clone(&jd);
120 assert_eq!(jd.raw(), cloned.raw());
121 }
122
123 #[test]
124 fn encoded_time_partial_eq() {
125 let a = JulianDate::<TT>::new(2_451_545.0);
126 let b = JulianDate::<TT>::new(2_451_545.0);
127 let c = JulianDate::<TT>::new(2_451_546.0);
128 assert_eq!(a, b);
129 assert_ne!(a, c);
130 }
131
132 #[test]
133 fn encoded_time_quantity_is_alias_for_raw() {
134 let jd = JulianDate::<TT>::new(2_451_545.5);
135 assert_eq!(jd.raw(), jd.quantity());
136 }
137
138 #[test]
139 fn encoded_time_new_accepts_scalar_values() {
140 let jd = JulianDate::<TT>::new(2_460_000.5);
141
142 assert_eq!(jd.raw(), Day::new(2_460_000.5));
143 }
144
145 #[test]
146 fn encoded_time_try_to_time_on_unix() {
147 let ctx = TimeContext::new();
148 let unix = UnixTime::try_new(Second::new(946_727_935.816)).unwrap();
149 let time = unix.to_j2000s();
150 let back = <Unix as crate::format::FormatForScale<UTC>>::try_from_time(time, &ctx).unwrap();
151 assert!((back - Second::new(946_727_935.816)).abs() < Second::new(1e-3));
152 }
153
154 #[test]
155 fn encoded_time_to_infallible_conversion() {
156 let jd = JulianDate::<TT>::new(2_451_545.0);
157 let mjd: ModifiedJulianDate<TT> = jd.to::<MJD>();
158 assert!((mjd.raw().value() - 51_544.5).abs() < 1e-9);
159 }
160
161 #[test]
162 fn encoded_time_try_to_conversion() {
163 let jd = JulianDate::<TT>::new(2_451_545.0);
164 let mjd: ModifiedJulianDate<TT> = jd.try_to::<MJD>().unwrap();
165 assert!((mjd.raw().value() - 51_544.5).abs() < 1e-9);
166 }
167
168 #[test]
169 fn encoded_time_to_with_for_unix() {
170 let ctx = TimeContext::new();
171 let jd = JulianDate::<UTC>::new(2_451_545.0);
172 let unix: UnixTime = jd.to_with::<Unix>(&ctx).unwrap();
173 let unix_sec = unix.try_raw_with(&ctx).unwrap();
174 assert!(unix_sec.value().is_finite());
175 assert!(unix_sec.value() > 9e8 && unix_sec.value() < 1e10);
176 }
177
178 #[test]
179 fn gps_format_roundtrip_through_tai() {
180 let gps_seconds = Second::new(0.0);
181 let time = <GPS as crate::format::InfallibleFormatForScale<TAI>>::into_time(gps_seconds);
182 let back = <GPS as crate::format::InfallibleFormatForScale<TAI>>::from_time(time);
183 assert!((back - gps_seconds).abs() < Second::new(1e-12));
184 }
185
186 #[test]
187 fn gps_encoded_time_to_time_roundtrip() {
188 let gps = GpsTime::new(1_234_567.89);
189 let time = gps.to_j2000s();
190 let back: GpsTime = time.to::<GPS>();
191 assert!((back.raw() - gps.raw()).abs() < Second::new(1e-6));
192 }
193
194 #[test]
195 fn from_encoded_time_into_time() {
196 let jd = JulianDate::<TT>::new(2_451_545.0);
197 let time: crate::model::time::Time<TT> = jd.into();
198 let back: JulianDate<TT> = time.to::<JD>();
199 assert!((back.raw() - Day::new(2_451_545.0)).abs() < Day::new(1e-12));
200 }
201
202 #[test]
203 fn encoded_into_default_time_matches_to_j2000s() {
204 let jd = JulianDate::<TT>::new(2_451_545.25);
205 let mjd = ModifiedJulianDate::<TT>::new(51_545.0);
206 assert_eq!(crate::Time::<TT>::from(jd), jd.to_j2000s());
207 assert_eq!(crate::Time::<TT>::from(mjd), mjd.to_j2000s());
208 let unix = UnixTime::try_new(Second::new(1_700_000_000.0)).unwrap();
209 assert_eq!(crate::Time::<UTC>::from(unix), unix.to_j2000s());
210 let gps = GpsTime::new(100.0);
211 assert_eq!(crate::Time::<TAI>::from(gps), gps.to_j2000s());
212 }
213
214 #[test]
215 fn period_try_new_accepts_encoded_endpoints_via_into() {
216 use crate::Period;
217
218 let jd_a = JulianDate::<TT>::new(2_451_545.0);
219 let jd_b = JulianDate::<TT>::new(2_451_546.0);
220 let from_jd = Period::<TT>::try_new(jd_a, jd_b).unwrap();
221 let explicit_jd = Period::<TT>::try_new(jd_a.to_j2000s(), jd_b.to_j2000s()).unwrap();
222 assert_eq!(from_jd, explicit_jd);
223
224 let mjd_a = ModifiedJulianDate::<TT>::new(51_544.0);
225 let mjd_b = ModifiedJulianDate::<TT>::new(51_545.0);
226 let from_mjd = Period::<TT>::try_new(mjd_a, mjd_b).unwrap();
227 let explicit_mjd = Period::<TT>::try_new(mjd_a.to_j2000s(), mjd_b.to_j2000s()).unwrap();
228 assert_eq!(from_mjd, explicit_mjd);
229 }
230
231 #[test]
232 fn infallible_conversion_target_for_j2000s() {
233 let jd = JulianDate::<TT>::new(2_451_545.0);
234 let time = jd.to_j2000s();
235 let j2k: J2000Seconds<TT> = time.to::<J2000s>();
236 assert!((j2k.raw().value()).abs() < 1e-6);
237 }
238
239 #[test]
240 fn conversion_target_try_convert_for_j2000s() {
241 let jd = JulianDate::<TT>::new(2_451_545.0);
242 let time = jd.to_j2000s();
243 let j2k: J2000Seconds<TT> = time.try_to::<J2000s>().unwrap();
244 assert!((j2k.raw().value()).abs() < 1e-6);
245 }
246
247 #[test]
248 fn conversion_target_try_convert_for_jd() {
249 let mjd = ModifiedJulianDate::<TT>::new(51_544.0);
250 let time = mjd.to_j2000s();
251 let jd: JulianDate<TT> = JD::try_convert(time).unwrap();
252 assert!((jd.raw().value() - 2_451_544.5).abs() < 1e-9);
253 }
254
255 #[test]
256 fn conversion_target_try_convert_for_mjd() {
257 let jd = JulianDate::<TT>::new(2_451_545.0);
258 let time = jd.to_j2000s();
259 let mjd: ModifiedJulianDate<TT> = MJD::try_convert(time).unwrap();
260 assert!((mjd.raw().value() - 51_544.5).abs() < 1e-9);
261 }
262
263 #[test]
264 fn gps_conversion_target_try_convert() {
265 let jd = JulianDate::<TT>::new(2_451_545.0);
266 let time = jd.to_j2000s();
267 let gps: GpsTime = GPS::try_convert(time).unwrap();
268 assert!(gps.raw().is_finite());
269 }
270
271 #[test]
272 fn unix_context_conversion_target() {
273 let ctx = TimeContext::new();
274 let jd = JulianDate::<UTC>::new(2_451_545.0);
275 let utc_time = jd.to_j2000s();
276 let unix: crate::model::time::Time<UTC, Unix> =
277 <Unix as crate::model::target::ContextConversionTarget<
278 UTC,
279 crate::format::J2000s,
280 >>::convert_with(utc_time, &ctx)
281 .unwrap();
282 let unix_sec = unix.try_raw_with(&ctx).unwrap();
283 assert!(unix_sec.value().is_finite());
284 assert!(unix_sec.value() > 9e8 && unix_sec.value() < 1e10);
285 }
286
287 #[test]
288 fn debug_includes_format_and_scale() {
289 let jd = JulianDate::<TT>::new(2_451_545.0);
290 let dbg = format!("{jd:?}");
291 assert!(dbg.contains("TT"), "debug should contain scale name");
292 assert!(dbg.contains("JD"), "debug should contain format name");
293 }
294
295 #[test]
296 fn jd_on_tt_and_utc_are_distinct_types() {
297 fn accept_tt(x: JulianDate<TT>) -> Day {
298 x.raw()
299 }
300 fn accept_utc(x: JulianDate<UTC>) -> Day {
301 x.raw()
302 }
303
304 let tt_jd = JulianDate::<TT>::new(2_451_545.0);
305 let utc_jd = JulianDate::<UTC>::new(2_451_545.0);
306
307 let _ = accept_tt(tt_jd);
308 let _ = accept_utc(utc_jd);
309 }
310
311 #[test]
312 fn format_names_are_correct() {
313 assert_eq!(JD::NAME, "JD");
314 assert_eq!(MJD::NAME, "MJD");
315 assert_eq!(J2000s::NAME, "J2000s");
316 assert_eq!(Unix::NAME, "Unix");
317 assert_eq!(GPS::NAME, "GPS");
318 }
319
320 #[test]
321 fn chrono_helpers_with_explicit_context_cover_tt_encoded_formats() {
322 let ctx = TimeContext::new().allow_pre_definition_utc();
323 let dt =
324 ::chrono::DateTime::<::chrono::Utc>::from_timestamp(946_728_123, 250_000_000).unwrap();
325
326 let jd = JulianDate::<TT>::try_from_chrono_with(dt, &ctx).unwrap();
327 let mjd = ModifiedJulianDate::<TT>::from_chrono_with(dt, &ctx);
328 let j2k = J2000Seconds::<TT>::from(dt);
329
330 let jd_back = jd.try_to_chrono_with(&ctx).unwrap();
331 let mjd_back = mjd.to_chrono_with(&ctx).unwrap();
332 let j2k_back = j2k.to_chrono().unwrap();
333
334 assert!(
335 (jd_back.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap()).abs()
336 < 50_000
337 );
338 assert!(
339 (mjd_back.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap()).abs()
340 < 50_000
341 );
342 assert!(
343 (j2k_back.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap()).abs()
344 < 50_000
345 );
346 }
347
348 #[test]
349 fn format_trait_impls_cover_j2000_jd_mjd_and_gps() {
350 let ctx = TimeContext::new();
351 let tt = J2000Seconds::<TT>::new(123.5);
352 let tai = crate::Time::<TAI>::new(456.75);
353
354 let j2000 = <J2000s as crate::format::FormatForScale<TT>>::try_from_time(tt, &ctx).unwrap();
355 assert_eq!(j2000, tt.raw());
356 assert_eq!(
357 <J2000s as crate::format::FormatForScale<TT>>::try_into_time(j2000, &ctx).unwrap(),
358 tt
359 );
360
361 let jd = <JD as crate::format::FormatForScale<TT>>::try_from_time(tt, &ctx).unwrap();
362 let mjd = <MJD as crate::format::FormatForScale<TT>>::try_from_time(tt, &ctx).unwrap();
363 assert!(
364 (<JD as crate::format::FormatForScale<TT>>::try_into_time(jd, &ctx)
365 .unwrap()
366 .to_j2000s()
367 .raw()
368 .value()
369 - tt.raw().value())
370 .abs()
371 < 1e-4
372 );
373 assert!(
374 (<MJD as crate::format::FormatForScale<TT>>::try_into_time(mjd, &ctx)
375 .unwrap()
376 .to_j2000s()
377 .raw()
378 .value()
379 - tt.raw().value())
380 .abs()
381 < 1e-4
382 );
383
384 let gps = <GPS as crate::format::FormatForScale<TAI>>::try_from_time(tai, &ctx).unwrap();
385 assert_eq!(
386 <GPS as crate::format::FormatForScale<TAI>>::try_into_time(gps, &ctx).unwrap(),
387 tai.to::<GPS>()
388 );
389 }
390}