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