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