Skip to main content

tempoch_core/
representation.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Typed encoded representations of [`crate::Time`].
5
6use core::fmt;
7use core::marker::PhantomData;
8
9use crate::context::TimeContext;
10use crate::encoding::{
11    j2000_seconds_to_jd, j2000_seconds_to_mjd, jd_to_j2000_seconds, mjd_to_j2000_seconds,
12};
13use crate::error::ConversionError;
14use crate::scale::conversion::InfallibleScaleConvert;
15use crate::scale::{CoordinateScale, Scale, TAI, TDB, TT, UTC};
16use crate::sealed::Sealed;
17use crate::target::{ContextConversionTarget, ConversionTarget, InfallibleConversionTarget};
18use crate::time::Time;
19use qtty::{Day, Quantity, Second, Unit};
20
21/// Marker trait for external time encodings such as JD or Unix time.
22#[allow(private_bounds)]
23pub trait TimeRepresentation: Sealed + Copy + Clone + fmt::Debug + 'static {
24    /// Quantity unit used by this representation.
25    type Unit: Unit;
26
27    /// Human-readable representation name.
28    const NAME: &'static str;
29}
30
31/// Representation witness for scale `S`.
32#[allow(private_bounds)]
33pub trait RepresentationForScale<S: Scale>: TimeRepresentation + Sealed {
34    fn try_from_time(
35        time: Time<S>,
36        ctx: &TimeContext,
37    ) -> Result<Quantity<Self::Unit>, ConversionError>;
38    fn try_into_time(
39        raw: Quantity<Self::Unit>,
40        ctx: &TimeContext,
41    ) -> Result<Time<S>, ConversionError>;
42}
43
44/// Representation witness for scale `S` with context-free round-trips.
45#[allow(private_bounds)]
46pub trait InfallibleRepresentationForScale<S: Scale>: RepresentationForScale<S> + Sealed {
47    fn from_time(time: Time<S>) -> Quantity<Self::Unit>;
48    fn into_time(raw: Quantity<Self::Unit>) -> Time<S>;
49}
50
51/// J2000 seconds on the source scale's coordinate axis.
52#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
53pub struct J2000s;
54
55/// Julian Day on the source scale's coordinate axis.
56#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub struct JD;
58
59/// Modified Julian Day on the source scale's coordinate axis.
60#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61pub struct MJD;
62
63/// POSIX seconds on the UTC civil axis.
64#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
65pub struct Unix;
66
67/// GPS seconds since the GPS epoch on the TAI/GPS continuous axis.
68#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
69pub struct GPS;
70
71impl Sealed for J2000s {}
72impl Sealed for JD {}
73impl Sealed for MJD {}
74impl Sealed for Unix {}
75impl Sealed for GPS {}
76
77impl TimeRepresentation for J2000s {
78    type Unit = qtty::unit::Second;
79    const NAME: &'static str = "J2000s";
80}
81
82impl TimeRepresentation for JD {
83    type Unit = qtty::unit::Day;
84    const NAME: &'static str = "Julian Day";
85}
86
87impl TimeRepresentation for MJD {
88    type Unit = qtty::unit::Day;
89    const NAME: &'static str = "Modified Julian Day";
90}
91
92impl TimeRepresentation for Unix {
93    type Unit = qtty::unit::Second;
94    const NAME: &'static str = "Unix";
95}
96
97impl TimeRepresentation for GPS {
98    type Unit = qtty::unit::Second;
99    const NAME: &'static str = "GPS";
100}
101
102/// A typed external representation of a [`Time<S>`] instant.
103pub struct EncodedTime<S: Scale, R: TimeRepresentation> {
104    raw: Quantity<R::Unit>,
105    _marker: PhantomData<fn() -> S>,
106}
107
108impl<S: Scale, R: TimeRepresentation> Copy for EncodedTime<S, R> {}
109
110impl<S: Scale, R: TimeRepresentation> Clone for EncodedTime<S, R> {
111    #[inline]
112    fn clone(&self) -> Self {
113        *self
114    }
115}
116
117impl<S: Scale, R: TimeRepresentation> fmt::Debug for EncodedTime<S, R> {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        f.debug_struct("EncodedTime")
120            .field("scale", &S::NAME)
121            .field("representation", &R::NAME)
122            .field("raw", &self.raw)
123            .finish()
124    }
125}
126
127impl<S: Scale, R: TimeRepresentation> fmt::Display for EncodedTime<S, R>
128where
129    qtty::Quantity<R::Unit>: fmt::Display,
130{
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "{} ", R::NAME)?;
133        fmt::Display::fmt(&self.raw, f)
134    }
135}
136
137impl<S: Scale, R: TimeRepresentation> fmt::LowerExp for EncodedTime<S, R>
138where
139    qtty::Quantity<R::Unit>: fmt::LowerExp,
140{
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        fmt::LowerExp::fmt(&self.raw, f)
143    }
144}
145
146impl<S: Scale, R: TimeRepresentation> fmt::UpperExp for EncodedTime<S, R>
147where
148    qtty::Quantity<R::Unit>: fmt::UpperExp,
149{
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        fmt::UpperExp::fmt(&self.raw, f)
152    }
153}
154
155impl<S: Scale, R: TimeRepresentation> PartialEq for EncodedTime<S, R> {
156    #[inline]
157    fn eq(&self, other: &Self) -> bool {
158        self.raw == other.raw
159    }
160}
161
162impl<S: Scale, R: TimeRepresentation> PartialOrd for EncodedTime<S, R> {
163    #[inline]
164    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
165        self.raw.partial_cmp(&other.raw)
166    }
167}
168
169impl<S: Scale, R: TimeRepresentation> EncodedTime<S, R> {
170    /// Construct from a raw quantity, bypassing the finite check.
171    ///
172    /// Passing a non-finite value yields an instant whose behaviour is
173    /// unspecified. Prefer [`Self::try_new`] for user-supplied data; use this
174    /// only when the value is known to be finite (e.g. compile-time constants).
175    #[inline]
176    pub const fn new_unchecked(raw: Quantity<R::Unit>) -> Self {
177        Self {
178            raw,
179            _marker: PhantomData,
180        }
181    }
182
183    /// Return the underlying typed quantity.
184    #[inline]
185    pub const fn raw(self) -> Quantity<R::Unit> {
186        self.raw
187    }
188
189    /// Alias for [`Self::raw`].
190    #[inline]
191    pub const fn quantity(self) -> Quantity<R::Unit> {
192        self.raw
193    }
194}
195
196impl<S: Scale, R> EncodedTime<S, R>
197where
198    R: RepresentationForScale<S>,
199{
200    /// Construct a typed encoded instant from its raw quantity.
201    #[inline]
202    pub fn try_new(raw: Quantity<R::Unit>) -> Result<Self, ConversionError> {
203        if raw.is_finite() {
204            Ok(Self::new_unchecked(raw))
205        } else {
206            Err(ConversionError::NonFinite)
207        }
208    }
209
210    /// Convert this encoded instant to the canonical [`Time<S>`] model.
211    ///
212    /// Snapshots the active time-data bundle at call time via
213    /// [`TimeContext::new`]. For reproducible pipelines, prefer
214    /// [`to_time_with`](Self::to_time_with) with an explicit context.
215    #[inline]
216    pub fn try_to_time(self) -> Result<Time<S>, ConversionError> {
217        R::try_into_time(self.raw, &TimeContext::new())
218    }
219
220    /// Convert this encoded instant to the canonical [`Time<S>`] model using an explicit context.
221    #[inline]
222    pub fn to_time_with(self, ctx: &TimeContext) -> Result<Time<S>, ConversionError> {
223        R::try_into_time(self.raw, ctx)
224    }
225}
226
227impl<S: Scale, R> EncodedTime<S, R>
228where
229    R: InfallibleRepresentationForScale<S>,
230{
231    #[inline]
232    pub(crate) fn from_time_infallible(time: Time<S>) -> Self {
233        Self::new_unchecked(R::from_time(time))
234    }
235
236    /// Infallible conversion to the canonical [`Time<S>`] model.
237    #[inline]
238    pub fn to_time(self) -> Time<S> {
239        R::into_time(self.raw)
240    }
241
242    /// Unified infallible conversion to a target scale or encoded format.
243    #[allow(private_bounds)]
244    #[inline]
245    pub fn to<T>(self) -> T::Output
246    where
247        T: InfallibleConversionTarget<S>,
248    {
249        T::convert(self.to_time())
250    }
251
252    /// Unified fallible conversion to a target scale or encoded format.
253    #[allow(private_bounds)]
254    #[inline]
255    pub fn try_to<T>(self) -> Result<T::Output, ConversionError>
256    where
257        T: ConversionTarget<S>,
258    {
259        T::try_convert(self.to_time())
260    }
261}
262
263impl<S: Scale, R> EncodedTime<S, R>
264where
265    R: RepresentationForScale<S>,
266{
267    /// Unified context-backed conversion to a target scale or encoded format.
268    #[allow(private_bounds)]
269    #[inline]
270    pub fn to_with<T>(self, ctx: &TimeContext) -> Result<T::Output, ConversionError>
271    where
272        T: ContextConversionTarget<S>,
273    {
274        T::convert_with(self.to_time_with(ctx)?, ctx)
275    }
276}
277
278/// `EncodedTime<S, JD>` convenience alias.
279pub type JulianDate<S> = EncodedTime<S, JD>;
280
281/// `EncodedTime<S, MJD>` convenience alias.
282pub type ModifiedJulianDate<S> = EncodedTime<S, MJD>;
283
284/// `EncodedTime<S, J2000s>` convenience alias.
285pub type J2000Seconds<S> = EncodedTime<S, J2000s>;
286
287/// `EncodedTime<UTC, Unix>` convenience alias.
288pub type UnixTime = EncodedTime<UTC, Unix>;
289
290/// `EncodedTime<TAI, GPS>` convenience alias.
291pub type GpsTime = EncodedTime<TAI, GPS>;
292
293/// J2000.0 epoch as a TT-scale Julian Date (JD(TT) = 2 451 545.0).
294pub const J2000_TT: JulianDate<TT> = EncodedTime::<TT, JD>::new_unchecked(Day::new(2_451_545.0));
295
296// ── Inherent helpers for Day-based encoded times (JD and MJD) ────────────────
297
298impl<S: Scale, R> EncodedTime<S, R>
299where
300    R: TimeRepresentation<Unit = qtty::unit::Day>,
301{
302    /// Earlier of `self` and `other`.
303    ///
304    /// Equivalent to `if self <= other { self } else { other }`.
305    #[inline]
306    pub fn min(self, other: Self) -> Self {
307        if self.raw.value() <= other.raw.value() {
308            self
309        } else {
310            other
311        }
312    }
313
314    /// Later of `self` and `other`.
315    #[inline]
316    pub fn max(self, other: Self) -> Self {
317        if self.raw.value() >= other.raw.value() {
318            self
319        } else {
320            other
321        }
322    }
323
324    /// Midpoint between `self` and `other`.
325    #[inline]
326    pub fn mean(self, other: Self) -> Self {
327        Self::new_unchecked(Day::new((self.raw.value() + other.raw.value()) * 0.5))
328    }
329}
330
331// ── JulianDate<S> inherent helpers ───────────────────────────────────────────
332
333/// Length of a Julian year in days (exactly 365.25 d).
334pub const JULIAN_YEAR_DAYS: Day = Day::new(365.25);
335
336impl<S: CoordinateScale> JulianDate<S> {
337    /// Construct from a raw Julian Day value without validation.
338    ///
339    /// Prefer [`Self::try_new`] for untrusted input.
340    #[inline]
341    pub fn new(jd: f64) -> Self {
342        Self::new_unchecked(Day::new(jd))
343    }
344
345    /// Raw Julian Day value as `f64` (days since noon 1 January 4713 BC JD).
346    #[inline]
347    pub fn jd_value(self) -> f64 {
348        self.raw().value()
349    }
350
351    /// Julian centuries since J2000.0: `T = (JD − 2 451 545.0) / 36 525`.
352    #[inline]
353    pub fn julian_centuries(self) -> f64 {
354        (self.raw().value() - crate::constats::J2000_JD_TT.value())
355            / crate::constats::DAYS_PER_JC.value()
356    }
357
358    /// Julian millennia since J2000.0: `T = (JD − 2 451 545.0) / 365 250`.
359    #[inline]
360    pub fn julian_millennias(self) -> f64 {
361        (self.raw().value() - crate::constats::J2000_JD_TT.value())
362            / (crate::constats::DAYS_PER_JC.value() * 10.0)
363    }
364
365    /// Length of a Julian year in days (365.25 d).
366    pub const JULIAN_YEAR: Day = Day::new(365.25);
367
368    /// Length of a Julian century in days (36 525 d).
369    pub const JULIAN_CENTURY: Day = Day::new(36_525.0);
370}
371
372impl JulianDate<TT> {
373    /// J2000.0 epoch as a TT-scale Julian Date (`JD(TT) = 2 451 545.0`).
374    pub const J2000: Self = J2000_TT;
375
376    /// Convert this TT Julian Date to the TDB scale.
377    ///
378    /// Uses the Fairhead-Bretagnon periodic correction stored in the scale
379    /// conversion layer. The result is on the TDB coordinate time axis, still
380    /// expressed as a Julian Date.
381    #[inline]
382    pub fn tt_to_tdb(self) -> JulianDate<TDB> {
383        JulianDate::<TDB>::from_time_infallible(self.to_time().to_scale::<TDB>())
384    }
385
386    /// Build a TT Julian Date from a `chrono::DateTime<Utc>`.
387    ///
388    /// Converts UTC → TAI → TT internally.  Panics if the UTC time data is
389    /// unavailable for the supplied instant; use
390    /// [`Time::<UTC>::try_from_chrono`] for a fallible path.
391    #[inline]
392    pub fn from_chrono(dt: chrono::DateTime<chrono::Utc>) -> Self {
393        let utc = Time::<UTC>::from_chrono(dt);
394        Self::from_time_infallible(utc.to_scale::<TT>())
395    }
396
397    /// Convert this TT Julian Date to a `chrono::DateTime<Utc>`.
398    ///
399    /// Returns `None` if the value is outside the supported UTC range.
400    #[inline]
401    pub fn to_chrono(self) -> Option<chrono::DateTime<chrono::Utc>> {
402        self.to_time().to_scale::<UTC>().to_chrono()
403    }
404}
405
406// ── Cross-representation From conversions (JD ↔ MJD for CoordinateScales) ───
407
408impl<S: CoordinateScale> From<ModifiedJulianDate<S>> for JulianDate<S> {
409    /// Convert a Modified Julian Date to a Julian Date on the same scale.
410    #[inline]
411    fn from(mjd: ModifiedJulianDate<S>) -> Self {
412        mjd.to::<JD>()
413    }
414}
415
416impl<S: CoordinateScale> From<JulianDate<S>> for ModifiedJulianDate<S> {
417    /// Convert a Julian Date to a Modified Julian Date on the same scale.
418    #[inline]
419    fn from(jd: JulianDate<S>) -> Self {
420        jd.to::<MJD>()
421    }
422}
423
424// ── ModifiedJulianDate<S> inherent helpers ────────────────────────────────────
425
426impl<S: CoordinateScale> ModifiedJulianDate<S> {
427    /// Construct from a raw Modified Julian Day value without validation.
428    ///
429    /// Prefer [`Self::try_new`] for untrusted input.
430    #[inline]
431    pub fn new(mjd: f64) -> Self {
432        Self::new_unchecked(Day::new(mjd))
433    }
434
435    /// Raw Modified Julian Day value as `f64` (days since midnight 17 November 1858).
436    #[inline]
437    pub fn mjd_value(self) -> f64 {
438        self.raw().value()
439    }
440}
441
442impl ModifiedJulianDate<TT> {
443    /// Build a TT Modified Julian Date from a `chrono::DateTime<Utc>`.
444    ///
445    /// Converts UTC → TAI → TT internally.  Panics if UTC time data is
446    /// unavailable; use [`Time::<UTC>::try_from_chrono`] for a fallible path.
447    #[inline]
448    pub fn from_chrono(dt: chrono::DateTime<chrono::Utc>) -> Self {
449        let utc = Time::<UTC>::from_chrono(dt);
450        Self::from_time_infallible(utc.to_scale::<TT>())
451    }
452
453    /// Convert this TT Modified Julian Date to a `chrono::DateTime<Utc>`.
454    ///
455    /// Returns `None` if the value is outside the supported UTC range.
456    #[inline]
457    pub fn to_chrono(self) -> Option<chrono::DateTime<chrono::Utc>> {
458        self.to_time().to_scale::<UTC>().to_chrono()
459    }
460}
461
462macro_rules! coordinate_representation {
463    ($repr:ty, $quantity:ty, $from_time:expr, $to_time:expr) => {
464        impl<S: CoordinateScale> RepresentationForScale<S> for $repr {
465            #[inline]
466            fn try_from_time(
467                time: Time<S>,
468                _ctx: &TimeContext,
469            ) -> Result<$quantity, ConversionError> {
470                Ok(<Self as InfallibleRepresentationForScale<S>>::from_time(
471                    time,
472                ))
473            }
474
475            #[inline]
476            fn try_into_time(
477                raw: $quantity,
478                _ctx: &TimeContext,
479            ) -> Result<Time<S>, ConversionError> {
480                Ok(<Self as InfallibleRepresentationForScale<S>>::into_time(
481                    raw,
482                ))
483            }
484        }
485
486        impl<S: CoordinateScale> InfallibleRepresentationForScale<S> for $repr {
487            #[inline]
488            fn from_time(time: Time<S>) -> $quantity {
489                $from_time(time)
490            }
491
492            #[inline]
493            fn into_time(raw: $quantity) -> Time<S> {
494                $to_time(raw)
495            }
496        }
497    };
498}
499
500coordinate_representation!(
501    J2000s,
502    Second,
503    |time: Time<_>| time.raw_j2000_seconds(),
504    |raw: Second| Time::from_raw_j2000_seconds(raw).expect("finite J2000 seconds must decode")
505);
506coordinate_representation!(
507    JD,
508    Day,
509    |time: Time<_>| j2000_seconds_to_jd(time.raw_j2000_seconds()),
510    |raw: Day| Time::from_raw_j2000_seconds(jd_to_j2000_seconds(raw))
511        .expect("finite Julian date must decode")
512);
513coordinate_representation!(
514    MJD,
515    Day,
516    |time: Time<_>| j2000_seconds_to_mjd(time.raw_j2000_seconds()),
517    |raw: Day| Time::from_raw_j2000_seconds(mjd_to_j2000_seconds(raw))
518        .expect("finite Modified Julian date must decode")
519);
520
521impl RepresentationForScale<UTC> for Unix {
522    #[inline]
523    fn try_from_time(time: Time<UTC>, ctx: &TimeContext) -> Result<Second, ConversionError> {
524        time.raw_unix_seconds_with(ctx)
525    }
526
527    #[inline]
528    fn try_into_time(raw: Second, ctx: &TimeContext) -> Result<Time<UTC>, ConversionError> {
529        Time::from_raw_unix_seconds_with(raw, ctx)
530    }
531}
532
533impl RepresentationForScale<TAI> for GPS {
534    #[inline]
535    fn try_from_time(time: Time<TAI>, _ctx: &TimeContext) -> Result<Second, ConversionError> {
536        Ok(<Self as InfallibleRepresentationForScale<TAI>>::from_time(
537            time,
538        ))
539    }
540
541    #[inline]
542    fn try_into_time(raw: Second, _ctx: &TimeContext) -> Result<Time<TAI>, ConversionError> {
543        Ok(<Self as InfallibleRepresentationForScale<TAI>>::into_time(
544            raw,
545        ))
546    }
547}
548
549impl InfallibleRepresentationForScale<TAI> for GPS {
550    #[inline]
551    fn from_time(time: Time<TAI>) -> Second {
552        time.raw_gps_seconds()
553    }
554
555    #[inline]
556    fn into_time(raw: Second) -> Time<TAI> {
557        Time::from_raw_gps_seconds(raw).expect("finite GPS seconds must decode")
558    }
559}
560
561// ── Arithmetic on EncodedTime ────────────────────────────────────────────────
562//
563// For JD- and MJD-based representations (both use `qtty::unit::Day`), it is
564// natural to shift an instant by a number of days and to compute the signed
565// duration between two instants.
566
567impl<S: Scale, R> core::ops::Add<Day> for EncodedTime<S, R>
568where
569    R: TimeRepresentation<Unit = qtty::unit::Day>,
570{
571    type Output = Self;
572
573    #[inline]
574    fn add(self, rhs: Day) -> Self {
575        Self::new_unchecked(Day::new(self.raw.value() + rhs.value()))
576    }
577}
578
579impl<S: Scale, R> core::ops::Sub<Day> for EncodedTime<S, R>
580where
581    R: TimeRepresentation<Unit = qtty::unit::Day>,
582{
583    type Output = Self;
584
585    #[inline]
586    fn sub(self, rhs: Day) -> Self {
587        Self::new_unchecked(Day::new(self.raw.value() - rhs.value()))
588    }
589}
590
591impl<S: Scale, R> core::ops::AddAssign<Day> for EncodedTime<S, R>
592where
593    R: TimeRepresentation<Unit = qtty::unit::Day>,
594{
595    #[inline]
596    fn add_assign(&mut self, rhs: Day) {
597        *self = *self + rhs;
598    }
599}
600
601impl<S: Scale, R> core::ops::SubAssign<Day> for EncodedTime<S, R>
602where
603    R: TimeRepresentation<Unit = qtty::unit::Day>,
604{
605    #[inline]
606    fn sub_assign(&mut self, rhs: Day) {
607        *self = *self - rhs;
608    }
609}
610
611/// `b - a` returns the signed offset in days: positive when `b` is later.
612impl<S: Scale, R> core::ops::Sub for EncodedTime<S, R>
613where
614    R: TimeRepresentation<Unit = qtty::unit::Day>,
615{
616    type Output = Day;
617
618    #[inline]
619    fn sub(self, rhs: Self) -> Day {
620        Day::new(self.raw.value() - rhs.raw.value())
621    }
622}
623
624impl<S: Scale, R> From<EncodedTime<S, R>> for Time<S>
625where
626    R: InfallibleRepresentationForScale<S>,
627{
628    #[inline]
629    fn from(value: EncodedTime<S, R>) -> Self {
630        value.to_time()
631    }
632}
633
634impl<S: Scale, R> From<Time<S>> for EncodedTime<S, R>
635where
636    R: InfallibleRepresentationForScale<S>,
637{
638    #[inline]
639    fn from(value: Time<S>) -> Self {
640        Self::from_time_infallible(value)
641    }
642}
643
644// ── ConversionTarget impls for format markers ────────────────────────────────
645
646impl<S: CoordinateScale> ConversionTarget<S> for J2000s {
647    type Output = EncodedTime<S, J2000s>;
648
649    #[inline]
650    fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
651        Ok(EncodedTime::from_time_infallible(src))
652    }
653}
654
655impl<S: CoordinateScale> InfallibleConversionTarget<S> for J2000s {
656    #[inline]
657    fn convert(src: Time<S>) -> Self::Output {
658        EncodedTime::from_time_infallible(src)
659    }
660}
661
662impl<S: CoordinateScale> ConversionTarget<S> for JD {
663    type Output = EncodedTime<S, JD>;
664
665    #[inline]
666    fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
667        Ok(EncodedTime::from_time_infallible(src))
668    }
669}
670
671impl<S: CoordinateScale> InfallibleConversionTarget<S> for JD {
672    #[inline]
673    fn convert(src: Time<S>) -> Self::Output {
674        EncodedTime::from_time_infallible(src)
675    }
676}
677
678impl<S: CoordinateScale> ConversionTarget<S> for MJD {
679    type Output = EncodedTime<S, MJD>;
680
681    #[inline]
682    fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
683        Ok(EncodedTime::from_time_infallible(src))
684    }
685}
686
687impl<S: CoordinateScale> InfallibleConversionTarget<S> for MJD {
688    #[inline]
689    fn convert(src: Time<S>) -> Self::Output {
690        EncodedTime::from_time_infallible(src)
691    }
692}
693
694impl<S> ConversionTarget<S> for Unix
695where
696    S: crate::scale::Scale + InfallibleScaleConvert<UTC>,
697{
698    type Output = EncodedTime<UTC, Unix>;
699
700    /// Snapshots the active time-data bundle at call time via
701    /// [`TimeContext::new`]. For reproducible pipelines, prefer
702    /// [`to_with::<Unix>(&ctx)`](crate::time::Time::to_with).
703    #[inline]
704    fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
705        let utc = src.to_scale::<UTC>();
706        let raw = Unix::try_from_time(utc, &TimeContext::new())?;
707        Ok(EncodedTime::new_unchecked(raw))
708    }
709}
710
711impl ContextConversionTarget<UTC> for Unix {
712    type Output = EncodedTime<UTC, Unix>;
713
714    #[inline]
715    fn convert_with(src: Time<UTC>, ctx: &TimeContext) -> Result<Self::Output, ConversionError> {
716        let raw = Unix::try_from_time(src, ctx)?;
717        Ok(EncodedTime::new_unchecked(raw))
718    }
719}
720
721impl<S> ContextConversionTarget<S> for Unix
722where
723    S: crate::scale::Scale + crate::scale::conversion::ContextScaleConvert<UTC>,
724{
725    type Output = EncodedTime<UTC, Unix>;
726
727    #[inline]
728    fn convert_with(src: Time<S>, ctx: &TimeContext) -> Result<Self::Output, ConversionError> {
729        let utc = src.to_scale_with::<UTC>(ctx)?;
730        let raw = Unix::try_from_time(utc, ctx)?;
731        Ok(EncodedTime::new_unchecked(raw))
732    }
733}
734
735impl<S> ConversionTarget<S> for GPS
736where
737    S: crate::scale::Scale + InfallibleScaleConvert<TAI>,
738{
739    type Output = EncodedTime<TAI, GPS>;
740
741    #[inline]
742    fn try_convert(src: Time<S>) -> Result<Self::Output, ConversionError> {
743        Ok(Self::convert(src))
744    }
745}
746
747impl<S> InfallibleConversionTarget<S> for GPS
748where
749    S: crate::scale::Scale + InfallibleScaleConvert<TAI>,
750{
751    #[inline]
752    fn convert(src: Time<S>) -> Self::Output {
753        EncodedTime::from_time_infallible(src.to_scale::<TAI>())
754    }
755}
756
757impl<S> ContextConversionTarget<S> for GPS
758where
759    S: crate::scale::Scale + crate::scale::conversion::ContextScaleConvert<TAI>,
760{
761    type Output = EncodedTime<TAI, GPS>;
762
763    #[inline]
764    fn convert_with(src: Time<S>, ctx: &TimeContext) -> Result<Self::Output, ConversionError> {
765        let tai = src.to_scale_with::<TAI>(ctx)?;
766        Ok(EncodedTime::from_time_infallible(tai))
767    }
768}
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773    use crate::data::active::{active_time_data, with_test_time_data};
774    use crate::scale::{TAI, TT, UT1, UTC};
775
776    #[test]
777    fn encoded_time_display_delegates_to_quantity() {
778        let jd = JulianDate::<TT>::try_new(Day::new(2_451_545.123_456_789)).unwrap();
779
780        assert_eq!(format!("{jd:.9}"), "Julian Day 2451545.123456789 d");
781    }
782
783    #[test]
784    fn encoded_time_lower_exp_delegates_to_quantity() {
785        let seconds = J2000Seconds::<TT>::try_new(Second::new(1_234.5)).unwrap();
786        let formatted = format!("{seconds:.2e}");
787
788        assert!(formatted.contains("e"));
789        assert!(formatted.ends_with(" s"));
790    }
791
792    #[test]
793    #[allow(clippy::clone_on_copy)]
794    fn encoded_time_core_helpers_and_day_arithmetic() {
795        let base = JulianDate::<TT>::try_new(Day::new(2_451_545.0)).unwrap();
796        let later = JulianDate::<TT>::try_new(Day::new(2_451_547.0)).unwrap();
797
798        assert_eq!(base.clone(), base);
799        assert!(format!("{base:?}").contains("Julian Day"));
800        assert!(format!("{base:.2E}").ends_with(" d"));
801        assert_eq!(base.raw(), Day::new(2_451_545.0));
802        assert_eq!(base.quantity(), base.raw());
803        assert_eq!(base.jd_value(), 2_451_545.0);
804        assert_eq!(base.julian_centuries(), 0.0);
805        assert_eq!(base.julian_millennias(), 0.0);
806
807        assert_eq!(base.min(later), base);
808        assert_eq!(later.min(base), base);
809        assert_eq!(base.max(later), later);
810        assert_eq!(later.max(base), later);
811        assert_eq!(base.mean(later).raw(), Day::new(2_451_546.0));
812
813        assert_eq!((base + Day::new(2.0)).raw(), later.raw());
814        assert_eq!((later - Day::new(2.0)).raw(), base.raw());
815        assert_eq!(later - base, Day::new(2.0));
816
817        let mut shifted = base;
818        shifted += Day::new(3.0);
819        shifted -= Day::new(1.0);
820        assert_eq!(shifted, later);
821
822        let mjd = ModifiedJulianDate::<TT>::new(51_544.5);
823        assert_eq!(mjd.mjd_value(), 51_544.5);
824        assert_eq!(JulianDate::<TT>::from(mjd).raw(), base.raw());
825        assert_eq!(
826            ModifiedJulianDate::<TT>::from(base).raw(),
827            Day::new(51_544.5)
828        );
829    }
830
831    #[test]
832    fn encoded_time_conversion_helpers_cover_targets() {
833        let ctx = TimeContext::new();
834        let seconds = J2000Seconds::<TT>::try_new(Second::new(86_400.0)).unwrap();
835
836        let time = seconds.try_to_time().unwrap();
837        assert_eq!(seconds.to_time_with(&ctx).unwrap(), time);
838        assert_eq!(seconds.to_time(), time);
839
840        let jd = seconds.to::<JD>();
841        let mjd = seconds.try_to::<MJD>().unwrap();
842        let ut1 = seconds.to_with::<UT1>(&ctx).unwrap();
843        assert!(ut1.raw_seconds_pair().0.is_finite());
844
845        assert_eq!(jd.raw(), Day::new(2_451_546.0));
846        assert_eq!(mjd.raw(), Day::new(51_545.5));
847
848        let time_from_encoded: Time<TT> = seconds.into();
849        let encoded_from_time: J2000Seconds<TT> = time_from_encoded.into();
850        assert_eq!(
851            encoded_from_time,
852            J2000Seconds::<TT>::try_new(Second::new(86_400.0)).unwrap()
853        );
854
855        assert!(J2000Seconds::<TT>::try_new(Second::new(f64::NAN)).is_err());
856    }
857
858    #[test]
859    fn gps_and_unix_encoded_representations_roundtrip() {
860        let ctx = TimeContext::new();
861        let utc =
862            Time::<UTC>::from_raw_unix_seconds_with(Second::new(946_728_000.0), &ctx).unwrap();
863
864        let unix = utc.to_with::<Unix>(&ctx).unwrap();
865        let utc_from_unix = unix.to_time_with(&ctx).unwrap();
866        assert!((utc_from_unix - utc).abs() < Second::new(1e-4));
867
868        let ut1_from_unix = unix.to_with::<UT1>(&ctx).unwrap();
869        assert!(ut1_from_unix.raw_seconds_pair().0.is_finite());
870
871        let tai = utc.to::<TAI>();
872        let gps = tai.try_to::<GPS>().unwrap();
873        assert_eq!(gps.try_to_time().unwrap(), tai);
874        assert_eq!(gps.to_time_with(&ctx).unwrap(), tai);
875
876        let gps_as_jd = gps.try_to::<JD>().unwrap();
877        assert!(gps_as_jd.raw().is_finite());
878    }
879
880    #[test]
881    fn tt_jd_and_mjd_chrono_helpers_roundtrip() {
882        let bundle = active_time_data().as_ref().clone();
883        with_test_time_data(bundle, || {
884            let dt = chrono::DateTime::from_timestamp(946_728_000, 250_000_000).unwrap();
885
886            let jd = JulianDate::<TT>::from_chrono(dt);
887            let jd_back = jd.to_chrono().unwrap();
888            let jd_delta_ns =
889                jd_back.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap();
890            assert!(jd_delta_ns.abs() < 50_000);
891
892            let mjd = ModifiedJulianDate::<TT>::from_chrono(dt);
893            let mjd_back = mjd.to_chrono().unwrap();
894            let mjd_delta_ns =
895                mjd_back.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap();
896            assert!(mjd_delta_ns.abs() < 50_000);
897
898            assert!(JulianDate::<TT>::J2000.tt_to_tdb().raw().is_finite());
899        });
900    }
901}