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}