1use crate::foundation::error::ConversionError;
44use crate::model::scale::{CoordinateScale, BDT, GPST, GST, QZSST};
45use crate::model::time::Time;
46
47const SECONDS_PER_WEEK: qtty::i128::Second = qtty::i128::Second::new(7 * 86_400);
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct GnssWeek {
52 pub week: qtty::u32::Week,
54 pub seconds_of_week: qtty::u32::Second,
56 pub subsecond_nanos: qtty::u32::Nanosecond,
58}
59
60impl GnssWeek {
61 pub fn new(
63 week: qtty::u32::Week,
64 seconds_of_week: qtty::u32::Second,
65 subsecond_nanos: qtty::u32::Nanosecond,
66 ) -> Result<Self, ConversionError> {
67 if seconds_of_week.value() as i128 >= SECONDS_PER_WEEK.value()
68 || subsecond_nanos.value() >= 1_000_000_000
69 {
70 return Err(ConversionError::OutOfRange);
71 }
72 Ok(Self {
73 week,
74 seconds_of_week,
75 subsecond_nanos,
76 })
77 }
78
79 pub fn subsecond_nanoseconds_u(&self) -> qtty::u32::Nanosecond {
83 self.subsecond_nanos
84 }
85
86 pub fn seconds_of_week_u(&self) -> qtty::u32::Second {
90 self.seconds_of_week
91 }
92
93 pub fn new_with_nanoseconds_u(
97 week: qtty::u32::Week,
98 seconds_of_week: qtty::u32::Second,
99 subsecond: qtty::u32::Nanosecond,
100 ) -> Result<Self, ConversionError> {
101 Self::new(week, seconds_of_week, subsecond)
102 }
103
104 pub fn to_duration_since_epoch(&self) -> crate::ExactDuration {
106 let week_count = self.week.value() as i128;
107 let sow = self.seconds_of_week.value() as i128;
108 let seconds = week_count * SECONDS_PER_WEEK.value() + sow;
109 let nanos = seconds * 1_000_000_000 + self.subsecond_nanos.value() as i128;
110 crate::ExactDuration::from_nanos(nanos)
111 }
112}
113
114pub trait GnssWeekScale: CoordinateScale {
118 fn epoch_j2000_seconds() -> f64;
122
123 fn rollover_period_weeks() -> u32;
126}
127
128const GPST_EPOCH_J2000_SECONDS: f64 = -630_763_200.0;
138const GST_EPOCH_J2000_SECONDS: f64 = -11_447_987.0;
139const BDT_EPOCH_J2000_SECONDS: f64 = 189_345_600.0;
140const QZSST_EPOCH_J2000_SECONDS: f64 = GPST_EPOCH_J2000_SECONDS;
141
142impl GnssWeekScale for GPST {
143 fn epoch_j2000_seconds() -> f64 {
144 GPST_EPOCH_J2000_SECONDS
145 }
146 fn rollover_period_weeks() -> u32 {
147 1024
148 }
149}
150impl GnssWeekScale for GST {
151 fn epoch_j2000_seconds() -> f64 {
152 GST_EPOCH_J2000_SECONDS
153 }
154 fn rollover_period_weeks() -> u32 {
155 4096
156 }
157}
158impl GnssWeekScale for BDT {
159 fn epoch_j2000_seconds() -> f64 {
160 BDT_EPOCH_J2000_SECONDS
161 }
162 fn rollover_period_weeks() -> u32 {
163 8192
164 }
165}
166impl GnssWeekScale for QZSST {
167 fn epoch_j2000_seconds() -> f64 {
168 QZSST_EPOCH_J2000_SECONDS
169 }
170 fn rollover_period_weeks() -> u32 {
171 1024
172 }
173}
174
175impl<S: GnssWeekScale> Time<S> {
176 pub fn to_gnss_week(&self) -> Result<GnssWeek, ConversionError> {
187 let (hi, lo) = self.to_j2000s().raw_seconds_pair();
188 let hi_val = hi.value();
189 let lo_val = lo.value();
190
191 let hi_int = hi_val.round();
193 let sub_sec = (hi_val - hi_int) + lo_val;
195
196 let epoch_i128 = S::epoch_j2000_seconds() as i128;
198 let hi_i128 = hi_int as i64 as i128;
200 let mut secs_since_epoch = hi_i128 - epoch_i128;
201
202 let raw_nanos = (sub_sec * 1.0e9).round() as i64;
204 let sub_nanos = if raw_nanos < 0 {
205 secs_since_epoch -= 1;
206 (raw_nanos + 1_000_000_000) as u32
207 } else if raw_nanos >= 1_000_000_000 {
208 secs_since_epoch += 1;
209 (raw_nanos - 1_000_000_000) as u32
210 } else {
211 raw_nanos as u32
212 };
213
214 if secs_since_epoch < 0 {
215 return Err(ConversionError::OutOfRange);
216 }
217
218 let total_secs = secs_since_epoch as u64;
219 let week_u64 = total_secs / SECONDS_PER_WEEK.value() as u64;
220 if week_u64 > u32::MAX as u64 {
221 return Err(ConversionError::OutOfRange);
222 }
223 let week = week_u64 as u32;
224 let seconds_of_week = (total_secs % SECONDS_PER_WEEK.value() as u64) as u32;
225
226 Ok(GnssWeek {
227 week: qtty::u32::Week::new(week),
228 seconds_of_week: qtty::u32::Second::new(seconds_of_week),
229 subsecond_nanos: qtty::u32::Nanosecond::new(sub_nanos),
230 })
231 }
232
233 pub fn from_gnss_week(gw: GnssWeek) -> Result<Self, ConversionError> {
240 let epoch = Time::<S>::from_raw_j2000_seconds(qtty::Second::new(S::epoch_j2000_seconds()))?;
241 Ok(epoch.add_exact(gw.to_duration_since_epoch()))
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::format::iso::parse_rfc3339_utc;
249
250 #[test]
251 fn gps_epoch_is_week_zero_second_zero() {
252 let utc = parse_rfc3339_utc("1980-01-06T00:00:00Z").unwrap();
253 let gpst: Time<GPST> = utc.to::<GPST>();
254 let gw = gpst.to_gnss_week().unwrap();
255 assert_eq!(gw.week.value(), 0, "expected week 0, got {gw:?}");
256 assert_eq!(gw.seconds_of_week.value(), 0, "expected sow=0, got {gw:?}");
257 assert_eq!(gw.subsecond_nanos.value(), 0, "expected ns=0, got {gw:?}");
258 }
259
260 #[test]
261 fn galileo_epoch_is_week_zero_second_zero() {
262 let utc = parse_rfc3339_utc("1999-08-22T00:00:00Z").unwrap();
263 let gst: Time<GST> = utc.to::<GST>();
264 let gw = gst.to_gnss_week().unwrap();
265 assert_eq!(gw.week.value(), 0, "expected GST week 0, got {gw:?}");
266 assert_eq!(gw.seconds_of_week.value(), 0, "expected sow=0, got {gw:?}");
267 assert_eq!(gw.subsecond_nanos.value(), 0, "expected ns=0, got {gw:?}");
268 }
269
270 #[test]
271 fn beidou_epoch_is_week_zero_second_zero() {
272 let utc = parse_rfc3339_utc("2006-01-01T00:00:00Z").unwrap();
273 let bdt: Time<BDT> = utc.to::<BDT>();
274 let gw = bdt.to_gnss_week().unwrap();
275 assert_eq!(gw.week.value(), 0, "expected BDT week 0, got {gw:?}");
276 assert_eq!(gw.seconds_of_week.value(), 0, "expected sow=0, got {gw:?}");
277 assert_eq!(gw.subsecond_nanos.value(), 0, "expected ns=0, got {gw:?}");
278 }
279
280 #[test]
281 fn qzsst_aligned_with_gpst() {
282 let utc = parse_rfc3339_utc("1980-01-06T00:00:00Z").unwrap();
283 let q: Time<QZSST> = utc.to::<QZSST>();
284 let gp: Time<GPST> = utc.to::<GPST>();
285 let qw = q.to_gnss_week().unwrap();
286 let gw = gp.to_gnss_week().unwrap();
287 assert_eq!(qw.week, gw.week);
288 assert_eq!(qw.seconds_of_week, gw.seconds_of_week);
289 assert_eq!(qw.subsecond_nanos, gw.subsecond_nanos);
290 }
291
292 #[test]
296 fn gps_week_round_trip_nanosecond_accurate() {
297 let gw = GnssWeek::new(
298 qtty::u32::Week::new(2200),
299 qtty::u32::Second::new(345_600),
300 qtty::u32::Nanosecond::new(123_456_789),
301 )
302 .unwrap();
303 let t = Time::<GPST>::from_gnss_week(gw).unwrap();
304 let back = t.to_gnss_week().unwrap();
305 assert_eq!(back.week, gw.week, "week mismatch: {back:?} vs {gw:?}");
306 assert_eq!(
307 back.seconds_of_week, gw.seconds_of_week,
308 "sow mismatch: {back:?} vs {gw:?}"
309 );
310 let ns_delta =
313 (back.subsecond_nanos.value() as i64 - gw.subsecond_nanos.value() as i64).abs();
314 assert!(
315 ns_delta <= 200,
316 "subsecond_nanos drift {ns_delta} ns: {back:?} vs {gw:?}"
317 );
318 }
319
320 #[test]
322 fn gps_week_boundary() {
323 let gw = GnssWeek::new(
324 qtty::u32::Week::new(2200),
325 qtty::u32::Second::new(604_799),
326 qtty::u32::Nanosecond::new(999_999_999),
327 )
328 .unwrap();
329 let t = Time::<GPST>::from_gnss_week(gw).unwrap();
330 let back = t.to_gnss_week().unwrap();
331 assert_eq!(back.week, gw.week, "week mismatch at boundary: {back:?}");
332 assert_eq!(
333 back.seconds_of_week, gw.seconds_of_week,
334 "sow mismatch at boundary: {back:?}"
335 );
336 let ns_delta =
337 (back.subsecond_nanos.value() as i64 - gw.subsecond_nanos.value() as i64).abs();
338 assert!(
339 ns_delta <= 200,
340 "subsecond_nanos drift {ns_delta} ns at boundary: {back:?}"
341 );
342 }
343
344 #[test]
346 fn gps_week_1024_no_rollover() {
347 let gw = GnssWeek::new(
348 qtty::u32::Week::new(1024),
349 qtty::u32::Second::new(0),
350 qtty::u32::Nanosecond::new(0),
351 )
352 .unwrap();
353 let t = Time::<GPST>::from_gnss_week(gw).unwrap();
354 let back = t.to_gnss_week().unwrap();
355 assert_eq!(back.week.value(), 1024);
356 assert_eq!(back.seconds_of_week.value(), 0);
357 assert_eq!(back.subsecond_nanos.value(), 0);
358 }
359
360 #[test]
362 fn gps_week_2048_no_rollover() {
363 let gw = GnssWeek::new(
364 qtty::u32::Week::new(2048),
365 qtty::u32::Second::new(0),
366 qtty::u32::Nanosecond::new(0),
367 )
368 .unwrap();
369 let t = Time::<GPST>::from_gnss_week(gw).unwrap();
370 let back = t.to_gnss_week().unwrap();
371 assert_eq!(back.week.value(), 2048);
372 assert_eq!(back.seconds_of_week.value(), 0);
373 assert_eq!(back.subsecond_nanos.value(), 0);
374 }
375
376 #[test]
377 fn rollover_periods_are_documented() {
378 assert_eq!(<GPST as GnssWeekScale>::rollover_period_weeks(), 1024);
379 assert_eq!(<GST as GnssWeekScale>::rollover_period_weeks(), 4096);
380 assert_eq!(<BDT as GnssWeekScale>::rollover_period_weeks(), 8192);
381 assert_eq!(<QZSST as GnssWeekScale>::rollover_period_weeks(), 1024);
382 }
383
384 #[test]
385 fn out_of_range_inputs_rejected() {
386 assert!(GnssWeek::new(
387 qtty::u32::Week::new(0),
388 qtty::u32::Second::new(604_800),
389 qtty::u32::Nanosecond::new(0),
390 )
391 .is_err());
392 assert!(GnssWeek::new(
393 qtty::u32::Week::new(0),
394 qtty::u32::Second::new(0),
395 qtty::u32::Nanosecond::new(1_000_000_000),
396 )
397 .is_err());
398 }
399
400 #[test]
401 fn subsecond_nanoseconds_u_matches_field() {
402 let gw = GnssWeek::new(
403 qtty::u32::Week::new(100),
404 qtty::u32::Second::new(12_345),
405 qtty::u32::Nanosecond::new(987_654_321),
406 )
407 .unwrap();
408 assert_eq!(gw.subsecond_nanoseconds_u().value(), 987_654_321_u32);
409 }
410
411 #[test]
412 fn new_with_nanoseconds_u_accepts_valid() {
413 let ns = qtty::u32::Nanosecond::new(123_456_789);
414 let gw = GnssWeek::new_with_nanoseconds_u(
415 qtty::u32::Week::new(500),
416 qtty::u32::Second::new(100_000),
417 ns,
418 )
419 .unwrap();
420 assert_eq!(gw.subsecond_nanos.value(), 123_456_789);
421 }
422
423 #[test]
424 fn new_with_nanoseconds_u_rejects_invalid() {
425 let big = qtty::u32::Nanosecond::new(1_000_000_000);
427 assert!(GnssWeek::new_with_nanoseconds_u(
428 qtty::u32::Week::new(0),
429 qtty::u32::Second::new(0),
430 big,
431 )
432 .is_err());
433 }
434
435 #[test]
436 fn to_gnss_week_overflow_returns_out_of_range() {
437 let gw_max = GnssWeek {
450 week: qtty::u32::Week::new(u32::MAX),
451 seconds_of_week: qtty::u32::Second::new(0),
452 subsecond_nanos: qtty::u32::Nanosecond::new(0),
453 };
454 let dur = gw_max.to_duration_since_epoch();
456 let (_s, _n) = dur
457 .as_seconds_i64_nanos_checked()
458 .expect("should fit in i64");
459 let epoch =
461 Time::<GPST>::from_raw_j2000_seconds(qtty::Second::new(GPST_EPOCH_J2000_SECONDS))
462 .unwrap();
463 let t = epoch.add_exact(dur);
464 let back = t.to_gnss_week().unwrap();
466 assert_eq!(back.week.value(), u32::MAX);
467
468 let overflow_secs = (u32::MAX as i128 + 1) * SECONDS_PER_WEEK.value();
472 let epoch_j2000 = GPST_EPOCH_J2000_SECONDS as i128;
473 let j2000_secs = epoch_j2000 + overflow_secs;
474 let t2 =
476 Time::<GPST>::from_raw_j2000_seconds(qtty::Second::new(j2000_secs as f64)).unwrap();
477 let result = t2.to_gnss_week();
478 assert!(
479 result.is_err(),
480 "expected OutOfRange for week > u32::MAX, got {result:?}"
481 );
482 }
483}