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