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