1use core::fmt;
7use core::marker::PhantomData;
8use core::ops::{Add, AddAssign, Sub, SubAssign};
9
10use crate::earth::context::TimeContext;
11use crate::encoding::jd_to_julian_centuries;
12use crate::format::{J2000s, TimeFormat};
13use crate::foundation::error::ConversionError;
14use crate::model::scale::conversion::{ContextScaleConvert, InfallibleScaleConvert};
15use crate::model::scale::{CoordinateScale, Scale, TT, UTC};
16use crate::model::target::{ContextConversionTarget, ConversionTarget, InfallibleConversionTarget};
17use crate::qtty::time::TimeUnit;
18use crate::qtty::{self, Quantity, Second};
19use crate::{FormatForScale, InfallibleFormatForScale};
20use affn::algebra::{Space, SplitPoint1, SplitQuantity};
21
22#[inline]
24fn coordinate_pair_ok(hi: f64, lo: f64) -> bool {
25 !hi.is_nan() && !lo.is_nan()
26}
27
28#[derive(Copy, Clone)]
29pub(crate) struct ScaleAxis<S: Scale>(PhantomData<fn() -> S>);
30
31impl<S: Scale> fmt::Debug for ScaleAxis<S> {
32 #[inline]
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.debug_tuple("ScaleAxis").field(&S::NAME).finish()
35 }
36}
37
38impl<S: Scale> Space for ScaleAxis<S> {}
39
40pub struct Time<S: Scale, F: TimeFormat = J2000s> {
54 instant: SplitPoint1<ScaleAxis<S>, qtty::unit::Second>,
55 _fmt: PhantomData<fn() -> F>,
56}
57
58impl<S: Scale, F: TimeFormat> Copy for Time<S, F> {}
59
60impl<S: Scale, F: TimeFormat> Clone for Time<S, F> {
61 #[inline]
62 fn clone(&self) -> Self {
63 *self
64 }
65}
66
67impl<S: Scale, F: TimeFormat> PartialEq for Time<S, F> {
68 #[inline]
69 fn eq(&self, other: &Self) -> bool {
70 self.split_seconds() == other.split_seconds()
71 }
72}
73
74impl<S: Scale, F: TimeFormat> PartialOrd for Time<S, F> {
75 #[inline]
76 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
77 let (self_hi, self_lo) = self.split_seconds();
78 let (other_hi, other_lo) = other.split_seconds();
79 match self_hi.partial_cmp(&other_hi) {
80 Some(core::cmp::Ordering::Equal) => self_lo.partial_cmp(&other_lo),
81 ordering => ordering,
82 }
83 }
84}
85
86impl<S: Scale, F: TimeFormat> fmt::Debug for Time<S, F> {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 let (hi, lo) = self.split_seconds();
89 f.debug_struct("Time")
90 .field("scale", &S::NAME)
91 .field("format", &F::NAME)
92 .field("hi_s", &hi)
93 .field("lo_s", &lo)
94 .finish()
95 }
96}
97
98impl<S: CoordinateScale, F> fmt::Display for Time<S, F>
99where
100 F: InfallibleFormatForScale<S>,
101 crate::qtty::Quantity<F::Unit>: fmt::Display,
102{
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 if F::NAME == J2000s::NAME {
105 write!(f, "{} {:.9}", S::NAME, self.total_seconds().value())
106 } else {
107 fmt::Display::fmt(&F::from_time(*self), f)
108 }
109 }
110}
111
112impl fmt::Display for Time<UTC, crate::format::Unix> {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 match self.try_raw_with(&TimeContext::new()) {
115 Ok(q) => fmt::Display::fmt(&q, f),
116 Err(_) => f.write_str("Unix(<invalid for display>)"),
117 }
118 }
119}
120
121impl<S: CoordinateScale, F> fmt::LowerExp for Time<S, F>
122where
123 F: InfallibleFormatForScale<S>,
124 crate::qtty::Quantity<F::Unit>: fmt::LowerExp,
125{
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 fmt::LowerExp::fmt(&F::from_time(*self), f)
128 }
129}
130
131impl<S: CoordinateScale, F> fmt::UpperExp for Time<S, F>
132where
133 F: InfallibleFormatForScale<S>,
134 crate::qtty::Quantity<F::Unit>: fmt::UpperExp,
135{
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 fmt::UpperExp::fmt(&F::from_time(*self), f)
138 }
139}
140
141impl<S: Scale, F: TimeFormat> Time<S, F> {
142 #[inline]
143 pub(crate) fn from_split(hi: Second, lo: Second) -> Self {
144 debug_assert!(
145 coordinate_pair_ok(hi.value(), lo.value()),
146 "time split pair must not contain NaN"
147 );
148 let instant = SplitPoint1::new(hi, lo);
149 let (hi, lo) = instant.coordinate().pair();
150 debug_assert!(
151 coordinate_pair_ok(hi.value(), lo.value()),
152 "time split pair must not contain NaN"
153 );
154 Self {
155 instant,
156 _fmt: PhantomData,
157 }
158 }
159
160 #[inline]
161 pub(crate) fn try_from_split(hi: Second, lo: Second) -> Result<Self, ConversionError> {
162 if coordinate_pair_ok(hi.value(), lo.value()) {
163 Ok(Self::from_split(hi, lo))
164 } else {
165 Err(ConversionError::NonFinite)
166 }
167 }
168
169 #[inline]
171 pub fn reinterpret<G: TimeFormat>(self) -> Time<S, G> {
172 Time {
173 instant: self.instant,
174 _fmt: PhantomData,
175 }
176 }
177
178 #[inline]
180 pub fn to_j2000s(self) -> Time<S, J2000s> {
181 self.reinterpret()
182 }
183
184 #[inline]
185 pub(crate) fn split_seconds(self) -> (Second, Second) {
186 self.instant.coordinate().pair()
187 }
188
189 #[inline]
190 pub(crate) fn total_seconds(self) -> Second {
191 self.instant.coordinate().total()
192 }
193
194 #[inline]
196 pub fn raw_seconds_pair(self) -> (Second, Second) {
197 self.split_seconds()
198 }
199}
200
201impl<S: CoordinateScale> Time<S, J2000s> {
202 #[inline]
204 pub fn from_raw_j2000_seconds(seconds: Second) -> Result<Self, ConversionError> {
205 Self::try_from_split(seconds, Second::new(0.0))
206 }
207
208 #[inline]
210 pub fn try_from_raw_j2000_seconds_split(
211 hi: Second,
212 lo: Second,
213 ) -> Result<Self, ConversionError> {
214 Self::try_from_split(hi, lo)
215 }
216
217 #[inline]
218 pub(crate) fn raw_j2000_seconds(self) -> Second {
219 self.total_seconds()
220 }
221
222 #[inline]
224 pub fn shifted_by<U>(self, delta: crate::qtty::Quantity<U>) -> Self
225 where
226 U: TimeUnit,
227 {
228 self + delta
229 }
230
231 #[inline]
233 pub fn shifted_back_by<U>(self, delta: crate::qtty::Quantity<U>) -> Self
234 where
235 U: TimeUnit,
236 {
237 self - delta
238 }
239
240 #[inline]
242 pub fn duration_since(self, other: Self) -> Second {
243 self - other
244 }
245
246 #[inline]
248 pub fn duration_until(self, other: Self) -> Second {
249 other - self
250 }
251}
252
253impl<S: CoordinateScale, F: InfallibleFormatForScale<S>> Time<S, F> {
254 #[inline]
256 pub fn raw(self) -> Quantity<F::Unit> {
257 F::from_time(self)
258 }
259
260 #[inline]
262 pub fn quantity(self) -> Quantity<F::Unit> {
263 F::from_time(self)
264 }
265}
266
267impl<S: CoordinateScale, F: TimeFormat> Time<S, F> {
268 #[inline]
286 pub fn diff_exact(self, other: Self) -> Result<crate::ExactDuration, crate::DurationError> {
287 let delta: Second = self.instant - other.instant;
288 crate::ExactDuration::try_from_quantity(delta)
289 }
290
291 #[inline]
302 pub fn try_add_exact(
303 self,
304 delta: crate::ExactDuration,
305 ) -> Result<Self, crate::foundation::duration::DurationError> {
306 let (whole_secs, sub_nanos) = delta.as_seconds_i64_nanos_checked()?;
307 let t = self.instant + Second::new(whole_secs as f64);
308 Ok(Self {
309 instant: t + Second::new(sub_nanos as f64 * 1e-9),
310 _fmt: PhantomData,
311 })
312 }
313
314 #[inline]
319 pub fn try_sub_exact(
320 self,
321 delta: crate::ExactDuration,
322 ) -> Result<Self, crate::foundation::duration::DurationError> {
323 let (whole_secs, sub_nanos) = delta.as_seconds_i64_nanos_checked()?;
324 let t = self.instant - Second::new(whole_secs as f64);
325 Ok(Self {
326 instant: t - Second::new(sub_nanos as f64 * 1e-9),
327 _fmt: PhantomData,
328 })
329 }
330
331 #[inline]
339 pub fn add_exact(self, delta: crate::ExactDuration) -> Self {
340 self.try_add_exact(delta)
341 .expect("ExactDuration::add_exact: duration exceeds i64 seconds range")
342 }
343
344 #[inline]
351 pub fn sub_exact(self, delta: crate::ExactDuration) -> Self {
352 self.try_sub_exact(delta)
353 .expect("ExactDuration::sub_exact: duration exceeds i64 seconds range")
354 }
355
356 pub fn round_to_epoch(self, epoch: Self, quantum: crate::ExactDuration) -> Self {
360 match self.diff_exact(epoch) {
361 Ok(d) => epoch.add_exact(d.round_to(quantum)),
362 Err(_) => self,
363 }
364 }
365
366 pub fn floor_to_epoch(self, epoch: Self, quantum: crate::ExactDuration) -> Self {
368 match self.diff_exact(epoch) {
369 Ok(d) => epoch.add_exact(d.floor_to(quantum)),
370 Err(_) => self,
371 }
372 }
373
374 pub fn ceil_to_epoch(self, epoch: Self, quantum: crate::ExactDuration) -> Self {
376 match self.diff_exact(epoch) {
377 Ok(d) => epoch.add_exact(d.ceil_to(quantum)),
378 Err(_) => self,
379 }
380 }
381}
382
383impl<S: CoordinateScale, F> Time<S, F>
384where
385 F: FormatForScale<S>,
386{
387 #[inline]
388 pub fn try_raw_with(self, ctx: &TimeContext) -> Result<Quantity<F::Unit>, ConversionError> {
389 F::try_from_time(self, ctx)
390 }
391}
392
393impl<S: Scale, F: TimeFormat> Time<S, F> {
394 #[allow(private_bounds)]
396 #[inline]
397 pub fn to<T>(self) -> T::Output
398 where
399 T: InfallibleConversionTarget<S, F>,
400 {
401 T::convert(self)
402 }
403
404 #[allow(private_bounds)]
406 #[inline]
407 pub fn try_to<T>(self) -> Result<T::Output, ConversionError>
408 where
409 T: ConversionTarget<S, F>,
410 {
411 T::try_convert(self)
412 }
413
414 #[allow(private_bounds)]
416 #[inline]
417 pub fn to_with<T>(self, ctx: &TimeContext) -> Result<T::Output, ConversionError>
418 where
419 T: ContextConversionTarget<S, F>,
420 {
421 T::convert_with(self, ctx)
422 }
423
424 #[allow(private_bounds)]
426 #[inline]
427 pub fn to_scale<S2: Scale>(self) -> Time<S2, F>
428 where
429 S: InfallibleScaleConvert<S2>,
430 {
431 let (hi, lo) = self.split_seconds();
432 let (new_hi, new_lo) = <S as InfallibleScaleConvert<S2>>::convert(hi, lo);
433 Time::from_split(new_hi, new_lo)
434 }
435
436 #[allow(private_bounds)]
438 #[inline]
439 pub fn to_scale_with<S2: Scale>(self, ctx: &TimeContext) -> Result<Time<S2, F>, ConversionError>
440 where
441 S: ContextScaleConvert<S2>,
442 {
443 let (hi, lo) = self.split_seconds();
444 let (new_hi, new_lo) = <S as ContextScaleConvert<S2>>::convert_with(hi, lo, ctx)?;
445 Ok(Time::from_split(new_hi, new_lo))
446 }
447}
448
449impl<S: Scale, F: FormatForScale<S>> Time<S, F> {
450 #[inline]
455 pub fn try_new(raw: Quantity<F::Unit>) -> Result<Self, ConversionError> {
456 F::try_into_time(raw, &TimeContext::new())
457 }
458
459 #[inline]
461 pub fn try_new_with(
462 raw: Quantity<F::Unit>,
463 ctx: &TimeContext,
464 ) -> Result<Self, ConversionError> {
465 F::try_into_time(raw, ctx)
466 }
467}
468
469impl<S: Scale, F: InfallibleFormatForScale<S>> Time<S, F> {
470 #[track_caller]
476 #[inline]
477 pub fn new(value: f64) -> Self {
478 assert!(
479 !value.is_nan(),
480 "time scalar must not be NaN (±∞ is allowed)"
481 );
482 F::into_time(Quantity::<F::Unit>::new(value))
483 }
484}
485
486impl<S: CoordinateScale, F: InfallibleFormatForScale<S>> Time<S, F> {
487 #[inline]
488 pub fn min(self, other: Self) -> Self {
489 if self <= other {
490 self
491 } else {
492 other
493 }
494 }
495
496 #[inline]
497 pub fn max(self, other: Self) -> Self {
498 if self >= other {
499 self
500 } else {
501 other
502 }
503 }
504
505 #[inline]
506 pub fn mean(self, other: Self) -> Self {
507 let t = self.to_j2000s() + ((other.to_j2000s() - self.to_j2000s()) * 0.5);
508 t.reinterpret()
509 }
510}
511
512impl Time<TT, crate::format::JD> {
514 pub const JD_EPOCH_J2000_0: Self = Self {
515 instant: SplitPoint1::from_split(SplitQuantity::from_normalized_parts(
516 Second::new(0.0),
517 Second::new(0.0),
518 )),
519 _fmt: PhantomData,
520 };
521}
522
523impl<S: Scale> Time<S, crate::format::JD> {
524 #[inline]
526 pub fn jd_epoch_tt() -> Self
527 where
528 S: CoordinateScale,
529 {
530 Time::<S, J2000s>::from_raw_j2000_seconds(Second::new(0.0))
531 .expect("J2000 origin")
532 .reinterpret()
533 }
534
535 #[inline]
536 pub fn value(self) -> f64
537 where
538 S: CoordinateScale,
539 {
540 self.raw().value()
541 }
542
543 #[inline]
544 pub fn julian_centuries(self) -> f64
545 where
546 S: CoordinateScale,
547 {
548 jd_to_julian_centuries(self.raw())
549 }
550}
551
552impl<S: Scale> Time<S, crate::format::MJD> {
553 #[inline]
554 pub fn value(self) -> f64
555 where
556 S: CoordinateScale,
557 {
558 self.raw().value()
559 }
560}
561
562impl<S: CoordinateScale, F, U> Add<Quantity<U>> for Time<S, F>
563where
564 F: InfallibleFormatForScale<S>,
565 U: TimeUnit,
566{
567 type Output = Self;
568
569 #[inline]
570 fn add(self, rhs: Quantity<U>) -> Self::Output {
571 Self {
572 instant: self.instant + rhs.to::<qtty::unit::Second>(),
573 _fmt: PhantomData,
574 }
575 }
576}
577
578impl<S: CoordinateScale, F, U> Sub<Quantity<U>> for Time<S, F>
579where
580 F: InfallibleFormatForScale<S>,
581 U: TimeUnit,
582{
583 type Output = Self;
584
585 #[inline]
586 fn sub(self, rhs: Quantity<U>) -> Self::Output {
587 Self {
588 instant: self.instant - rhs.to::<qtty::unit::Second>(),
589 _fmt: PhantomData,
590 }
591 }
592}
593
594impl<S: CoordinateScale, F> Sub for Time<S, F>
595where
596 F: InfallibleFormatForScale<S>,
597 F::Unit: TimeUnit,
598{
599 type Output = Quantity<F::Unit>;
600
601 #[inline]
602 fn sub(self, rhs: Self) -> Self::Output {
603 let delta: Second = self.instant - rhs.instant;
604 delta.to::<F::Unit>()
605 }
606}
607
608impl<S: CoordinateScale, F, U> AddAssign<Quantity<U>> for Time<S, F>
609where
610 F: InfallibleFormatForScale<S>,
611 U: TimeUnit,
612{
613 #[inline]
614 fn add_assign(&mut self, rhs: Quantity<U>) {
615 *self = *self + rhs;
616 }
617}
618
619impl<S: CoordinateScale, F, U> SubAssign<Quantity<U>> for Time<S, F>
620where
621 F: InfallibleFormatForScale<S>,
622 U: TimeUnit,
623{
624 #[inline]
625 fn sub_assign(&mut self, rhs: Quantity<U>) {
626 *self = *self - rhs;
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633 use crate::format::J2000s;
634 use crate::foundation::duration::ExactDuration;
635 use crate::model::scale::TAI;
636
637 type TaiJ2000 = Time<TAI, J2000s>;
638
639 fn j2000_tai() -> TaiJ2000 {
640 TaiJ2000::from_raw_j2000_seconds(Second::new(0.0)).unwrap()
641 }
642
643 fn j2000_tai_plus_50yr() -> TaiJ2000 {
644 TaiJ2000::from_raw_j2000_seconds(Second::new(1_577_836_800.0)).unwrap()
646 }
647
648 #[test]
649 fn add_exact_1ns_at_j2000() {
650 let t = j2000_tai();
651 let d = ExactDuration::from_nanos(1);
652 let shifted = t.add_exact(d);
653 let diff = shifted.diff_exact(t).unwrap();
654 assert_eq!(diff.as_nanos_i128(), 1, "1 ns shift at J2000 must be exact");
656 }
657
658 #[test]
659 fn add_sub_round_trip_1ns_at_j2000_plus_50yr() {
660 let t = j2000_tai_plus_50yr();
661 for ns in [1_i128, 123, 999] {
663 let d = ExactDuration::from_nanos(ns);
664 let shifted = t.add_exact(d).sub_exact(d);
665 let back = shifted.diff_exact(t).unwrap();
666 assert!(
667 back.as_nanos_i128().abs() < 100,
668 "add/sub round-trip drift at J2000+50yr for {ns} ns: {} ns",
669 back.as_nanos_i128()
670 );
671 }
672 }
673
674 #[test]
675 fn add_exact_1yr_plus_1ns_preserves_1ns() {
676 let t = j2000_tai();
677 let one_year = ExactDuration::from_nanos(31_557_600 * 1_000_000_000);
679 let one_ns = ExactDuration::from_nanos(1);
680 let combined = (one_year + one_ns)
681 .checked_add(ExactDuration::ZERO)
682 .unwrap();
683 let d_year = t.add_exact(one_year);
684 let d_combined = t.add_exact(combined);
685 let diff = d_combined.diff_exact(d_year).unwrap();
686 assert!(
688 diff.as_nanos_i128().abs() <= 2,
689 "1 yr + 1 ns shift must preserve 1 ns component; diff = {} ns",
690 diff.as_nanos_i128()
691 );
692 }
693
694 #[test]
695 fn try_add_exact_overflow_returns_err() {
696 let t = j2000_tai();
697 let result = t.try_add_exact(ExactDuration::MAX);
699 assert!(
700 result.is_err(),
701 "expected Err for try_add_exact(MAX), got Ok"
702 );
703 let result2 = t.try_sub_exact(ExactDuration::MAX);
704 assert!(
705 result2.is_err(),
706 "expected Err for try_sub_exact(MAX), got Ok"
707 );
708 }
709
710 #[test]
711 #[should_panic(expected = "ExactDuration::add_exact")]
712 fn add_exact_panics_on_overflow() {
713 let t = j2000_tai();
714 let _ = t.add_exact(ExactDuration::MAX);
715 }
716}