1pub mod civil;
21pub mod eop;
22pub mod gnss;
23pub mod model;
24pub mod scales;
25
26pub use crate::astro::data::iers::Ut1Entry;
27pub use civil::{
28 civil_from_j2000_seconds, civil_from_julian_day_number, civil_from_split_julian_date,
29 day_of_year, day_of_year_int, days_in_month, fractional_day_of_year_from_instant, is_leap_year,
30 j2000_seconds, j2000_seconds_from_split, julian_date_from_instant, mjd_from_jd, second_of_day,
31 second_of_day_from_instant, split_julian_date, split_julian_date_add_seconds,
32 split_julian_date_from_j2000_seconds,
33};
34pub use eop::{
35 CoverageError, DegradeReason, LeapSecondTable, TimeScaleInputErrorKind, Ut1Provenance,
36 Validated, ValidityMode,
37};
38pub use model::{
39 Duration, GnssWeekTow, Instant, InstantRepr, JulianDateSplit, TimeModelError, TimeScale,
40 SECONDS_PER_WEEK,
41};
42pub use scales::{
43 find_leap_seconds, gps_utc_offset_s, leap_second_table, tai_utc_offset_s, tcb_to_tdb_jd,
44 tcg_to_tt_jd, tdb_to_tcb_jd, timescale_offset_at_s, timescale_offset_s, tt_to_tcg_jd,
45 LeapSecondEntry, TimeOffsetError, TimeOffsetErrorCode, TimeScales, TimeTables,
46 GLONASST_MINUS_UTC_S, TCG_TCB_REFERENCE_JD, TDB_TCB_OFFSET_TDB0_S, TDB_TCB_RATE_L_B,
47 TT_TCG_RATE_L_G,
48};
49
50#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
55pub struct Time {
56 pub seconds_since_j2000: f64,
57}
58
59impl Time {
60 pub fn new(seconds_since_j2000: f64) -> Result<Self, TimeModelError> {
61 if !seconds_since_j2000.is_finite() {
62 return Err(TimeModelError::InvalidInput {
63 field: "seconds_since_j2000",
64 reason: "must be finite",
65 });
66 }
67 Ok(Self {
68 seconds_since_j2000,
69 })
70 }
71
72 pub fn tdb(&self) -> f64 {
73 self.seconds_since_j2000
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn week_tow_normalizes_overflow() {
83 let wt = GnssWeekTow::new(TimeScale::Gpst, 100, SECONDS_PER_WEEK + 5.0)
84 .expect("valid week/TOW")
85 .normalized()
86 .expect("valid normalized week/TOW");
87 assert_eq!(wt.week, 101);
88 assert!((wt.tow_s - 5.0).abs() < 1e-9);
89 }
90
91 #[test]
92 fn week_tow_borrows_negative() {
93 let wt = GnssWeekTow::new(TimeScale::Gpst, 100, -10.0)
94 .expect("valid week/TOW")
95 .normalized()
96 .expect("valid normalized week/TOW");
97 assert_eq!(wt.week, 99);
98 assert!((wt.tow_s - (SECONDS_PER_WEEK - 10.0)).abs() < 1e-6);
99 }
100
101 #[test]
102 fn week_rollover_unrolls() {
103 let wt = GnssWeekTow::new(TimeScale::Gpst, 10, 0.0).expect("valid week/TOW");
104 assert_eq!(wt.unrolled_week(2).expect("valid unrolled week"), 10 + 2048);
105 }
106
107 #[test]
108 fn scalar_time_rejects_nonfinite_epoch_seconds() {
109 assert!(Time::new(f64::NAN).is_err());
110 assert!(Time::new(f64::INFINITY).is_err());
111 assert!(Time::new(f64::NEG_INFINITY).is_err());
112 }
113
114 #[test]
115 fn scalar_time_valid_epoch_is_unchanged() {
116 let t = Time::new(123.25).expect("valid scalar time");
117 assert_eq!(t.seconds_since_j2000, 123.25);
118 assert_eq!(t.tdb(), 123.25);
119 }
120
121 #[test]
122 fn ut1_coverage_strict_vs_permissive() {
123 let prov = scales::ut1_coverage();
124 let mid = (prov.first_jd_tt + prov.last_jd_tt) / 2.0;
126 assert_eq!(
127 eop::check_ut1_coverage(&prov, mid, ValidityMode::Strict),
128 Ok(None)
129 );
130 let before = prov.first_jd_tt - 1.0;
132 assert!(eop::check_ut1_coverage(&prov, before, ValidityMode::Strict).is_err());
133 assert_eq!(
134 eop::check_ut1_coverage(&prov, before, ValidityMode::Permissive),
135 Ok(Some(DegradeReason::BeforeCoverage))
136 );
137 }
138
139 #[test]
140 fn time_scales_from_utc_unchanged_shape() {
141 let ts = TimeScales::from_utc(2000, 1, 1, 12, 0, 0.0).expect("valid UTC instant");
143 assert!((ts.jd_tt - 2451545.0).abs() < 1e-3);
144 }
145
146 #[test]
147 fn time_scales_from_utc_rejects_non_finite_seconds() {
148 let err = TimeScales::from_utc(2000, 1, 1, 12, 0, f64::NAN)
149 .expect_err("non-finite second must error before time-scale arithmetic");
150 assert_eq!(
151 err,
152 CoverageError::InvalidInput {
153 field: "second",
154 kind: TimeScaleInputErrorKind::NonFinite
155 }
156 );
157
158 let err =
159 TimeScales::from_utc_validated(2000, 1, 1, 12, 0, f64::INFINITY, ValidityMode::Strict)
160 .expect_err("validated path must reject non-finite seconds before coverage checks");
161 assert_eq!(
162 err,
163 CoverageError::InvalidInput {
164 field: "second",
165 kind: TimeScaleInputErrorKind::NonFinite
166 }
167 );
168 }
169
170 #[test]
171 fn time_scales_from_utc_rejects_invalid_civil_datetime() {
172 let err = TimeScales::from_utc(2001, 2, 29, 12, 0, 0.0)
173 .expect_err("invalid civil date must error before time-scale arithmetic");
174 assert_eq!(
175 err,
176 CoverageError::InvalidInput {
177 field: "civil datetime",
178 kind: TimeScaleInputErrorKind::InvalidCivilDate,
179 }
180 );
181
182 let err = TimeScales::from_utc_validated(2000, 1, 1, 24, 0, 0.0, ValidityMode::Strict)
183 .expect_err("invalid civil time must error before coverage checks");
184 assert_eq!(
185 err,
186 CoverageError::InvalidInput {
187 field: "civil datetime",
188 kind: TimeScaleInputErrorKind::InvalidCivilTime,
189 }
190 );
191 }
192
193 #[test]
194 fn time_scales_from_utc_maps_positive_leap_second_between_neighbors() {
195 fn tt_delta_seconds(later: TimeScales, earlier: TimeScales) -> f64 {
196 (later.jd_whole - earlier.jd_whole) * crate::constants::SECONDS_PER_DAY
197 + (later.tt_fraction - earlier.tt_fraction) * crate::constants::SECONDS_PER_DAY
198 }
199
200 let before = TimeScales::from_utc(2016, 12, 31, 23, 59, 59.0).expect("leap eve second");
201 let leap = TimeScales::from_utc(2016, 12, 31, 23, 59, 60.0).expect("inserted leap second");
202 let after = TimeScales::from_utc(2017, 1, 1, 0, 0, 0.0).expect("post-leap midnight");
203
204 assert!(
205 (tt_delta_seconds(leap, before) - 1.0).abs() < 1.0e-8,
206 "leap label must be one SI second after :59"
207 );
208 assert!(
209 (tt_delta_seconds(after, leap) - 1.0).abs() < 1.0e-8,
210 "post-leap midnight must be one SI second after :60"
211 );
212 assert_ne!(leap, after, "leap second must not collapse onto midnight");
213
214 assert!(TimeScales::from_utc(2017, 1, 1, 0, 0, 60.0).is_err());
215 assert!(TimeScales::from_utc(2016, 12, 31, 23, 59, 61.0).is_err());
216 assert!(TimeScales::from_utc(2016, 12, 31, 23, 59, -1.0).is_err());
217 }
218
219 #[test]
220 fn from_utc_validated_in_coverage_is_bit_identical_and_not_degraded() {
221 let plain = TimeScales::from_utc(2000, 1, 1, 12, 0, 0.0).expect("valid UTC instant");
223 for mode in [ValidityMode::Strict, ValidityMode::Permissive] {
224 let v = TimeScales::from_utc_validated(2000, 1, 1, 12, 0, 0.0, mode)
225 .expect("in-coverage instant must not error in either mode");
226 assert_eq!(v.degraded, None, "in-coverage must not be degraded");
227 assert_eq!(
229 v.value, plain,
230 "validated numerics must equal from_utc bit-for-bit"
231 );
232 }
233 }
234
235 #[test]
236 fn from_utc_validated_strict_errors_before_coverage() {
237 let prov = scales::ut1_coverage();
238 let (y, m, d) = (1960, 1, 1);
240 let plain = TimeScales::from_utc(y, m, d, 0, 0, 0.0).expect("valid UTC instant");
241 assert!(
242 plain.jd_tt < prov.first_jd_tt,
243 "fixture must be before coverage"
244 );
245
246 let err = TimeScales::from_utc_validated(y, m, d, 0, 0, 0.0, ValidityMode::Strict)
247 .expect_err("strict mode must error before coverage");
248 assert_eq!(
249 err,
250 CoverageError::OutsideCoverage(DegradeReason::BeforeCoverage)
251 );
252 }
253
254 #[test]
255 fn from_utc_validated_strict_errors_after_coverage() {
256 let prov = scales::ut1_coverage();
257 let (y, m, d) = (2100, 1, 1);
259 let plain = TimeScales::from_utc(y, m, d, 0, 0, 0.0).expect("valid UTC instant");
260 assert!(
261 plain.jd_tt > prov.last_jd_tt,
262 "fixture must be after coverage"
263 );
264
265 let err = TimeScales::from_utc_validated(y, m, d, 0, 0, 0.0, ValidityMode::Strict)
266 .expect_err("strict mode must error after coverage");
267 assert_eq!(
268 err,
269 CoverageError::OutsideCoverage(DegradeReason::AfterCoverage)
270 );
271 }
272
273 #[test]
274 fn from_utc_validated_permissive_clamps_and_marks_degraded() {
275 let plain_before = TimeScales::from_utc(1960, 1, 1, 0, 0, 0.0).expect("valid UTC instant");
278 let before =
279 TimeScales::from_utc_validated(1960, 1, 1, 0, 0, 0.0, ValidityMode::Permissive)
280 .expect("permissive must not error");
281 assert_eq!(before.degraded, Some(DegradeReason::BeforeCoverage));
282 assert_eq!(before.value, plain_before);
283
284 let plain_after = TimeScales::from_utc(2100, 1, 1, 0, 0, 0.0).expect("valid UTC instant");
286 let after = TimeScales::from_utc_validated(2100, 1, 1, 0, 0, 0.0, ValidityMode::Permissive)
287 .expect("permissive must not error");
288 assert_eq!(after.degraded, Some(DegradeReason::AfterCoverage));
289 assert_eq!(after.value, plain_after);
290 }
291}