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