1#[derive(Debug, Clone, Copy, PartialEq)]
18#[repr(C)]
19pub struct LocationFix {
20 pub latitude_deg: f64,
22 pub longitude_deg: f64,
24 pub accuracy_m: f32,
26 pub altitude_m: f32,
29 pub altitude_accuracy_m: f32,
32 pub heading_deg: f32,
35 pub speed_mps: f32,
37 pub timestamp_ms: u64,
40}
41
42impl_option!(LocationFix, OptionLocationFix, [Debug, Clone, Copy, PartialEq]);
48
49impl LocationFix {
50 #[must_use] pub const fn altitude(&self) -> Option<f32> {
51 if self.altitude_m.is_nan() {
52 None
53 } else {
54 Some(self.altitude_m)
55 }
56 }
57
58 #[must_use] pub const fn altitude_accuracy(&self) -> Option<f32> {
59 if self.altitude_accuracy_m.is_nan() {
60 None
61 } else {
62 Some(self.altitude_accuracy_m)
63 }
64 }
65
66 #[must_use] pub const fn heading(&self) -> Option<f32> {
67 if self.heading_deg.is_nan() {
68 None
69 } else {
70 Some(self.heading_deg)
71 }
72 }
73
74 #[must_use] pub const fn speed(&self) -> Option<f32> {
75 if self.speed_mps.is_nan() {
76 None
77 } else {
78 Some(self.speed_mps)
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy)]
87#[repr(C)]
88pub struct GeolocationProbeConfig {
89 pub high_accuracy: bool,
94 pub background: bool,
99 pub max_accuracy_m: f32,
102 pub min_interval_ms: u32,
106}
107
108impl Default for GeolocationProbeConfig {
109 fn default() -> Self {
110 Self {
111 high_accuracy: false,
112 background: false,
113 max_accuracy_m: 0.0,
114 min_interval_ms: 0,
115 }
116 }
117}
118
119const fn canon_bits(f: f32) -> u32 {
124 let bits = f.to_bits();
125 if bits.trailing_zeros() >= 31 {
126 0 } else if f.is_nan() {
128 f32::NAN.to_bits() } else {
130 bits
131 }
132}
133
134impl PartialEq for GeolocationProbeConfig {
139 fn eq(&self, other: &Self) -> bool {
140 self.high_accuracy == other.high_accuracy
141 && self.background == other.background
142 && canon_bits(self.max_accuracy_m) == canon_bits(other.max_accuracy_m)
143 && self.min_interval_ms == other.min_interval_ms
144 }
145}
146
147impl Eq for GeolocationProbeConfig {}
148
149impl PartialOrd for GeolocationProbeConfig {
150 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
151 Some(self.cmp(other))
152 }
153}
154
155impl Ord for GeolocationProbeConfig {
156 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
157 (
160 self.high_accuracy,
161 self.background,
162 canon_bits(self.max_accuracy_m),
163 self.min_interval_ms,
164 )
165 .cmp(&(
166 other.high_accuracy,
167 other.background,
168 canon_bits(other.max_accuracy_m),
169 other.min_interval_ms,
170 ))
171 }
172}
173
174impl core::hash::Hash for GeolocationProbeConfig {
175 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
176 self.high_accuracy.hash(state);
177 self.background.hash(state);
178 canon_bits(self.max_accuracy_m).hash(state);
179 self.min_interval_ms.hash(state);
180 }
181}
182
183#[cfg(test)]
184#[allow(clippy::float_cmp, clippy::eq_op, clippy::unusual_byte_groupings)]
185mod autotest_generated {
186 use core::{
187 cmp::Ordering,
188 hash::{Hash, Hasher},
189 };
190
191 use super::*;
192
193 const fn fix_all_present() -> LocationFix {
200 LocationFix {
201 latitude_deg: 48.208_8,
202 longitude_deg: 16.372_1,
203 accuracy_m: 5.0,
204 altitude_m: 171.0,
205 altitude_accuracy_m: 3.0,
206 heading_deg: 90.0,
207 speed_mps: 1.5,
208 timestamp_ms: 1_234,
209 }
210 }
211
212 const fn fix_all_absent() -> LocationFix {
214 LocationFix {
215 latitude_deg: 0.0,
216 longitude_deg: 0.0,
217 accuracy_m: 0.0,
218 altitude_m: f32::NAN,
219 altitude_accuracy_m: f32::NAN,
220 heading_deg: f32::NAN,
221 speed_mps: f32::NAN,
222 timestamp_ms: 0,
223 }
224 }
225
226 const fn cfg(
227 high_accuracy: bool,
228 background: bool,
229 max_accuracy_m: f32,
230 min_interval_ms: u32,
231 ) -> GeolocationProbeConfig {
232 GeolocationProbeConfig {
233 high_accuracy,
234 background,
235 max_accuracy_m,
236 min_interval_ms,
237 }
238 }
239
240 fn same_f32(a: f32, b: f32) -> bool {
245 if a.is_nan() {
246 b.is_nan()
247 } else {
248 a.to_bits() == b.to_bits()
249 }
250 }
251
252 fn same_f64(a: f64, b: f64) -> bool {
253 if a.is_nan() {
254 b.is_nan()
255 } else {
256 a.to_bits() == b.to_bits()
257 }
258 }
259
260 fn same_fix(a: &LocationFix, b: &LocationFix) -> bool {
261 same_f64(a.latitude_deg, b.latitude_deg)
262 && same_f64(a.longitude_deg, b.longitude_deg)
263 && same_f32(a.accuracy_m, b.accuracy_m)
264 && same_f32(a.altitude_m, b.altitude_m)
265 && same_f32(a.altitude_accuracy_m, b.altitude_accuracy_m)
266 && same_f32(a.heading_deg, b.heading_deg)
267 && same_f32(a.speed_mps, b.speed_mps)
268 && a.timestamp_ms == b.timestamp_ms
269 }
270
271 struct Fnv1a(u64);
274
275 impl Fnv1a {
276 const fn new() -> Self {
277 Self(0xcbf2_9ce4_8422_2325)
278 }
279 }
280
281 impl Hasher for Fnv1a {
282 fn finish(&self) -> u64 {
283 self.0
284 }
285 fn write(&mut self, bytes: &[u8]) {
286 for b in bytes {
287 self.0 ^= u64::from(*b);
288 self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
289 }
290 }
291 }
292
293 fn hash_of(c: &GeolocationProbeConfig) -> u64 {
294 let mut h = Fnv1a::new();
295 c.hash(&mut h);
296 h.finish()
297 }
298
299 const F32_PATTERNS: [u32; 16] = [
302 0x0000_0000, 0x8000_0000, 0x0000_0001, 0x8000_0001, 0x007f_ffff, 0x0080_0000, 0x3f80_0000, 0xbf80_0000, 0x7f7f_ffff, 0xff7f_ffff, 0x7f80_0000, 0xff80_0000, 0x7fc0_0000, 0xffc0_0000, 0x7f80_0001, 0x7fff_ffff, ];
319
320 #[test]
330 fn getter_is_none_exactly_when_field_is_nan() {
331 for bits in F32_PATTERNS {
332 let v = f32::from_bits(bits);
333
334 let mut alt = fix_all_present();
337 alt.altitude_m = v;
338 let mut alt_acc = fix_all_present();
339 alt_acc.altitude_accuracy_m = v;
340 let mut head = fix_all_present();
341 head.heading_deg = v;
342 let mut spd = fix_all_present();
343 spd.speed_mps = v;
344
345 let got = [
346 ("altitude", alt.altitude()),
347 ("altitude_accuracy", alt_acc.altitude_accuracy()),
348 ("heading", head.heading()),
349 ("speed", spd.speed()),
350 ];
351
352 for (name, out) in got {
353 if v.is_nan() {
354 assert!(
355 out.is_none(),
356 "{name}() must be None for NaN bits {bits:#010x}, got {out:?}"
357 );
358 } else {
359 let inner = out.unwrap_or_else(|| {
360 panic!("{name}() must be Some for non-NaN bits {bits:#010x}")
361 });
362 assert_eq!(
363 inner.to_bits(),
364 bits,
365 "{name}() must return the field verbatim for {bits:#010x}"
366 );
367 }
368 }
369
370 assert_eq!(alt.heading(), Some(90.0));
372 assert_eq!(alt_acc.speed(), Some(1.5));
373 assert_eq!(head.altitude(), Some(171.0));
374 assert_eq!(spd.altitude_accuracy(), Some(3.0));
375 }
376 }
377
378 #[test]
382 fn getters_preserve_negative_zero() {
383 let mut fix = fix_all_present();
384 fix.altitude_m = -0.0;
385 fix.altitude_accuracy_m = -0.0;
386 fix.heading_deg = -0.0;
387 fix.speed_mps = -0.0;
388
389 for out in [
390 fix.altitude(),
391 fix.altitude_accuracy(),
392 fix.heading(),
393 fix.speed(),
394 ] {
395 let v = out.expect("-0.0 is not NaN, so the field is 'reported'");
396 assert_eq!(v.to_bits(), 0x8000_0000, "sign bit of -0.0 was dropped");
397 assert!(v == 0.0, "-0.0 must still compare equal to 0.0");
398 }
399 }
400
401 #[test]
404 fn getters_pass_infinities_through() {
405 let mut fix = fix_all_absent();
406 fix.altitude_m = f32::INFINITY;
407 fix.altitude_accuracy_m = f32::NEG_INFINITY;
408 fix.heading_deg = f32::INFINITY;
409 fix.speed_mps = f32::NEG_INFINITY;
410
411 assert_eq!(fix.altitude(), Some(f32::INFINITY));
412 assert_eq!(fix.altitude_accuracy(), Some(f32::NEG_INFINITY));
413 assert_eq!(fix.heading(), Some(f32::INFINITY));
414 assert_eq!(fix.speed(), Some(f32::NEG_INFINITY));
415 }
416
417 #[test]
421 fn getters_on_extreme_and_all_absent_fix() {
422 let mut fix = fix_all_absent();
423 fix.latitude_deg = f64::MAX;
424 fix.longitude_deg = f64::MIN;
425 fix.accuracy_m = f32::MAX;
426 fix.timestamp_ms = u64::MAX;
427
428 assert_eq!(fix.altitude(), None);
429 assert_eq!(fix.altitude_accuracy(), None);
430 assert_eq!(fix.heading(), None);
431 assert_eq!(fix.speed(), None);
432
433 assert_eq!(fix.timestamp_ms, u64::MAX);
435 assert!(fix.latitude_deg == f64::MAX);
436
437 let zeroed = LocationFix {
439 latitude_deg: 0.0,
440 longitude_deg: 0.0,
441 accuracy_m: 0.0,
442 altitude_m: 0.0,
443 altitude_accuracy_m: 0.0,
444 heading_deg: 0.0,
445 speed_mps: 0.0,
446 timestamp_ms: 0,
447 };
448 assert_eq!(zeroed.altitude(), Some(0.0));
449 assert_eq!(zeroed.altitude_accuracy(), Some(0.0));
450 assert_eq!(zeroed.heading(), Some(0.0));
451 assert_eq!(zeroed.speed(), Some(0.0));
452 }
453
454 #[test]
457 fn getters_are_const_evaluable() {
458 const PRESENT: LocationFix = fix_all_present();
459 const ABSENT: LocationFix = fix_all_absent();
460
461 const ALT: Option<f32> = PRESENT.altitude();
462 const ALT_ACC: Option<f32> = PRESENT.altitude_accuracy();
463 const HEADING: Option<f32> = PRESENT.heading();
464 const SPEED: Option<f32> = PRESENT.speed();
465 const NO_ALT: Option<f32> = ABSENT.altitude();
466 const NO_HEADING: Option<f32> = ABSENT.heading();
467
468 assert_eq!(ALT, Some(171.0));
469 assert_eq!(ALT_ACC, Some(3.0));
470 assert_eq!(HEADING, Some(90.0));
471 assert_eq!(SPEED, Some(1.5));
472 assert_eq!(NO_ALT, None);
473 assert_eq!(NO_HEADING, None);
474 }
475
476 #[test]
482 fn location_fix_partial_eq_is_not_reflexive_when_fields_are_absent() {
483 let absent = fix_all_absent();
484 assert!(absent != absent, "IEEE semantics: NaN != NaN");
485 assert!(same_fix(&absent, &absent), "but it is the same fix");
486
487 let present = fix_all_present();
488 assert!(present == present, "a fully-reported fix is self-equal");
489 assert!(same_fix(&present, &present));
490
491 let mut neg_zero = fix_all_present();
493 neg_zero.altitude_m = -0.0;
494 let mut pos_zero = fix_all_present();
495 pos_zero.altitude_m = 0.0;
496 assert!(neg_zero == pos_zero);
497 assert!(!same_fix(&neg_zero, &pos_zero));
498 }
499
500 #[test]
508 fn option_location_fix_roundtrips() {
509 assert!(OptionLocationFix::default().is_none());
510 assert!(!OptionLocationFix::default().is_some());
511 assert_eq!(
512 Option::<LocationFix>::from(OptionLocationFix::default()),
513 None
514 );
515
516 let fix = fix_all_present();
517 let wrapped: OptionLocationFix = Some(fix).into();
518 assert!(wrapped.is_some());
519 assert!(!wrapped.is_none());
520 assert_eq!(wrapped.as_ref(), Some(&fix));
521 assert_eq!(wrapped.as_option(), Some(&fix));
522 assert_eq!(wrapped.into_option(), Some(fix));
523
524 let decoded = Option::<LocationFix>::from(wrapped).expect("Some in, Some out");
525 assert!(same_fix(&decoded, &fix), "encode == decode");
526
527 let none: OptionLocationFix = Option::<LocationFix>::None.into();
528 assert!(none.is_none());
529 assert_eq!(none.as_ref(), None);
530 assert_eq!(none.map(|f| f.timestamp_ms), None);
531 assert_eq!(none.and_then(|f| f.altitude()), None);
532 assert_eq!(wrapped.map(|f| f.timestamp_ms), Some(1_234));
533 assert_eq!(wrapped.and_then(|f| f.altitude()), Some(171.0));
534
535 let mut slot = OptionLocationFix::None;
537 assert!(slot.replace(fix).is_none());
538 assert_eq!(slot.as_option(), Some(&fix));
539
540 let other = fix_all_absent();
541 let prev = slot.replace(other);
542 assert!(prev.is_some());
543 assert!(same_fix(
544 &Option::<LocationFix>::from(prev).expect("previous fix"),
545 &fix
546 ));
547 assert!(same_fix(
548 slot.as_option().expect("current fix"),
549 &fix_all_absent()
550 ));
551
552 if let Some(inner) = slot.as_mut() {
554 inner.timestamp_ms = u64::MAX;
555 }
556 assert_eq!(slot.as_option().expect("still Some").timestamp_ms, u64::MAX);
557 }
558
559 #[test]
564 fn option_location_fix_roundtrip_survives_absent_fields() {
565 let mut fix = fix_all_absent();
566 fix.latitude_deg = -89.999_999_9;
567 fix.longitude_deg = 179.999_999_9;
568 fix.accuracy_m = f32::MIN_POSITIVE;
569 fix.timestamp_ms = u64::MAX;
570
571 let decoded = Option::<LocationFix>::from(OptionLocationFix::Some(fix))
572 .expect("Some survives the round-trip");
573
574 assert!(same_fix(&decoded, &fix), "encode == decode for a NaN-y fix");
575 assert_eq!(decoded.altitude(), None);
576 assert_eq!(decoded.altitude_accuracy(), None);
577 assert_eq!(decoded.heading(), None);
578 assert_eq!(decoded.speed(), None);
579 assert_eq!(decoded.timestamp_ms, u64::MAX);
580
581 assert!(OptionLocationFix::Some(fix) != OptionLocationFix::Some(fix));
584 assert!(OptionLocationFix::None == OptionLocationFix::None);
585 }
586
587 #[test]
591 fn option_location_fix_is_tagged_not_niche_packed() {
592 use core::mem::{align_of, size_of};
593 assert!(
594 size_of::<OptionLocationFix>() > size_of::<LocationFix>(),
595 "repr(C, u8) must carry an explicit discriminant"
596 );
597 assert!(align_of::<OptionLocationFix>() >= align_of::<LocationFix>());
598 assert!(size_of::<LocationFix>() >= 2 * size_of::<f64>() + 5 * size_of::<f32>());
599 }
600
601 #[test]
608 fn probe_config_default_is_permissive_zero() {
609 let d = GeolocationProbeConfig::default();
610 assert!(!d.high_accuracy);
611 assert!(!d.background);
612 assert!(d.max_accuracy_m == 0.0);
613 assert_eq!(d.max_accuracy_m.to_bits(), 0, "default must be +0.0, not -0.0");
614 assert_eq!(d.min_interval_ms, 0);
615 assert_eq!(d, cfg(false, false, 0.0, 0));
616 assert_eq!(d.cmp(&GeolocationProbeConfig::default()), Ordering::Equal);
617 }
618
619 #[test]
623 fn probe_config_ord_field_precedence() {
624 let low_but_maxed = cfg(false, true, f32::from_bits(u32::MAX), u32::MAX);
625 let high_but_minimal = cfg(true, false, 0.0, 0);
626 assert!(low_but_maxed < high_but_minimal, "high_accuracy dominates");
627
628 let fg_maxed = cfg(true, false, f32::from_bits(u32::MAX), u32::MAX);
629 let bg_minimal = cfg(true, true, 0.0, 0);
630 assert!(fg_maxed < bg_minimal, "background dominates the numeric fields");
631
632 let small_acc = cfg(true, true, 1.0, u32::MAX);
633 let big_acc = cfg(true, true, 2.0, 0);
634 assert!(small_acc < big_acc, "accuracy bits dominate the interval");
635
636 assert!(cfg(true, true, 1.0, 0) < cfg(true, true, 1.0, 1));
637 assert!(cfg(true, true, 1.0, u32::MAX - 1) < cfg(true, true, 1.0, u32::MAX));
638 assert_eq!(
639 cfg(true, true, 1.0, u32::MAX).cmp(&cfg(true, true, 1.0, u32::MAX)),
640 Ordering::Equal
641 );
642 }
643
644 #[test]
649 fn probe_config_ord_is_bitwise_not_numeric() {
650 let neg = cfg(false, false, -1.0, 0);
651 let pos = cfg(false, false, 1.0, 0);
652 assert!(neg.max_accuracy_m < pos.max_accuracy_m, "numerically: -1 < 1");
653 assert_eq!(
654 neg.cmp(&pos),
655 Ordering::Greater,
656 "bitwise: 0xbf80_0000 > 0x3f80_0000"
657 );
658
659 let inf = cfg(false, false, f32::INFINITY, 0);
660 let nan = cfg(false, false, f32::NAN, 0);
661 assert_eq!(nan.cmp(&inf), Ordering::Greater, "NaN bits sort above +inf");
662 assert_eq!(inf.cmp(&pos), Ordering::Greater);
663 }
664
665 #[test]
670 fn probe_config_ord_is_a_total_order_with_nan() {
671 let nan = cfg(false, false, f32::NAN, 7);
672 assert_eq!(nan.cmp(&nan), Ordering::Equal, "cmp must be reflexive");
673 assert_eq!(nan.partial_cmp(&nan), Some(Ordering::Equal));
674
675 let mut list = [
676 cfg(true, true, f32::NAN, u32::MAX),
677 cfg(false, false, -0.0, 0),
678 cfg(false, false, f32::INFINITY, 1),
679 nan,
680 cfg(true, false, f32::NEG_INFINITY, 3),
681 cfg(false, true, 0.0, u32::MAX),
682 GeolocationProbeConfig::default(),
683 ];
684 list.sort_unstable();
685 for w in list.windows(2) {
686 assert_ne!(
687 w[0].cmp(&w[1]),
688 Ordering::Greater,
689 "sort_unstable must leave the slice ordered under cmp"
690 );
691 }
692
693 for a in list {
695 for b in list {
696 assert_eq!(
697 a.cmp(&b),
698 b.cmp(&a).reverse(),
699 "cmp must be antisymmetric"
700 );
701 for c in list {
702 if a.cmp(&b) != Ordering::Greater && b.cmp(&c) != Ordering::Greater {
703 assert_ne!(a.cmp(&c), Ordering::Greater, "cmp must be transitive");
704 }
705 }
706 }
707 }
708 }
709
710 #[test]
716 fn probe_config_eq_ord_hash_agree_on_ordinary_configs() {
717 let configs = [
718 GeolocationProbeConfig::default(),
719 cfg(false, false, 25.0, 1_000),
720 cfg(true, false, 5.0, 0),
721 cfg(true, true, 0.5, u32::MAX),
722 cfg(false, true, f32::MAX, 1),
723 cfg(true, true, f32::MIN_POSITIVE, 16),
724 ];
725
726 for a in configs {
727 for b in configs {
728 assert_eq!(a.partial_cmp(&b), Some(a.cmp(&b)), "PartialOrd must mirror Ord");
729 assert_eq!(
730 a == b,
731 a.cmp(&b) == Ordering::Equal,
732 "`==` must agree with `cmp == Equal`"
733 );
734 if a == b {
735 assert_eq!(hash_of(&a), hash_of(&b), "Eq implies equal hashes");
736 }
737 }
738 assert_eq!(a, a);
740 assert_eq!(a.cmp(&a), Ordering::Equal);
741 assert_eq!(hash_of(&a), hash_of(&a));
742 }
743 }
744
745 #[test]
750 fn probe_config_hash_is_stable_and_discriminating() {
751 let a = cfg(true, false, 12.5, 250);
752 assert_eq!(hash_of(&a), hash_of(&cfg(true, false, 12.5, 250)));
753
754 let nan = cfg(false, false, f32::NAN, 0);
755 assert_eq!(hash_of(&nan), hash_of(&nan), "hashing NaN must be stable");
756
757 for other in [
758 cfg(false, false, 12.5, 250),
759 cfg(true, true, 12.5, 250),
760 cfg(true, false, 12.75, 250),
761 cfg(true, false, 12.5, 251),
762 ] {
763 assert_ne!(a, other);
764 assert_ne!(
765 hash_of(&a),
766 hash_of(&other),
767 "configs differing in one field must not collide: {other:?}"
768 );
769 }
770 }
771
772 #[test]
786 fn probe_config_eq_implies_equal_hash_and_ordering() {
787 let pos = cfg(false, false, 0.0, 0);
788 let neg = cfg(false, false, -0.0, 0);
789
790 assert_eq!(pos, neg, "IEEE: -0.0 == 0.0");
791 assert_eq!(hash_of(&pos), hash_of(&neg), "Eq/Hash contract");
792 assert_eq!(pos.cmp(&neg), Ordering::Equal, "Eq/Ord contract");
793 }
794
795 #[test]
801 #[allow(clippy::eq_op)]
802 fn probe_config_eq_is_reflexive_with_nan() {
803 let c = cfg(false, false, f32::NAN, 0);
804
805 assert_eq!(c.cmp(&c), Ordering::Equal, "Ord says equal");
806 assert_eq!(c, c, "so Eq must too");
807 }
808}