1use chrono::{DateTime, Utc};
17use qtty::*;
18use std::marker::PhantomData;
19use std::ops::{Add, AddAssign, Sub, SubAssign};
20
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
23
24pub trait TimeScale: Copy + Clone + std::fmt::Debug + PartialEq + PartialOrd + 'static {
43 const LABEL: &'static str;
45
46 fn to_jd_tt(value: Days) -> Days;
48
49 fn from_jd_tt(jd_tt: Days) -> Days;
51}
52
53#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
63pub struct Time<S: TimeScale> {
64 quantity: Days,
65 _scale: PhantomData<S>,
66}
67
68impl<S: TimeScale> Time<S> {
69 #[inline]
73 pub const fn new(value: f64) -> Self {
74 Self {
75 quantity: Days::new(value),
76 _scale: PhantomData,
77 }
78 }
79
80 #[inline]
82 pub const fn from_days(days: Days) -> Self {
83 Self {
84 quantity: days,
85 _scale: PhantomData,
86 }
87 }
88
89 #[inline]
93 pub const fn quantity(&self) -> Days {
94 self.quantity
95 }
96
97 #[inline]
99 pub const fn value(&self) -> f64 {
100 self.quantity.value()
101 }
102
103 #[inline]
105 pub fn julian_day(&self) -> Days {
106 S::to_jd_tt(self.quantity)
107 }
108
109 #[inline]
111 pub fn julian_day_value(&self) -> f64 {
112 self.julian_day().value()
113 }
114
115 #[inline]
117 pub fn from_julian_day(jd: Days) -> Self {
118 Self::from_days(S::from_jd_tt(jd))
119 }
120
121 #[inline]
134 pub fn to<T: TimeScale>(&self) -> Time<T> {
135 Time::<T>::from_julian_day(S::to_jd_tt(self.quantity))
136 }
137
138 pub fn to_utc(&self) -> Option<DateTime<Utc>> {
145 use super::scales::UT;
146 const UNIX_EPOCH_JD: f64 = 2_440_587.5;
147 let jd_ut = self.to::<UT>().quantity();
148 let seconds_since_epoch = (jd_ut - Days::new(UNIX_EPOCH_JD)).to::<Second>().value();
149 let secs = seconds_since_epoch.floor() as i64;
150 let nanos = ((seconds_since_epoch - secs as f64) * 1e9) as u32;
151 DateTime::<Utc>::from_timestamp(secs, nanos)
152 }
153
154 pub fn from_utc(datetime: DateTime<Utc>) -> Self {
160 use super::scales::UT;
161 const UNIX_EPOCH_JD: f64 = 2_440_587.5;
162 let seconds_since_epoch = Seconds::new(datetime.timestamp() as f64);
163 let nanos = Seconds::new(datetime.timestamp_subsec_nanos() as f64 / 1e9);
164 let jd_ut = Days::new(UNIX_EPOCH_JD) + (seconds_since_epoch + nanos).to::<Day>();
165 Time::<UT>::from_days(jd_ut).to::<S>()
166 }
167
168 #[inline]
172 pub const fn min(self, other: Self) -> Self {
173 Self::from_days(self.quantity.min_const(other.quantity))
174 }
175
176 #[inline]
178 pub const fn max(self, other: Self) -> Self {
179 Self::from_days(self.quantity.max_const(other.quantity))
180 }
181
182 #[inline]
184 pub const fn mean(self, other: Self) -> Self {
185 Self::from_days(self.quantity.const_add(other.quantity).const_div(2.0))
186 }
187}
188
189impl<S: TimeScale> std::fmt::Display for Time<S> {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 write!(f, "{} {}", S::LABEL, self.quantity)
198 }
199}
200
201#[cfg(feature = "serde")]
204impl<S: TimeScale> Serialize for Time<S> {
205 fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
206 where
207 Ser: Serializer,
208 {
209 serializer.serialize_f64(self.value())
210 }
211}
212
213#[cfg(feature = "serde")]
214impl<'de, S: TimeScale> Deserialize<'de> for Time<S> {
215 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
216 where
217 D: Deserializer<'de>,
218 {
219 let v = f64::deserialize(deserializer)?;
220 Ok(Self::new(v))
221 }
222}
223
224impl<S: TimeScale> Add<Days> for Time<S> {
227 type Output = Self;
228 #[inline]
229 fn add(self, rhs: Days) -> Self::Output {
230 Self::from_days(self.quantity + rhs)
231 }
232}
233
234impl<S: TimeScale> AddAssign<Days> for Time<S> {
235 #[inline]
236 fn add_assign(&mut self, rhs: Days) {
237 self.quantity += rhs;
238 }
239}
240
241impl<S: TimeScale> Sub<Days> for Time<S> {
242 type Output = Self;
243 #[inline]
244 fn sub(self, rhs: Days) -> Self::Output {
245 Self::from_days(self.quantity - rhs)
246 }
247}
248
249impl<S: TimeScale> SubAssign<Days> for Time<S> {
250 #[inline]
251 fn sub_assign(&mut self, rhs: Days) {
252 self.quantity -= rhs;
253 }
254}
255
256impl<S: TimeScale> Sub for Time<S> {
257 type Output = Days;
258 #[inline]
259 fn sub(self, rhs: Self) -> Self::Output {
260 self.quantity - rhs.quantity
261 }
262}
263
264impl<S: TimeScale> std::ops::Div<Days> for Time<S> {
265 type Output = f64;
266 #[inline]
267 fn div(self, rhs: Days) -> Self::Output {
268 (self.quantity / rhs).simplify().value()
269 }
270}
271
272impl<S: TimeScale> std::ops::Div<f64> for Time<S> {
273 type Output = f64;
274 #[inline]
275 fn div(self, rhs: f64) -> Self::Output {
276 (self.quantity / rhs).value()
277 }
278}
279
280impl<S: TimeScale> From<Days> for Time<S> {
283 #[inline]
284 fn from(days: Days) -> Self {
285 Self::from_days(days)
286 }
287}
288
289impl<S: TimeScale> From<Time<S>> for Days {
290 #[inline]
291 fn from(time: Time<S>) -> Self {
292 time.quantity
293 }
294}
295
296pub trait TimeInstant: Copy + Clone + PartialEq + PartialOrd + Sized {
305 type Duration;
307
308 fn to_utc(&self) -> Option<DateTime<Utc>>;
310
311 fn from_utc(datetime: DateTime<Utc>) -> Self;
313
314 fn difference(&self, other: &Self) -> Self::Duration;
316
317 fn add_duration(&self, duration: Self::Duration) -> Self;
319
320 fn sub_duration(&self, duration: Self::Duration) -> Self;
322}
323
324impl<S: TimeScale> TimeInstant for Time<S> {
325 type Duration = Days;
326
327 #[inline]
328 fn to_utc(&self) -> Option<DateTime<Utc>> {
329 Time::to_utc(self)
330 }
331
332 #[inline]
333 fn from_utc(datetime: DateTime<Utc>) -> Self {
334 Time::from_utc(datetime)
335 }
336
337 #[inline]
338 fn difference(&self, other: &Self) -> Self::Duration {
339 *self - *other
340 }
341
342 #[inline]
343 fn add_duration(&self, duration: Self::Duration) -> Self {
344 *self + duration
345 }
346
347 #[inline]
348 fn sub_duration(&self, duration: Self::Duration) -> Self {
349 *self - duration
350 }
351}
352
353impl TimeInstant for DateTime<Utc> {
354 type Duration = chrono::Duration;
355
356 fn to_utc(&self) -> Option<DateTime<Utc>> {
357 Some(*self)
358 }
359
360 fn from_utc(datetime: DateTime<Utc>) -> Self {
361 datetime
362 }
363
364 fn difference(&self, other: &Self) -> Self::Duration {
365 *self - *other
366 }
367
368 fn add_duration(&self, duration: Self::Duration) -> Self {
369 *self + duration
370 }
371
372 fn sub_duration(&self, duration: Self::Duration) -> Self {
373 *self - duration
374 }
375}
376
377#[cfg(test)]
382mod tests {
383 use super::super::scales::{JD, MJD};
384 use super::*;
385 use chrono::TimeZone;
386
387 #[test]
388 fn test_julian_day_creation() {
389 let jd = Time::<JD>::new(2_451_545.0);
390 assert_eq!(jd.quantity(), Days::new(2_451_545.0));
391 }
392
393 #[test]
394 fn test_jd_utc_roundtrip() {
395 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
397 let jd = Time::<JD>::from_utc(datetime);
398 let back = jd.to_utc().expect("to_utc");
399 let delta_ns =
400 back.timestamp_nanos_opt().unwrap() - datetime.timestamp_nanos_opt().unwrap();
401 assert!(delta_ns.abs() < 1_000, "roundtrip error: {} ns", delta_ns);
402 }
403
404 #[test]
405 fn test_from_utc_applies_delta_t() {
406 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
408 let jd = Time::<JD>::from_utc(datetime);
409 let delta_t_secs = (jd.quantity() - Days::new(2_451_545.0)).to::<Second>();
410 assert!(
411 (delta_t_secs - Seconds::new(63.83)).abs() < Seconds::new(1.0),
412 "ΔT correction = {} s, expected ~63.83 s",
413 delta_t_secs
414 );
415 }
416
417 #[test]
418 fn test_julian_conversions() {
419 let jd = Time::<JD>::J2000 + Days::new(365_250.0);
420 assert!((jd.julian_millennias() - Millennia::new(1.0)).abs() < 1e-12);
421 assert!((jd.julian_centuries() - Centuries::new(10.0)).abs() < Centuries::new(1e-12));
422 assert!((jd.julian_years() - JulianYears::new(1000.0)).abs() < JulianYears::new(1e-9));
423 }
424
425 #[test]
426 fn test_tt_to_tdb_and_min_max() {
427 let jd_tdb = Time::<JD>::tt_to_tdb(Time::<JD>::J2000);
428 assert!((jd_tdb - Time::<JD>::J2000).abs() < 1e-6);
429
430 let earlier = Time::<JD>::J2000;
431 let later = earlier + Days::new(1.0);
432 assert_eq!(earlier.min(later), earlier);
433 assert_eq!(earlier.max(later), later);
434 }
435
436 #[test]
437 fn test_const_min_max() {
438 const A: Time<JD> = Time::<JD>::new(10.0);
439 const B: Time<JD> = Time::<JD>::new(14.0);
440 const MIN: Time<JD> = A.min(B);
441 const MAX: Time<JD> = A.max(B);
442 assert_eq!(MIN.quantity(), Days::new(10.0));
443 assert_eq!(MAX.quantity(), Days::new(14.0));
444 }
445
446 #[test]
447 fn test_mean_and_const_mean() {
448 let a = Time::<JD>::new(10.0);
449 let b = Time::<JD>::new(14.0);
450 assert_eq!(a.mean(b).quantity(), Days::new(12.0));
451 assert_eq!(b.mean(a).quantity(), Days::new(12.0));
452
453 const MID: Time<JD> = Time::<JD>::new(10.0).mean(Time::<JD>::new(14.0));
454 assert_eq!(MID.quantity(), Days::new(12.0));
455 }
456
457 #[test]
458 fn test_into_days() {
459 let jd = Time::<JD>::new(2_451_547.5);
460 let days: Days = jd.into();
461 assert_eq!(days, 2_451_547.5);
462
463 let roundtrip = Time::<JD>::from(days);
464 assert_eq!(roundtrip, jd);
465 }
466
467 #[test]
468 fn test_into_julian_years() {
469 let jd = Time::<JD>::J2000 + Days::new(365.25 * 2.0);
470 let years: JulianYears = jd.into();
471 assert!((years - JulianYears::new(2.0)).abs() < JulianYears::new(1e-12));
472
473 let roundtrip = Time::<JD>::from(years);
474 assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-12));
475 }
476
477 #[test]
478 fn test_into_centuries() {
479 let jd = Time::<JD>::J2000 + Days::new(36_525.0 * 3.0);
480 let centuries: Centuries = jd.into();
481 assert!((centuries - Centuries::new(3.0)).abs() < Centuries::new(1e-12));
482
483 let roundtrip = Time::<JD>::from(centuries);
484 assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-12));
485 }
486
487 #[test]
488 fn test_into_millennia() {
489 let jd = Time::<JD>::J2000 + Days::new(365_250.0 * 1.5);
490 let millennia: Millennia = jd.into();
491 assert!((millennia - Millennia::new(1.5)).abs() < Millennia::new(1e-12));
492
493 let roundtrip = Time::<JD>::from(millennia);
494 assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-9));
495 }
496
497 #[test]
498 fn test_mjd_creation() {
499 let mjd = Time::<MJD>::new(51_544.5);
500 assert_eq!(mjd.quantity(), Days::new(51_544.5));
501 }
502
503 #[test]
504 fn test_mjd_into_jd() {
505 let mjd = Time::<MJD>::new(51_544.5);
506 let jd: Time<JD> = mjd.into();
507 assert_eq!(jd.quantity(), Days::new(2_451_545.0));
508 }
509
510 #[test]
511 fn test_mjd_utc_roundtrip() {
512 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
513 let mjd = Time::<MJD>::from_utc(datetime);
514 let back = mjd.to_utc().expect("to_utc");
515 let delta_ns =
516 back.timestamp_nanos_opt().unwrap() - datetime.timestamp_nanos_opt().unwrap();
517 assert!(delta_ns.abs() < 1_000, "roundtrip error: {} ns", delta_ns);
518 }
519
520 #[test]
521 fn test_mjd_from_utc_applies_delta_t() {
522 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
524 let mjd = Time::<MJD>::from_utc(datetime);
525 let delta_t_secs = (mjd.quantity() - Days::new(51_544.5)).to::<Second>();
526 assert!(
527 (delta_t_secs - Seconds::new(63.83)).abs() < Seconds::new(1.0),
528 "ΔT correction = {} s, expected ~63.83 s",
529 delta_t_secs
530 );
531 }
532
533 #[test]
534 fn test_mjd_add_days() {
535 let mjd = Time::<MJD>::new(59_000.0);
536 let result = mjd + Days::new(1.5);
537 assert_eq!(result.quantity(), Days::new(59_001.5));
538 }
539
540 #[test]
541 fn test_mjd_sub_days() {
542 let mjd = Time::<MJD>::new(59_000.0);
543 let result = mjd - Days::new(1.5);
544 assert_eq!(result.quantity(), Days::new(58_998.5));
545 }
546
547 #[test]
548 fn test_mjd_sub_mjd() {
549 let mjd1 = Time::<MJD>::new(59_001.0);
550 let mjd2 = Time::<MJD>::new(59_000.0);
551 let diff = mjd1 - mjd2;
552 assert_eq!(diff, 1.0);
553 }
554
555 #[test]
556 fn test_mjd_comparison() {
557 let mjd1 = Time::<MJD>::new(59_000.0);
558 let mjd2 = Time::<MJD>::new(59_001.0);
559 assert!(mjd1 < mjd2);
560 assert!(mjd2 > mjd1);
561 }
562
563 #[test]
564 fn test_display_jd() {
565 let jd = Time::<JD>::new(2_451_545.0);
566 let s = format!("{jd}");
567 assert!(s.contains("Julian Day"));
568 }
569
570 #[test]
571 fn test_display_mjd() {
572 let mjd = Time::<MJD>::new(51_544.5);
573 let s = format!("{mjd}");
574 assert!(s.contains("MJD"));
575 }
576
577 #[test]
578 fn test_add_assign_sub_assign() {
579 let mut jd = Time::<JD>::new(2_451_545.0);
580 jd += Days::new(1.0);
581 assert_eq!(jd.quantity(), Days::new(2_451_546.0));
582 jd -= Days::new(0.5);
583 assert_eq!(jd.quantity(), Days::new(2_451_545.5));
584 }
585
586 #[test]
587 fn test_add_years() {
588 let jd = Time::<JD>::new(2_450_000.0);
589 let with_years = jd + Years::new(1.0);
590 let span: Days = with_years - jd;
591 assert!((span - Time::<JD>::JULIAN_YEAR).abs() < Days::new(1e-9));
592 }
593
594 #[test]
595 fn test_div_days_and_f64() {
596 let jd = Time::<JD>::new(100.0);
597 assert!((jd / Days::new(2.0) - 50.0).abs() < 1e-12);
598 assert!((jd / 4.0 - 25.0).abs() < 1e-12);
599 }
600
601 #[test]
602 fn test_to_method_jd_mjd() {
603 let jd = Time::<JD>::new(2_451_545.0);
604 let mjd = jd.to::<MJD>();
605 assert!((mjd.quantity() - Days::new(51_544.5)).abs() < Days::new(1e-10));
606 }
607
608 #[test]
609 fn timeinstant_for_julian_date_handles_arithmetic() {
610 let jd = Time::<JD>::new(2_451_545.0);
611 let other = jd + Days::new(2.0);
612
613 assert_eq!(jd.difference(&other), Days::new(-2.0));
614 assert_eq!(
615 jd.add_duration(Days::new(1.5)).quantity(),
616 Days::new(2_451_546.5)
617 );
618 assert_eq!(
619 other.sub_duration(Days::new(0.5)).quantity(),
620 Days::new(2_451_546.5)
621 );
622 }
623
624 #[test]
625 fn timeinstant_for_modified_julian_date_roundtrips_utc() {
626 let dt = DateTime::from_timestamp(946_684_800, 123_000_000).unwrap(); let mjd = Time::<MJD>::from_utc(dt);
628 let back = mjd.to_utc().expect("mjd to utc");
629
630 assert_eq!(mjd.difference(&mjd), Days::new(0.0));
631 assert_eq!(
632 mjd.add_duration(Days::new(1.0)).quantity(),
633 mjd.quantity() + Days::new(1.0)
634 );
635 assert_eq!(
636 mjd.sub_duration(Days::new(0.5)).quantity(),
637 mjd.quantity() - Days::new(0.5)
638 );
639 let delta_ns = back.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap();
640 assert!(delta_ns.abs() < 10_000, "nanos differ by {}", delta_ns);
641 }
642
643 #[test]
644 fn timeinstant_for_datetime_uses_chrono_durations() {
645 let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
646 let later = Utc.with_ymd_and_hms(2024, 1, 2, 6, 0, 0).unwrap();
647 let diff = later.difference(&base);
648
649 assert_eq!(diff.num_hours(), 30);
650 assert_eq!(
651 base.add_duration(diff + chrono::Duration::hours(6)),
652 later + chrono::Duration::hours(6)
653 );
654 assert_eq!(later.sub_duration(diff), base);
655 assert_eq!(TimeInstant::to_utc(&later), Some(later));
656 }
657}