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, self.quantity)
258 }
259}
260
261#[cfg(feature = "serde")]
264impl<S: TimeScale> Serialize for Time<S> {
265 fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
266 where
267 Ser: Serializer,
268 {
269 serializer.serialize_f64(self.value())
270 }
271}
272
273#[cfg(feature = "serde")]
274impl<'de, S: TimeScale> Deserialize<'de> for Time<S> {
275 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
276 where
277 D: Deserializer<'de>,
278 {
279 let v = f64::deserialize(deserializer)?;
280 if !v.is_finite() {
281 return Err(serde::de::Error::custom(
282 "time value must be finite (not NaN or infinity)",
283 ));
284 }
285 Ok(Self::new(v))
286 }
287}
288
289impl<S: TimeScale> Add<Days> for Time<S> {
292 type Output = Self;
293 #[inline]
294 fn add(self, rhs: Days) -> Self::Output {
295 Self::from_days(self.quantity + rhs)
296 }
297}
298
299impl<S: TimeScale> AddAssign<Days> for Time<S> {
300 #[inline]
301 fn add_assign(&mut self, rhs: Days) {
302 self.quantity += rhs;
303 }
304}
305
306impl<S: TimeScale> Sub<Days> for Time<S> {
307 type Output = Self;
308 #[inline]
309 fn sub(self, rhs: Days) -> Self::Output {
310 Self::from_days(self.quantity - rhs)
311 }
312}
313
314impl<S: TimeScale> SubAssign<Days> for Time<S> {
315 #[inline]
316 fn sub_assign(&mut self, rhs: Days) {
317 self.quantity -= rhs;
318 }
319}
320
321impl<S: TimeScale> Sub for Time<S> {
322 type Output = Days;
323 #[inline]
324 fn sub(self, rhs: Self) -> Self::Output {
325 self.quantity - rhs.quantity
326 }
327}
328
329impl<S: TimeScale> std::ops::Div<Days> for Time<S> {
330 type Output = f64;
331 #[inline]
332 fn div(self, rhs: Days) -> Self::Output {
333 (self.quantity / rhs).simplify().value()
334 }
335}
336
337impl<S: TimeScale> std::ops::Div<f64> for Time<S> {
338 type Output = f64;
339 #[inline]
340 fn div(self, rhs: f64) -> Self::Output {
341 (self.quantity / rhs).value()
342 }
343}
344
345impl<S: TimeScale> From<Days> for Time<S> {
348 #[inline]
349 fn from(days: Days) -> Self {
350 Self::from_days(days)
351 }
352}
353
354impl<S: TimeScale> From<Time<S>> for Days {
355 #[inline]
356 fn from(time: Time<S>) -> Self {
357 time.quantity
358 }
359}
360
361pub trait TimeInstant: Copy + Clone + PartialEq + PartialOrd + Sized {
370 type Duration;
372
373 fn to_utc(&self) -> Option<DateTime<Utc>>;
375
376 fn from_utc(datetime: DateTime<Utc>) -> Self;
378
379 fn difference(&self, other: &Self) -> Self::Duration;
381
382 fn add_duration(&self, duration: Self::Duration) -> Self;
384
385 fn sub_duration(&self, duration: Self::Duration) -> Self;
387}
388
389impl<S: TimeScale> TimeInstant for Time<S> {
390 type Duration = Days;
391
392 #[inline]
393 fn to_utc(&self) -> Option<DateTime<Utc>> {
394 Time::to_utc(self)
395 }
396
397 #[inline]
398 fn from_utc(datetime: DateTime<Utc>) -> Self {
399 Time::from_utc(datetime)
400 }
401
402 #[inline]
403 fn difference(&self, other: &Self) -> Self::Duration {
404 *self - *other
405 }
406
407 #[inline]
408 fn add_duration(&self, duration: Self::Duration) -> Self {
409 *self + duration
410 }
411
412 #[inline]
413 fn sub_duration(&self, duration: Self::Duration) -> Self {
414 *self - duration
415 }
416}
417
418impl TimeInstant for DateTime<Utc> {
419 type Duration = chrono::Duration;
420
421 fn to_utc(&self) -> Option<DateTime<Utc>> {
422 Some(*self)
423 }
424
425 fn from_utc(datetime: DateTime<Utc>) -> Self {
426 datetime
427 }
428
429 fn difference(&self, other: &Self) -> Self::Duration {
430 *self - *other
431 }
432
433 fn add_duration(&self, duration: Self::Duration) -> Self {
434 *self + duration
435 }
436
437 fn sub_duration(&self, duration: Self::Duration) -> Self {
438 *self - duration
439 }
440}
441
442#[cfg(test)]
447mod tests {
448 use super::super::scales::{JD, MJD};
449 use super::*;
450 use chrono::TimeZone;
451
452 #[test]
453 fn test_julian_day_creation() {
454 let jd = Time::<JD>::new(2_451_545.0);
455 assert_eq!(jd.quantity(), Days::new(2_451_545.0));
456 }
457
458 #[test]
459 fn test_jd_utc_roundtrip() {
460 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
462 let jd = Time::<JD>::from_utc(datetime);
463 let back = jd.to_utc().expect("to_utc");
464 let delta_ns =
465 back.timestamp_nanos_opt().unwrap() - datetime.timestamp_nanos_opt().unwrap();
466 assert!(delta_ns.abs() < 1_000, "roundtrip error: {} ns", delta_ns);
467 }
468
469 #[test]
470 fn test_from_utc_applies_delta_t() {
471 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
473 let jd = Time::<JD>::from_utc(datetime);
474 let delta_t_secs = (jd.quantity() - Days::new(2_451_545.0)).to::<Second>();
475 assert!(
476 (delta_t_secs - Seconds::new(63.83)).abs() < Seconds::new(1.0),
477 "ΔT correction = {} s, expected ~63.83 s",
478 delta_t_secs
479 );
480 }
481
482 #[test]
483 fn test_julian_conversions() {
484 let jd = Time::<JD>::J2000 + Days::new(365_250.0);
485 assert!((jd.julian_millennias() - Millennia::new(1.0)).abs() < 1e-12);
486 assert!((jd.julian_centuries() - Centuries::new(10.0)).abs() < Centuries::new(1e-12));
487 assert!((jd.julian_years() - JulianYears::new(1000.0)).abs() < JulianYears::new(1e-9));
488 }
489
490 #[test]
491 fn test_tt_to_tdb_and_min_max() {
492 let jd_tdb = Time::<JD>::tt_to_tdb(Time::<JD>::J2000);
493 assert!((jd_tdb - Time::<JD>::J2000).abs() < 1e-6);
494
495 let earlier = Time::<JD>::J2000;
496 let later = earlier + Days::new(1.0);
497 assert_eq!(earlier.min(later), earlier);
498 assert_eq!(earlier.max(later), later);
499 }
500
501 #[test]
502 fn test_const_min_max() {
503 const A: Time<JD> = Time::<JD>::new(10.0);
504 const B: Time<JD> = Time::<JD>::new(14.0);
505 const MIN: Time<JD> = A.min(B);
506 const MAX: Time<JD> = A.max(B);
507 assert_eq!(MIN.quantity(), Days::new(10.0));
508 assert_eq!(MAX.quantity(), Days::new(14.0));
509 }
510
511 #[test]
512 fn test_mean_and_const_mean() {
513 let a = Time::<JD>::new(10.0);
514 let b = Time::<JD>::new(14.0);
515 assert_eq!(a.mean(b).quantity(), Days::new(12.0));
516 assert_eq!(b.mean(a).quantity(), Days::new(12.0));
517
518 const MID: Time<JD> = Time::<JD>::new(10.0).mean(Time::<JD>::new(14.0));
519 assert_eq!(MID.quantity(), Days::new(12.0));
520 }
521
522 #[test]
523 fn test_into_days() {
524 let jd = Time::<JD>::new(2_451_547.5);
525 let days: Days = jd.into();
526 assert_eq!(days, 2_451_547.5);
527
528 let roundtrip = Time::<JD>::from(days);
529 assert_eq!(roundtrip, jd);
530 }
531
532 #[test]
533 fn test_into_julian_years() {
534 let jd = Time::<JD>::J2000 + Days::new(365.25 * 2.0);
535 let years: JulianYears = jd.into();
536 assert!((years - JulianYears::new(2.0)).abs() < JulianYears::new(1e-12));
537
538 let roundtrip = Time::<JD>::from(years);
539 assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-12));
540 }
541
542 #[test]
543 fn test_into_centuries() {
544 let jd = Time::<JD>::J2000 + Days::new(36_525.0 * 3.0);
545 let centuries: Centuries = jd.into();
546 assert!((centuries - Centuries::new(3.0)).abs() < Centuries::new(1e-12));
547
548 let roundtrip = Time::<JD>::from(centuries);
549 assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-12));
550 }
551
552 #[test]
553 fn test_into_millennia() {
554 let jd = Time::<JD>::J2000 + Days::new(365_250.0 * 1.5);
555 let millennia: Millennia = jd.into();
556 assert!((millennia - Millennia::new(1.5)).abs() < Millennia::new(1e-12));
557
558 let roundtrip = Time::<JD>::from(millennia);
559 assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-9));
560 }
561
562 #[test]
563 fn test_mjd_creation() {
564 let mjd = Time::<MJD>::new(51_544.5);
565 assert_eq!(mjd.quantity(), Days::new(51_544.5));
566 }
567
568 #[test]
569 fn test_mjd_into_jd() {
570 let mjd = Time::<MJD>::new(51_544.5);
571 let jd: Time<JD> = mjd.into();
572 assert_eq!(jd.quantity(), Days::new(2_451_545.0));
573 }
574
575 #[test]
576 fn test_mjd_utc_roundtrip() {
577 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
578 let mjd = Time::<MJD>::from_utc(datetime);
579 let back = mjd.to_utc().expect("to_utc");
580 let delta_ns =
581 back.timestamp_nanos_opt().unwrap() - datetime.timestamp_nanos_opt().unwrap();
582 assert!(delta_ns.abs() < 1_000, "roundtrip error: {} ns", delta_ns);
583 }
584
585 #[test]
586 fn test_mjd_from_utc_applies_delta_t() {
587 let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
589 let mjd = Time::<MJD>::from_utc(datetime);
590 let delta_t_secs = (mjd.quantity() - Days::new(51_544.5)).to::<Second>();
591 assert!(
592 (delta_t_secs - Seconds::new(63.83)).abs() < Seconds::new(1.0),
593 "ΔT correction = {} s, expected ~63.83 s",
594 delta_t_secs
595 );
596 }
597
598 #[test]
599 fn test_mjd_add_days() {
600 let mjd = Time::<MJD>::new(59_000.0);
601 let result = mjd + Days::new(1.5);
602 assert_eq!(result.quantity(), Days::new(59_001.5));
603 }
604
605 #[test]
606 fn test_mjd_sub_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(58_998.5));
610 }
611
612 #[test]
613 fn test_mjd_sub_mjd() {
614 let mjd1 = Time::<MJD>::new(59_001.0);
615 let mjd2 = Time::<MJD>::new(59_000.0);
616 let diff = mjd1 - mjd2;
617 assert_eq!(diff, 1.0);
618 }
619
620 #[test]
621 fn test_mjd_comparison() {
622 let mjd1 = Time::<MJD>::new(59_000.0);
623 let mjd2 = Time::<MJD>::new(59_001.0);
624 assert!(mjd1 < mjd2);
625 assert!(mjd2 > mjd1);
626 }
627
628 #[test]
629 fn test_display_jd() {
630 let jd = Time::<JD>::new(2_451_545.0);
631 let s = format!("{jd}");
632 assert!(s.contains("Julian Day"));
633 }
634
635 #[test]
636 fn test_try_new_finite() {
637 let jd = Time::<JD>::try_new(2_451_545.0);
638 assert!(jd.is_ok());
639 assert_eq!(jd.unwrap().value(), 2_451_545.0);
640 }
641
642 #[test]
643 fn test_try_new_nan() {
644 assert!(Time::<JD>::try_new(f64::NAN).is_err());
645 }
646
647 #[test]
648 fn test_try_new_infinity() {
649 assert!(Time::<JD>::try_new(f64::INFINITY).is_err());
650 assert!(Time::<JD>::try_new(f64::NEG_INFINITY).is_err());
651 }
652
653 #[test]
654 fn test_try_from_days() {
655 assert!(Time::<JD>::try_from_days(Days::new(100.0)).is_ok());
656 assert!(Time::<JD>::try_from_days(Days::new(f64::NAN)).is_err());
657 }
658
659 #[test]
660 fn test_display_mjd() {
661 let mjd = Time::<MJD>::new(51_544.5);
662 let s = format!("{mjd}");
663 assert!(s.contains("MJD"));
664 }
665
666 #[test]
667 fn test_add_assign_sub_assign() {
668 let mut jd = Time::<JD>::new(2_451_545.0);
669 jd += Days::new(1.0);
670 assert_eq!(jd.quantity(), Days::new(2_451_546.0));
671 jd -= Days::new(0.5);
672 assert_eq!(jd.quantity(), Days::new(2_451_545.5));
673 }
674
675 #[test]
676 fn test_add_years() {
677 let jd = Time::<JD>::new(2_450_000.0);
678 let with_years = jd + Years::new(1.0);
679 let span: Days = with_years - jd;
680 assert!((span - Time::<JD>::JULIAN_YEAR).abs() < Days::new(1e-9));
681 }
682
683 #[test]
684 fn test_div_days_and_f64() {
685 let jd = Time::<JD>::new(100.0);
686 assert!((jd / Days::new(2.0) - 50.0).abs() < 1e-12);
687 assert!((jd / 4.0 - 25.0).abs() < 1e-12);
688 }
689
690 #[test]
691 fn test_to_method_jd_mjd() {
692 let jd = Time::<JD>::new(2_451_545.0);
693 let mjd = jd.to::<MJD>();
694 assert!((mjd.quantity() - Days::new(51_544.5)).abs() < Days::new(1e-10));
695 }
696
697 #[test]
698 fn timeinstant_for_julian_date_handles_arithmetic() {
699 let jd = Time::<JD>::new(2_451_545.0);
700 let other = jd + Days::new(2.0);
701
702 assert_eq!(jd.difference(&other), Days::new(-2.0));
703 assert_eq!(
704 jd.add_duration(Days::new(1.5)).quantity(),
705 Days::new(2_451_546.5)
706 );
707 assert_eq!(
708 other.sub_duration(Days::new(0.5)).quantity(),
709 Days::new(2_451_546.5)
710 );
711 }
712
713 #[test]
714 fn timeinstant_for_modified_julian_date_roundtrips_utc() {
715 let dt = DateTime::from_timestamp(946_684_800, 123_000_000).unwrap(); let mjd = Time::<MJD>::from_utc(dt);
717 let back = mjd.to_utc().expect("mjd to utc");
718
719 assert_eq!(mjd.difference(&mjd), Days::new(0.0));
720 assert_eq!(
721 mjd.add_duration(Days::new(1.0)).quantity(),
722 mjd.quantity() + Days::new(1.0)
723 );
724 assert_eq!(
725 mjd.sub_duration(Days::new(0.5)).quantity(),
726 mjd.quantity() - Days::new(0.5)
727 );
728 let delta_ns = back.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap();
729 assert!(delta_ns.abs() < 10_000, "nanos differ by {}", delta_ns);
730 }
731
732 #[test]
733 fn timeinstant_for_datetime_uses_chrono_durations() {
734 let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
735 let later = Utc.with_ymd_and_hms(2024, 1, 2, 6, 0, 0).unwrap();
736 let diff = later.difference(&base);
737
738 assert_eq!(diff.num_hours(), 30);
739 assert_eq!(
740 base.add_duration(diff + chrono::Duration::hours(6)),
741 later + chrono::Duration::hours(6)
742 );
743 assert_eq!(later.sub_duration(diff), base);
744 assert_eq!(TimeInstant::to_utc(&later), Some(later));
745 }
746}