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