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