Skip to main content

deep_time/dt/
julian_date.rs

1use crate::{
2    ATTOS_PER_DAY, ATTOS_PER_HALF_DAY, ATTOS_PER_SEC_I128, Dt, J2000_JD_TT, JD_EPOCH_DAYS,
3    MJD_1970, Real, SEC_PER_DAYI64, Scale, TSpan, clamp_i128_to_i64,
4};
5
6impl Dt {
7    /// Returns the exact Julian Date of this instant as `(integer_days, fractional_attoseconds)`.
8    ///
9    /// The fractional part is always in `[0, ATTOS_PER_DAY)`.
10    ///
11    /// ### Behavior by `scale`
12    ///
13    /// - **`UTC`, `UTCSofa`, `UTCSpice`**: Computes **JD(UTC)** using the Unix epoch
14    ///   (1970-01-01 00:00:00 UTC) as reference. This produces the Julian Date that
15    ///   corresponds to the civil UTC clock reading (the value used by IERS C04 tables,
16    ///   most astronomy software, and online JD calculators).
17    ///
18    /// - **All other types** (TAI, TT, TDB, GPS, TCG, etc.): Computes **JD(TT)** (or
19    ///   equivalent uniform scale) using the J2000.0 TT epoch (`J2000_JD_TT = 2451545`).
20    ///   This is the continuous, leap-second-free value used for ephemerides and
21    ///   dynamical calculations.
22    ///
23    /// The returned value therefore depends on both the physical instant *and* the
24    /// declared time scale of `self`.
25    ///
26    /// # Precision
27    /// Exact (attosecond resolution). Use [`to_jd`](Self::to_jd) for the floating-point
28    /// version.
29    pub const fn to_jd_exact(self, target: Scale) -> (i64, u128) {
30        if target.is_ut() {
31            let canon_attos = self.to_tai_attos_since(Dt::UNIX_EPOCH);
32            let total_attos = canon_attos.saturating_add(ATTOS_PER_HALF_DAY);
33
34            let days_since_1970 = total_attos.div_euclid(ATTOS_PER_DAY);
35            let frac_attos = total_attos.rem_euclid(ATTOS_PER_DAY) as u128;
36            let days_i64 = clamp_i128_to_i64(days_since_1970);
37
38            let jd_int = 2_440_587i64.saturating_add(days_i64);
39            (jd_int, frac_attos)
40        } else {
41            let TSpan { sec, attos } = self.to(target);
42            let days_since_j2000 = sec.div_euclid(SEC_PER_DAYI64);
43            let remaining_sec = sec.rem_euclid(SEC_PER_DAYI64);
44
45            let frac_attos = (remaining_sec as u128) * ATTOS_PER_SEC_I128 as u128 + (attos as u128);
46
47            let jd_int = J2000_JD_TT.saturating_add(days_since_j2000);
48            (jd_int, frac_attos)
49        }
50    }
51
52    /// Returns the Julian Date of this instant as a floating-point `Real` (`f64`).
53    ///
54    /// This is the lossy counterpart to [`to_jd_exact`](Self::to_jd_exact).
55    /// See that method for the exact scale-dependent behavior (JD(UTC) vs JD(TT)).
56    #[inline]
57    pub const fn to_jd(self, target: Scale) -> Real {
58        let (days, attos) = self.to_jd_exact(target);
59        f!(days) + f!(attos) / f!(ATTOS_PER_DAY)
60    }
61
62    /// Returns the exact Modified Julian Date of this instant as `(integer_days, fractional_attoseconds)`.
63    ///
64    /// The fractional part is always in `[0, ATTOS_PER_DAY)`.
65    ///
66    /// ### Behavior by `scale`
67    ///
68    /// - **`UTC`, `UTCSofa`, `UTCSpice`**: Computes **MJD(UTC)** using the Unix epoch
69    ///   (1970-01-01 00:00:00 UTC). This matches the MJD column in IERS C04 / Bulletin A
70    ///   tables (0h UTC epochs) and most civil/UTC-labeled data products.
71    ///
72    /// - **All other types**: Computes the MJD equivalent of the uniform-scale JD
73    ///   (normally JD(TT) – 2_400_000.5) with proper half-day adjustment.
74    ///
75    /// # Precision
76    /// Exact (attosecond resolution). Use [`to_mjd`](Self::to_mjd) for the floating-point version.
77    pub const fn to_mjd_exact(self, target: Scale) -> (i64, u128) {
78        if target.is_ut() {
79            let canon_attos = self.to_tai_attos_since(Dt::UNIX_EPOCH);
80            let days_since_1970 = canon_attos.div_euclid(ATTOS_PER_DAY);
81            let frac_attos = canon_attos.rem_euclid(ATTOS_PER_DAY) as u128;
82            let days_i64 = clamp_i128_to_i64(days_since_1970);
83
84            let mjd_days = MJD_1970.saturating_add(days_i64);
85            (mjd_days, frac_attos)
86        } else {
87            let (jd_days, frac_attos) = self.to_jd_exact(target);
88
89            let mjd_days = jd_days.saturating_sub(2_400_001);
90            let mjd_attos = frac_attos.saturating_add(ATTOS_PER_HALF_DAY as u128);
91
92            if mjd_attos >= ATTOS_PER_DAY as u128 {
93                (
94                    mjd_days.saturating_add(1),
95                    mjd_attos.saturating_sub(ATTOS_PER_DAY as u128),
96                )
97            } else {
98                (mjd_days, mjd_attos)
99            }
100        }
101    }
102
103    /// Returns the Modified Julian Date of this instant as a floating-point `Real` (`f64`).
104    ///
105    /// This is the lossy counterpart to [`to_mjd_exact`](Self::to_mjd_exact).
106    /// See that method for the exact scale-dependent behavior (MJD(UTC) vs uniform MJD).
107    #[inline]
108    pub const fn to_mjd(self, target: Scale) -> Real {
109        let (days, attos) = self.to_mjd_exact(target);
110        f!(days) + f!(attos) / f!(ATTOS_PER_DAY)
111    }
112
113    /// Creates a `Dt` from an exact Julian Date, interpreting the JD in the
114    /// scale indicated by `orig_type`.
115    ///
116    /// - If `orig_type` is `UTC` / `UTCSofa` / `UTCSpice`, the input JD is treated as
117    ///   **JD(UTC)** and the resulting `Dt` will have the corresponding UTC
118    ///   civil time (leap-second aware).
119    /// - For all other types the input JD is treated as the uniform-scale JD
120    ///   (normally JD(TT)) and the resulting `Dt` is constructed on that scale.
121    ///
122    /// The returned `Dt` represents the physical instant whose JD (in the
123    /// requested scale) matches the input.
124    ///
125    /// # Precision
126    /// Exact (attosecond resolution).
127    pub const fn from_jd_exact(jd_days: i64, frac_attos: u128, orig_type: Scale) -> Self {
128        if orig_type.is_ut() {
129            let delta_days = (jd_days as i128).saturating_sub(JD_EPOCH_DAYS);
130
131            let frac_clamped = if frac_attos > i128::MAX as u128 {
132                i128::MAX
133            } else {
134                frac_attos as i128
135            };
136
137            let canon_attos = delta_days
138                .saturating_mul(ATTOS_PER_DAY)
139                .saturating_add(frac_clamped)
140                .saturating_sub(ATTOS_PER_HALF_DAY);
141
142            Self::from_tai_attos_since(canon_attos, Dt::UNIX_EPOCH)
143        } else {
144            let days_since_j2000 = jd_days.saturating_sub(J2000_JD_TT);
145            let seconds_from_days = days_since_j2000.saturating_mul(SEC_PER_DAYI64);
146
147            let extra_seconds = {
148                let quot = frac_attos / (ATTOS_PER_SEC_I128 as u128);
149                if quot > i64::MAX as u128 {
150                    i64::MAX
151                } else {
152                    quot as i64
153                }
154            };
155
156            let total_sec = seconds_from_days.saturating_add(extra_seconds);
157            let attos = (frac_attos % (ATTOS_PER_SEC_I128 as u128)) as u64;
158
159            Dt::from(total_sec, attos, orig_type)
160        }
161    }
162
163    /// Creates a `Dt` from an exact Modified Julian Date, interpreting the MJD
164    /// in the scale indicated by `orig_type`.
165    ///
166    /// This is the inverse of [`to_mjd_exact`](Self::to_mjd_exact). See that method
167    /// and [`from_jd_exact`](Self::from_jd_exact) for scale-specific behavior.
168    ///
169    /// # Precision
170    /// Exact (attosecond resolution).
171    pub const fn from_mjd_exact(mjd_days: i64, frac_attos: u128, orig_type: Scale) -> Self {
172        let jd_days = mjd_days.saturating_add(2_400_000);
173        let jd_attos = frac_attos.saturating_add(ATTOS_PER_HALF_DAY as u128);
174
175        if jd_attos >= ATTOS_PER_DAY as u128 {
176            Self::from_jd_exact(
177                jd_days.saturating_add(1),
178                jd_attos.saturating_sub(ATTOS_PER_DAY as u128),
179                orig_type,
180            )
181        } else {
182            Self::from_jd_exact(jd_days, jd_attos, orig_type)
183        }
184    }
185}