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