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, Clone, Copy, PartialEq, Eq)]
63pub struct NonFiniteTimeError;
64
65impl std::fmt::Display for NonFiniteTimeError {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(f, "time value must be finite (not NaN or infinity)")
68 }
69}
70
71impl std::error::Error for NonFiniteTimeError {}
72
73#[repr(transparent)]
83#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
84pub struct Time<S: TimeScale> {
85 quantity: Days,
86 _scale: PhantomData<S>,
87}
88
89impl<S: TimeScale> Time<S> {
90 #[inline]
98 pub const fn new(value: f64) -> Self {
99 Self {
100 quantity: Days::new(value),
101 _scale: PhantomData,
102 }
103 }
104
105 #[inline]
120 pub fn try_new(value: f64) -> Result<Self, NonFiniteTimeError> {
121 if value.is_finite() {
122 Ok(Self::new(value))
123 } else {
124 Err(NonFiniteTimeError)
125 }
126 }
127
128 #[inline]
134 pub const fn from_days(days: Days) -> Self {
135 Self {
136 quantity: days,
137 _scale: PhantomData,
138 }
139 }
140
141 #[inline]
146 pub fn try_from_days(days: Days) -> Result<Self, NonFiniteTimeError> {
147 Self::try_new(days.value())
148 }
149
150 #[inline]
154 pub const fn quantity(&self) -> Days {
155 self.quantity
156 }
157
158 #[inline]
160 pub const fn value(&self) -> f64 {
161 self.quantity.value()
162 }
163
164 #[inline]
166 pub fn julian_day(&self) -> Days {
167 S::to_jd_tt(self.quantity)
168 }
169
170 #[inline]
172 pub fn julian_day_value(&self) -> f64 {
173 self.julian_day().value()
174 }
175
176 #[inline]
178 pub fn from_julian_day(jd: Days) -> Self {
179 Self::from_days(S::from_jd_tt(jd))
180 }
181
182 #[inline]
195 pub fn to<T: TimeScale>(&self) -> Time<T> {
196 Time::<T>::from_julian_day(S::to_jd_tt(self.quantity))
197 }
198
199 pub fn to_utc(&self) -> Option<DateTime<Utc>> {
206 use super::scales::UT;
207 const UNIX_EPOCH_JD: f64 = 2_440_587.5;
208 let jd_ut = self.to::<UT>().quantity();
209 let seconds_since_epoch = (jd_ut - Days::new(UNIX_EPOCH_JD)).to::<Second>().value();
210 let secs = seconds_since_epoch.floor() as i64;
211 let nanos = ((seconds_since_epoch - secs as f64) * 1e9) as u32;
212 DateTime::<Utc>::from_timestamp(secs, nanos)
213 }
214
215 pub fn from_utc(datetime: DateTime<Utc>) -> Self {
221 use super::scales::UT;
222 const UNIX_EPOCH_JD: f64 = 2_440_587.5;
223 let seconds_since_epoch = Seconds::new(datetime.timestamp() as f64);
224 let nanos = Seconds::new(datetime.timestamp_subsec_nanos() as f64 / 1e9);
225 let jd_ut = Days::new(UNIX_EPOCH_JD) + (seconds_since_epoch + nanos).to::<Day>();
226 Time::<UT>::from_days(jd_ut).to::<S>()
227 }
228
229 #[inline]
233 pub const fn min(self, other: Self) -> Self {
234 Self::from_days(self.quantity.min_const(other.quantity))
235 }
236
237 #[inline]
239 pub const fn max(self, other: Self) -> Self {
240 Self::from_days(self.quantity.max_const(other.quantity))
241 }
242
243 #[inline]
245 pub const fn mean(self, other: Self) -> Self {
246 Self::from_days(self.quantity.const_add(other.quantity).const_div(2.0))
247 }
248}
249
250impl<S: TimeScale> std::fmt::Display for Time<S> {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 write!(f, "{} ", S::LABEL)?;
265 std::fmt::Display::fmt(&self.quantity.value(), f)
266 }
267}
268
269#[cfg(feature = "serde")]
272impl<S: TimeScale> Serialize for Time<S> {
273 fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
274 where
275 Ser: Serializer,
276 {
277 serializer.serialize_f64(self.value())
278 }
279}
280
281#[cfg(feature = "serde")]
282impl<'de, S: TimeScale> Deserialize<'de> for Time<S> {
283 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
284 where
285 D: Deserializer<'de>,
286 {
287 let v = f64::deserialize(deserializer)?;
288 if !v.is_finite() {
289 return Err(serde::de::Error::custom(
290 "time value must be finite (not NaN or infinity)",
291 ));
292 }
293 Ok(Self::new(v))
294 }
295}
296
297impl<S: TimeScale> Add<Days> for Time<S> {
300 type Output = Self;
301 #[inline]
302 fn add(self, rhs: Days) -> Self::Output {
303 Self::from_days(self.quantity + rhs)
304 }
305}
306
307impl<S: TimeScale> AddAssign<Days> for Time<S> {
308 #[inline]
309 fn add_assign(&mut self, rhs: Days) {
310 self.quantity += rhs;
311 }
312}
313
314impl<S: TimeScale> Sub<Days> for Time<S> {
315 type Output = Self;
316 #[inline]
317 fn sub(self, rhs: Days) -> Self::Output {
318 Self::from_days(self.quantity - rhs)
319 }
320}
321
322impl<S: TimeScale> SubAssign<Days> for Time<S> {
323 #[inline]
324 fn sub_assign(&mut self, rhs: Days) {
325 self.quantity -= rhs;
326 }
327}
328
329impl<S: TimeScale> Sub for Time<S> {
330 type Output = Days;
331 #[inline]
332 fn sub(self, rhs: Self) -> Self::Output {
333 self.quantity - rhs.quantity
334 }
335}
336
337impl<S: TimeScale> std::ops::Div<Days> for Time<S> {
338 type Output = f64;
339 #[inline]
340 fn div(self, rhs: Days) -> Self::Output {
341 (self.quantity / rhs).simplify().value()
342 }
343}
344
345impl<S: TimeScale> std::ops::Div<f64> for Time<S> {
346 type Output = f64;
347 #[inline]
348 fn div(self, rhs: f64) -> Self::Output {
349 (self.quantity / rhs).value()
350 }
351}
352
353impl<S: TimeScale> From<Days> for Time<S> {
356 #[inline]
357 fn from(days: Days) -> Self {
358 Self::from_days(days)
359 }
360}
361
362impl<S: TimeScale> From<Time<S>> for Days {
363 #[inline]
364 fn from(time: Time<S>) -> Self {
365 time.quantity
366 }
367}
368
369pub trait TimeInstant: Copy + Clone + PartialEq + PartialOrd + Sized {
378 type Duration;
380
381 fn to_utc(&self) -> Option<DateTime<Utc>>;
383
384 fn from_utc(datetime: DateTime<Utc>) -> Self;
386
387 fn difference(&self, other: &Self) -> Self::Duration;
389
390 fn add_duration(&self, duration: Self::Duration) -> Self;
392
393 fn sub_duration(&self, duration: Self::Duration) -> Self;
395}
396
397impl<S: TimeScale> TimeInstant for Time<S> {
398 type Duration = Days;
399
400 #[inline]
401 fn to_utc(&self) -> Option<DateTime<Utc>> {
402 Time::to_utc(self)
403 }
404
405 #[inline]
406 fn from_utc(datetime: DateTime<Utc>) -> Self {
407 Time::from_utc(datetime)
408 }
409
410 #[inline]
411 fn difference(&self, other: &Self) -> Self::Duration {
412 *self - *other
413 }
414
415 #[inline]
416 fn add_duration(&self, duration: Self::Duration) -> Self {
417 *self + duration
418 }
419
420 #[inline]
421 fn sub_duration(&self, duration: Self::Duration) -> Self {
422 *self - duration
423 }
424}
425
426impl TimeInstant for DateTime<Utc> {
427 type Duration = chrono::Duration;
428
429 fn to_utc(&self) -> Option<DateTime<Utc>> {
430 Some(*self)
431 }
432
433 fn from_utc(datetime: DateTime<Utc>) -> Self {
434 datetime
435 }
436
437 fn difference(&self, other: &Self) -> Self::Duration {
438 *self - *other
439 }
440
441 fn add_duration(&self, duration: Self::Duration) -> Self {
442 *self + duration
443 }
444
445 fn sub_duration(&self, duration: Self::Duration) -> Self {
446 *self - duration
447 }
448}
449
450#[cfg(test)]
455mod tests {
456 use super::super::scales::{JD, MJD};
457 use super::*;
458 use chrono::TimeZone;
459
460 #[test]
461 fn test_julian_day_creation() {
462 let jd = Time::<JD>::new(2_451_545.0);
463 assert_eq!(jd.quantity(), Days::new(2_451_545.0));
464 }
465
466 #[test]
467 fn test_jd_utc_roundtrip() {
468 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
470 let jd = Time::<JD>::from_utc(datetime);
471 let back = jd.to_utc().expect("to_utc");
472 let delta_ns =
473 back.timestamp_nanos_opt().unwrap() - datetime.timestamp_nanos_opt().unwrap();
474 assert!(delta_ns.abs() < 1_000, "roundtrip error: {} ns", delta_ns);
475 }
476
477 #[test]
478 fn test_from_utc_applies_delta_t() {
479 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
481 let jd = Time::<JD>::from_utc(datetime);
482 let delta_t_secs = (jd.quantity() - Days::new(2_451_545.0)).to::<Second>();
483 assert!(
484 (delta_t_secs - Seconds::new(63.83)).abs() < Seconds::new(1.0),
485 "ΔT correction = {} s, expected ~63.83 s",
486 delta_t_secs
487 );
488 }
489
490 #[test]
491 fn test_julian_conversions() {
492 let jd = Time::<JD>::J2000 + Days::new(365_250.0);
493 assert!((jd.julian_millennias() - Millennia::new(1.0)).abs() < 1e-12);
494 assert!((jd.julian_centuries() - Centuries::new(10.0)).abs() < Centuries::new(1e-12));
495 assert!((jd.julian_years() - JulianYears::new(1000.0)).abs() < JulianYears::new(1e-9));
496 }
497
498 #[test]
499 fn test_tt_to_tdb_and_min_max() {
500 let jd_tdb = Time::<JD>::tt_to_tdb(Time::<JD>::J2000);
501 assert!((jd_tdb - Time::<JD>::J2000).abs() < 1e-6);
502
503 let earlier = Time::<JD>::J2000;
504 let later = earlier + Days::new(1.0);
505 assert_eq!(earlier.min(later), earlier);
506 assert_eq!(earlier.max(later), later);
507 }
508
509 #[test]
510 fn test_const_min_max() {
511 const A: Time<JD> = Time::<JD>::new(10.0);
512 const B: Time<JD> = Time::<JD>::new(14.0);
513 const MIN: Time<JD> = A.min(B);
514 const MAX: Time<JD> = A.max(B);
515 assert_eq!(MIN.quantity(), Days::new(10.0));
516 assert_eq!(MAX.quantity(), Days::new(14.0));
517 }
518
519 #[test]
520 fn test_mean_and_const_mean() {
521 let a = Time::<JD>::new(10.0);
522 let b = Time::<JD>::new(14.0);
523 assert_eq!(a.mean(b).quantity(), Days::new(12.0));
524 assert_eq!(b.mean(a).quantity(), Days::new(12.0));
525
526 const MID: Time<JD> = Time::<JD>::new(10.0).mean(Time::<JD>::new(14.0));
527 assert_eq!(MID.quantity(), Days::new(12.0));
528 }
529
530 #[test]
531 fn test_into_days() {
532 let jd = Time::<JD>::new(2_451_547.5);
533 let days: Days = jd.into();
534 assert_eq!(days, 2_451_547.5);
535
536 let roundtrip = Time::<JD>::from(days);
537 assert_eq!(roundtrip, jd);
538 }
539
540 #[test]
541 fn test_into_julian_years() {
542 let jd = Time::<JD>::J2000 + Days::new(365.25 * 2.0);
543 let years: JulianYears = jd.into();
544 assert!((years - JulianYears::new(2.0)).abs() < JulianYears::new(1e-12));
545
546 let roundtrip = Time::<JD>::from(years);
547 assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-12));
548 }
549
550 #[test]
551 fn time_has_days_layout() {
552 assert_eq!(std::mem::size_of::<Time<JD>>(), std::mem::size_of::<Days>());
553 assert_eq!(
554 std::mem::align_of::<Time<JD>>(),
555 std::mem::align_of::<Days>()
556 );
557 }
558
559 #[test]
560 fn test_into_centuries() {
561 let jd = Time::<JD>::J2000 + Days::new(36_525.0 * 3.0);
562 let centuries: Centuries = jd.into();
563 assert!((centuries - Centuries::new(3.0)).abs() < Centuries::new(1e-12));
564
565 let roundtrip = Time::<JD>::from(centuries);
566 assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-12));
567 }
568
569 #[test]
570 fn test_into_millennia() {
571 let jd = Time::<JD>::J2000 + Days::new(365_250.0 * 1.5);
572 let millennia: Millennia = jd.into();
573 assert!((millennia - Millennia::new(1.5)).abs() < Millennia::new(1e-12));
574
575 let roundtrip = Time::<JD>::from(millennia);
576 assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-9));
577 }
578
579 #[test]
580 fn test_mjd_creation() {
581 let mjd = Time::<MJD>::new(51_544.5);
582 assert_eq!(mjd.quantity(), Days::new(51_544.5));
583 }
584
585 #[test]
586 fn test_mjd_into_jd() {
587 let mjd = Time::<MJD>::new(51_544.5);
588 let jd: Time<JD> = mjd.into();
589 assert_eq!(jd.quantity(), Days::new(2_451_545.0));
590 }
591
592 #[test]
593 fn test_mjd_utc_roundtrip() {
594 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
595 let mjd = Time::<MJD>::from_utc(datetime);
596 let back = mjd.to_utc().expect("to_utc");
597 let delta_ns =
598 back.timestamp_nanos_opt().unwrap() - datetime.timestamp_nanos_opt().unwrap();
599 assert!(delta_ns.abs() < 1_000, "roundtrip error: {} ns", delta_ns);
600 }
601
602 #[test]
603 fn test_mjd_from_utc_applies_delta_t() {
604 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
606 let mjd = Time::<MJD>::from_utc(datetime);
607 let delta_t_secs = (mjd.quantity() - Days::new(51_544.5)).to::<Second>();
608 assert!(
609 (delta_t_secs - Seconds::new(63.83)).abs() < Seconds::new(1.0),
610 "ΔT correction = {} s, expected ~63.83 s",
611 delta_t_secs
612 );
613 }
614
615 #[test]
616 fn test_mjd_add_days() {
617 let mjd = Time::<MJD>::new(59_000.0);
618 let result = mjd + Days::new(1.5);
619 assert_eq!(result.quantity(), Days::new(59_001.5));
620 }
621
622 #[test]
623 fn test_mjd_sub_days() {
624 let mjd = Time::<MJD>::new(59_000.0);
625 let result = mjd - Days::new(1.5);
626 assert_eq!(result.quantity(), Days::new(58_998.5));
627 }
628
629 #[test]
630 fn test_mjd_sub_mjd() {
631 let mjd1 = Time::<MJD>::new(59_001.0);
632 let mjd2 = Time::<MJD>::new(59_000.0);
633 let diff = mjd1 - mjd2;
634 assert_eq!(diff, 1.0);
635 }
636
637 #[test]
638 fn test_mjd_comparison() {
639 let mjd1 = Time::<MJD>::new(59_000.0);
640 let mjd2 = Time::<MJD>::new(59_001.0);
641 assert!(mjd1 < mjd2);
642 assert!(mjd2 > mjd1);
643 }
644
645 #[test]
646 fn test_display_jd() {
647 let jd = Time::<JD>::new(2_451_545.0);
648 let s = format!("{jd}");
649 assert!(s.contains("Julian Day"));
650 }
651
652 #[test]
653 fn test_try_new_finite() {
654 let jd = Time::<JD>::try_new(2_451_545.0);
655 assert!(jd.is_ok());
656 assert_eq!(jd.unwrap().value(), 2_451_545.0);
657 }
658
659 #[test]
660 fn test_try_new_nan() {
661 assert!(Time::<JD>::try_new(f64::NAN).is_err());
662 }
663
664 #[test]
665 fn test_try_new_infinity() {
666 assert!(Time::<JD>::try_new(f64::INFINITY).is_err());
667 assert!(Time::<JD>::try_new(f64::NEG_INFINITY).is_err());
668 }
669
670 #[test]
671 fn test_try_from_days() {
672 assert!(Time::<JD>::try_from_days(Days::new(100.0)).is_ok());
673 assert!(Time::<JD>::try_from_days(Days::new(f64::NAN)).is_err());
674 }
675
676 #[test]
677 fn test_display_mjd() {
678 let mjd = Time::<MJD>::new(51_544.5);
679 let s = format!("{mjd}");
680 assert!(s.contains("MJD"));
681 }
682
683 #[test]
684 fn test_add_assign_sub_assign() {
685 let mut jd = Time::<JD>::new(2_451_545.0);
686 jd += Days::new(1.0);
687 assert_eq!(jd.quantity(), Days::new(2_451_546.0));
688 jd -= Days::new(0.5);
689 assert_eq!(jd.quantity(), Days::new(2_451_545.5));
690 }
691
692 #[test]
693 fn test_add_years() {
694 let jd = Time::<JD>::new(2_450_000.0);
695 let with_years = jd + Years::new(1.0);
696 let span: Days = with_years - jd;
697 assert!((span - Time::<JD>::JULIAN_YEAR).abs() < Days::new(1e-9));
698 }
699
700 #[test]
701 fn test_div_days_and_f64() {
702 let jd = Time::<JD>::new(100.0);
703 assert!((jd / Days::new(2.0) - 50.0).abs() < 1e-12);
704 assert!((jd / 4.0 - 25.0).abs() < 1e-12);
705 }
706
707 #[test]
708 fn test_to_method_jd_mjd() {
709 let jd = Time::<JD>::new(2_451_545.0);
710 let mjd = jd.to::<MJD>();
711 assert!((mjd.quantity() - Days::new(51_544.5)).abs() < Days::new(1e-10));
712 }
713
714 #[test]
715 fn timeinstant_for_julian_date_handles_arithmetic() {
716 let jd = Time::<JD>::new(2_451_545.0);
717 let other = jd + Days::new(2.0);
718
719 assert_eq!(jd.difference(&other), Days::new(-2.0));
720 assert_eq!(
721 jd.add_duration(Days::new(1.5)).quantity(),
722 Days::new(2_451_546.5)
723 );
724 assert_eq!(
725 other.sub_duration(Days::new(0.5)).quantity(),
726 Days::new(2_451_546.5)
727 );
728 }
729
730 #[test]
731 fn timeinstant_for_modified_julian_date_roundtrips_utc() {
732 let dt = DateTime::from_timestamp(946_684_800, 123_000_000).unwrap(); let mjd = Time::<MJD>::from_utc(dt);
734 let back = mjd.to_utc().expect("mjd to utc");
735
736 assert_eq!(mjd.difference(&mjd), Days::new(0.0));
737 assert_eq!(
738 mjd.add_duration(Days::new(1.0)).quantity(),
739 mjd.quantity() + Days::new(1.0)
740 );
741 assert_eq!(
742 mjd.sub_duration(Days::new(0.5)).quantity(),
743 mjd.quantity() - Days::new(0.5)
744 );
745 let delta_ns = back.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap();
746 assert!(delta_ns.abs() < 10_000, "nanos differ by {}", delta_ns);
747 }
748
749 #[test]
750 fn timeinstant_for_datetime_uses_chrono_durations() {
751 let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
752 let later = Utc.with_ymd_and_hms(2024, 1, 2, 6, 0, 0).unwrap();
753 let diff = later.difference(&base);
754
755 assert_eq!(diff.num_hours(), 30);
756 assert_eq!(
757 base.add_duration(diff + chrono::Duration::hours(6)),
758 later + chrono::Duration::hours(6)
759 );
760 assert_eq!(later.sub_duration(diff), base);
761 assert_eq!(TimeInstant::to_utc(&later), Some(later));
762 }
763
764 #[test]
767 fn test_non_finite_error_display() {
768 let err = NonFiniteTimeError;
769 let msg = format!("{err}");
770 assert!(msg.contains("finite"), "unexpected: {msg}");
771 }
772
773 #[test]
774 fn test_julian_day_and_julian_day_value() {
775 let mjd = Time::<MJD>::new(51_544.5);
777 let jd_days = mjd.julian_day();
778 assert!(
779 (jd_days - Days::new(2_451_545.0)).abs() < Days::new(1e-10),
780 "julian_day mismatch: {jd_days}"
781 );
782 assert!(
783 (mjd.julian_day_value() - 2_451_545.0).abs() < 1e-10,
784 "julian_day_value mismatch: {}",
785 mjd.julian_day_value()
786 );
787 }
788
789 #[test]
790 fn test_timeinstant_trait_to_utc_and_from_utc_for_time() {
791 let jd = Time::<JD>::new(2_451_545.0);
794 let utc: Option<_> = TimeInstant::to_utc(&jd);
795 assert!(utc.is_some());
796 let back: Time<JD> = TimeInstant::from_utc(utc.unwrap());
797 assert!((back.value() - jd.value()).abs() < 1e-6);
798 }
799
800 #[test]
801 fn test_datetime_timeinstant_from_utc() {
802 let dt = DateTime::from_timestamp(0, 0).unwrap();
804 let back: DateTime<Utc> = TimeInstant::from_utc(dt);
805 assert_eq!(back, dt);
806 }
807
808 #[cfg(feature = "serde")]
809 #[test]
810 fn test_serde_serialize_time() {
811 let jd = Time::<JD>::new(2_451_545.0);
812 let json = serde_json::to_string(&jd).unwrap();
813 assert!(json.contains("2451545"), "serialized: {json}");
814 let back: Time<JD> = serde_json::from_str(&json).unwrap();
815 assert_eq!(jd.value(), back.value());
816 }
817
818 #[cfg(feature = "serde")]
819 #[test]
820 fn test_serde_deserialize_nan_rejected() {
821 use serde::{de::IntoDeserializer, Deserialize};
822 let result: Result<Time<JD>, serde::de::value::Error> =
823 Time::<JD>::deserialize(f64::NAN.into_deserializer());
824 assert!(result.is_err());
825 let msg = result.unwrap_err().to_string();
826 assert!(msg.contains("finite"), "unexpected error: {msg}");
827 }
828}