tempoch_core/model/
civil.rs1use crate::data::runtime_data::{
8 time_data_tai_seconds_from_utc, time_data_tai_seconds_is_in_leap_window,
9 time_data_try_tai_minus_utc_mjd, time_data_utc_from_tai_seconds,
10};
11use crate::earth::context::TimeContext;
12use crate::encoding::{day_to_j2000_seconds, unix_seconds_to_mjd};
13use crate::format::TimeFormat;
14use crate::format::MJD;
15use crate::foundation::constats::gps_epoch_tai_seconds;
16use crate::foundation::error::ConversionError;
17use crate::model::scale::{TAI, UTC};
18use crate::model::time::Time;
19use chrono::{DateTime, Utc};
20use qtty::Second;
21
22impl<F: TimeFormat> Time<UTC, F> {
23 #[inline]
26 pub fn try_from_chrono_with(
27 dt: DateTime<Utc>,
28 ctx: &TimeContext,
29 ) -> Result<Time<UTC, crate::format::J2000s>, ConversionError> {
30 let tai_secs =
31 time_data_tai_seconds_from_utc(ctx.time_data(), dt, ctx.allows_pre_definition_utc())?;
32 Time::<UTC, crate::format::J2000s>::try_from_raw_j2000_seconds_split(
33 tai_secs,
34 Second::new(0.0),
35 )
36 }
37
38 #[inline]
45 pub fn try_from_chrono(
46 dt: DateTime<Utc>,
47 ) -> Result<Time<UTC, crate::format::J2000s>, ConversionError> {
48 Self::try_from_chrono_with(dt, &TimeContext::new())
49 }
50
51 #[track_caller]
54 #[inline]
55 pub fn from_chrono_with(
56 dt: DateTime<Utc>,
57 ctx: &TimeContext,
58 ) -> Time<UTC, crate::format::J2000s> {
59 Self::try_from_chrono_with(dt, ctx)
60 .expect("UTC conversion failed; use try_from_chrono_with")
61 }
62
63 #[track_caller]
69 #[inline]
70 pub fn from_chrono(dt: DateTime<Utc>) -> Time<UTC, crate::format::J2000s> {
71 Self::try_from_chrono(dt).expect("UTC conversion failed; use try_from_chrono")
72 }
73
74 #[inline]
77 pub fn try_to_chrono_with(self, ctx: &TimeContext) -> Result<DateTime<Utc>, ConversionError> {
78 time_data_utc_from_tai_seconds(
79 ctx.time_data(),
80 self.to_j2000s().total_seconds(),
81 ctx.allows_pre_definition_utc(),
82 )
83 }
84
85 #[inline]
92 pub fn try_to_chrono(self) -> Result<DateTime<Utc>, ConversionError> {
93 self.try_to_chrono_with(&TimeContext::new())
94 }
95
96 #[inline]
99 pub fn to_chrono_with(self, ctx: &TimeContext) -> Option<DateTime<Utc>> {
100 self.try_to_chrono_with(ctx).ok()
101 }
102
103 #[inline]
109 pub fn to_chrono(self) -> Option<DateTime<Utc>> {
110 self.try_to_chrono().ok()
111 }
112
113 #[inline]
116 pub(crate) fn from_raw_unix_seconds_with(
117 seconds: Second,
118 ctx: &TimeContext,
119 ) -> Result<Time<UTC, crate::format::J2000s>, ConversionError> {
120 if seconds.value().is_nan() {
121 return Err(ConversionError::NonFinite);
122 }
123 let mjd_utc = unix_seconds_to_mjd(seconds);
124 let tai_minus_utc = time_data_try_tai_minus_utc_mjd(
125 ctx.time_data(),
126 mjd_utc,
127 ctx.allows_pre_definition_utc(),
128 )?;
129 let tai_secs = day_to_j2000_seconds::<MJD>(mjd_utc) + tai_minus_utc;
130 Time::<UTC, crate::format::J2000s>::try_from_raw_j2000_seconds_split(
131 tai_secs,
132 Second::new(0.0),
133 )
134 }
135
136 #[inline]
139 pub(crate) fn raw_unix_seconds_with(
140 self,
141 ctx: &TimeContext,
142 ) -> Result<Second, ConversionError> {
143 if self.to_j2000s().is_leap_second_with(ctx) {
144 return Err(ConversionError::InvalidLeapSecond);
145 }
146 let dt = self.try_to_chrono_with(ctx)?;
147 let nanos = dt.timestamp_subsec_nanos();
148 Ok(Second::new(dt.timestamp() as f64 + nanos as f64 / 1e9))
149 }
150
151 #[inline]
154 pub fn is_leap_second_with(self, ctx: &TimeContext) -> bool {
155 time_data_tai_seconds_is_in_leap_window(ctx.time_data(), self.to_j2000s().total_seconds())
156 }
157
158 #[inline]
165 pub fn is_leap_second(self) -> bool {
166 self.is_leap_second_with(&TimeContext::new())
167 }
168}
169
170impl<F: TimeFormat> Time<TAI, F> {
171 #[inline]
173 pub(crate) fn from_raw_gps_seconds(
174 seconds: Second,
175 ) -> Result<Time<TAI, crate::format::J2000s>, ConversionError> {
176 if seconds.value().is_nan() {
177 return Err(ConversionError::NonFinite);
178 }
179 Time::<TAI, crate::format::J2000s>::try_from_raw_j2000_seconds_split(
180 seconds + gps_epoch_tai_seconds(),
181 Second::new(0.0),
182 )
183 }
184
185 #[inline]
187 pub(crate) fn raw_gps_seconds(self) -> Second {
188 self.to_j2000s().total_seconds() - gps_epoch_tai_seconds()
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::data::runtime_data::{active_time_data, with_test_time_data};
196
197 #[test]
198 fn chrono_convenience_wrappers_roundtrip_with_context() {
199 let bundle = active_time_data().as_ref().clone();
200 with_test_time_data(bundle, || {
201 let ctx = TimeContext::new();
202 let dt = DateTime::from_timestamp(946_728_000, 125_000_000).unwrap();
203
204 let with_ctx = Time::<UTC>::from_chrono_with(dt, &ctx);
205 let default_ctx = Time::<UTC>::from_chrono(dt);
206 assert_eq!(with_ctx, default_ctx);
207
208 let back_with_ctx = with_ctx.to_chrono_with(&ctx).unwrap();
209 let back_default = with_ctx.to_chrono().unwrap();
210 let with_ctx_delta_ns =
211 back_with_ctx.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap();
212 let default_delta_ns =
213 back_default.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap();
214
215 assert!(with_ctx_delta_ns.abs() < 50_000);
216 assert!(default_delta_ns.abs() < 50_000);
217 });
218 }
219
220 #[test]
221 fn gps_raw_seconds_reject_nan_and_roundtrip_finite() {
222 assert!(matches!(
223 Time::<TAI>::from_raw_gps_seconds(Second::new(f64::NAN)),
224 Err(ConversionError::NonFinite)
225 ));
226
227 let tai = Time::<TAI>::from_raw_gps_seconds(Second::new(123.5)).unwrap();
228 assert_eq!(tai.raw_gps_seconds(), Second::new(123.5));
229 }
230}