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, PartialEq)]
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
119impl Eq for GeolocationProbeConfig {}
120
121impl PartialOrd for GeolocationProbeConfig {
122 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
123 Some(self.cmp(other))
124 }
125}
126
127impl Ord for GeolocationProbeConfig {
128 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
129 (
132 self.high_accuracy,
133 self.background,
134 self.max_accuracy_m.to_bits(),
135 self.min_interval_ms,
136 )
137 .cmp(&(
138 other.high_accuracy,
139 other.background,
140 other.max_accuracy_m.to_bits(),
141 other.min_interval_ms,
142 ))
143 }
144}
145
146impl core::hash::Hash for GeolocationProbeConfig {
147 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
148 self.high_accuracy.hash(state);
149 self.background.hash(state);
150 self.max_accuracy_m.to_bits().hash(state);
151 self.min_interval_ms.hash(state);
152 }
153}
154
155#[cfg(test)]
156#[allow(clippy::float_cmp, clippy::eq_op, clippy::unusual_byte_groupings)]
157mod autotest_generated {
158 use core::{
159 cmp::Ordering,
160 hash::{Hash, Hasher},
161 };
162
163 use super::*;
164
165 const fn fix_all_present() -> LocationFix {
172 LocationFix {
173 latitude_deg: 48.208_8,
174 longitude_deg: 16.372_1,
175 accuracy_m: 5.0,
176 altitude_m: 171.0,
177 altitude_accuracy_m: 3.0,
178 heading_deg: 90.0,
179 speed_mps: 1.5,
180 timestamp_ms: 1_234,
181 }
182 }
183
184 const fn fix_all_absent() -> LocationFix {
186 LocationFix {
187 latitude_deg: 0.0,
188 longitude_deg: 0.0,
189 accuracy_m: 0.0,
190 altitude_m: f32::NAN,
191 altitude_accuracy_m: f32::NAN,
192 heading_deg: f32::NAN,
193 speed_mps: f32::NAN,
194 timestamp_ms: 0,
195 }
196 }
197
198 const fn cfg(
199 high_accuracy: bool,
200 background: bool,
201 max_accuracy_m: f32,
202 min_interval_ms: u32,
203 ) -> GeolocationProbeConfig {
204 GeolocationProbeConfig {
205 high_accuracy,
206 background,
207 max_accuracy_m,
208 min_interval_ms,
209 }
210 }
211
212 fn same_f32(a: f32, b: f32) -> bool {
217 if a.is_nan() {
218 b.is_nan()
219 } else {
220 a.to_bits() == b.to_bits()
221 }
222 }
223
224 fn same_f64(a: f64, b: f64) -> bool {
225 if a.is_nan() {
226 b.is_nan()
227 } else {
228 a.to_bits() == b.to_bits()
229 }
230 }
231
232 fn same_fix(a: &LocationFix, b: &LocationFix) -> bool {
233 same_f64(a.latitude_deg, b.latitude_deg)
234 && same_f64(a.longitude_deg, b.longitude_deg)
235 && same_f32(a.accuracy_m, b.accuracy_m)
236 && same_f32(a.altitude_m, b.altitude_m)
237 && same_f32(a.altitude_accuracy_m, b.altitude_accuracy_m)
238 && same_f32(a.heading_deg, b.heading_deg)
239 && same_f32(a.speed_mps, b.speed_mps)
240 && a.timestamp_ms == b.timestamp_ms
241 }
242
243 struct Fnv1a(u64);
246
247 impl Fnv1a {
248 const fn new() -> Self {
249 Self(0xcbf2_9ce4_8422_2325)
250 }
251 }
252
253 impl Hasher for Fnv1a {
254 fn finish(&self) -> u64 {
255 self.0
256 }
257 fn write(&mut self, bytes: &[u8]) {
258 for b in bytes {
259 self.0 ^= u64::from(*b);
260 self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
261 }
262 }
263 }
264
265 fn hash_of(c: &GeolocationProbeConfig) -> u64 {
266 let mut h = Fnv1a::new();
267 c.hash(&mut h);
268 h.finish()
269 }
270
271 const F32_PATTERNS: [u32; 16] = [
274 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, ];
291
292 #[test]
302 fn getter_is_none_exactly_when_field_is_nan() {
303 for bits in F32_PATTERNS {
304 let v = f32::from_bits(bits);
305
306 let mut alt = fix_all_present();
309 alt.altitude_m = v;
310 let mut alt_acc = fix_all_present();
311 alt_acc.altitude_accuracy_m = v;
312 let mut head = fix_all_present();
313 head.heading_deg = v;
314 let mut spd = fix_all_present();
315 spd.speed_mps = v;
316
317 let got = [
318 ("altitude", alt.altitude()),
319 ("altitude_accuracy", alt_acc.altitude_accuracy()),
320 ("heading", head.heading()),
321 ("speed", spd.speed()),
322 ];
323
324 for (name, out) in got {
325 if v.is_nan() {
326 assert!(
327 out.is_none(),
328 "{name}() must be None for NaN bits {bits:#010x}, got {out:?}"
329 );
330 } else {
331 let inner = out.unwrap_or_else(|| {
332 panic!("{name}() must be Some for non-NaN bits {bits:#010x}")
333 });
334 assert_eq!(
335 inner.to_bits(),
336 bits,
337 "{name}() must return the field verbatim for {bits:#010x}"
338 );
339 }
340 }
341
342 assert_eq!(alt.heading(), Some(90.0));
344 assert_eq!(alt_acc.speed(), Some(1.5));
345 assert_eq!(head.altitude(), Some(171.0));
346 assert_eq!(spd.altitude_accuracy(), Some(3.0));
347 }
348 }
349
350 #[test]
354 fn getters_preserve_negative_zero() {
355 let mut fix = fix_all_present();
356 fix.altitude_m = -0.0;
357 fix.altitude_accuracy_m = -0.0;
358 fix.heading_deg = -0.0;
359 fix.speed_mps = -0.0;
360
361 for out in [
362 fix.altitude(),
363 fix.altitude_accuracy(),
364 fix.heading(),
365 fix.speed(),
366 ] {
367 let v = out.expect("-0.0 is not NaN, so the field is 'reported'");
368 assert_eq!(v.to_bits(), 0x8000_0000, "sign bit of -0.0 was dropped");
369 assert!(v == 0.0, "-0.0 must still compare equal to 0.0");
370 }
371 }
372
373 #[test]
376 fn getters_pass_infinities_through() {
377 let mut fix = fix_all_absent();
378 fix.altitude_m = f32::INFINITY;
379 fix.altitude_accuracy_m = f32::NEG_INFINITY;
380 fix.heading_deg = f32::INFINITY;
381 fix.speed_mps = f32::NEG_INFINITY;
382
383 assert_eq!(fix.altitude(), Some(f32::INFINITY));
384 assert_eq!(fix.altitude_accuracy(), Some(f32::NEG_INFINITY));
385 assert_eq!(fix.heading(), Some(f32::INFINITY));
386 assert_eq!(fix.speed(), Some(f32::NEG_INFINITY));
387 }
388
389 #[test]
393 fn getters_on_extreme_and_all_absent_fix() {
394 let mut fix = fix_all_absent();
395 fix.latitude_deg = f64::MAX;
396 fix.longitude_deg = f64::MIN;
397 fix.accuracy_m = f32::MAX;
398 fix.timestamp_ms = u64::MAX;
399
400 assert_eq!(fix.altitude(), None);
401 assert_eq!(fix.altitude_accuracy(), None);
402 assert_eq!(fix.heading(), None);
403 assert_eq!(fix.speed(), None);
404
405 assert_eq!(fix.timestamp_ms, u64::MAX);
407 assert!(fix.latitude_deg == f64::MAX);
408
409 let zeroed = LocationFix {
411 latitude_deg: 0.0,
412 longitude_deg: 0.0,
413 accuracy_m: 0.0,
414 altitude_m: 0.0,
415 altitude_accuracy_m: 0.0,
416 heading_deg: 0.0,
417 speed_mps: 0.0,
418 timestamp_ms: 0,
419 };
420 assert_eq!(zeroed.altitude(), Some(0.0));
421 assert_eq!(zeroed.altitude_accuracy(), Some(0.0));
422 assert_eq!(zeroed.heading(), Some(0.0));
423 assert_eq!(zeroed.speed(), Some(0.0));
424 }
425
426 #[test]
429 fn getters_are_const_evaluable() {
430 const PRESENT: LocationFix = fix_all_present();
431 const ABSENT: LocationFix = fix_all_absent();
432
433 const ALT: Option<f32> = PRESENT.altitude();
434 const ALT_ACC: Option<f32> = PRESENT.altitude_accuracy();
435 const HEADING: Option<f32> = PRESENT.heading();
436 const SPEED: Option<f32> = PRESENT.speed();
437 const NO_ALT: Option<f32> = ABSENT.altitude();
438 const NO_HEADING: Option<f32> = ABSENT.heading();
439
440 assert_eq!(ALT, Some(171.0));
441 assert_eq!(ALT_ACC, Some(3.0));
442 assert_eq!(HEADING, Some(90.0));
443 assert_eq!(SPEED, Some(1.5));
444 assert_eq!(NO_ALT, None);
445 assert_eq!(NO_HEADING, None);
446 }
447
448 #[test]
454 fn location_fix_partial_eq_is_not_reflexive_when_fields_are_absent() {
455 let absent = fix_all_absent();
456 assert!(absent != absent, "IEEE semantics: NaN != NaN");
457 assert!(same_fix(&absent, &absent), "but it is the same fix");
458
459 let present = fix_all_present();
460 assert!(present == present, "a fully-reported fix is self-equal");
461 assert!(same_fix(&present, &present));
462
463 let mut neg_zero = fix_all_present();
465 neg_zero.altitude_m = -0.0;
466 let mut pos_zero = fix_all_present();
467 pos_zero.altitude_m = 0.0;
468 assert!(neg_zero == pos_zero);
469 assert!(!same_fix(&neg_zero, &pos_zero));
470 }
471
472 #[test]
480 fn option_location_fix_roundtrips() {
481 assert!(OptionLocationFix::default().is_none());
482 assert!(!OptionLocationFix::default().is_some());
483 assert_eq!(
484 Option::<LocationFix>::from(OptionLocationFix::default()),
485 None
486 );
487
488 let fix = fix_all_present();
489 let wrapped: OptionLocationFix = Some(fix).into();
490 assert!(wrapped.is_some());
491 assert!(!wrapped.is_none());
492 assert_eq!(wrapped.as_ref(), Some(&fix));
493 assert_eq!(wrapped.as_option(), Some(&fix));
494 assert_eq!(wrapped.into_option(), Some(fix));
495
496 let decoded = Option::<LocationFix>::from(wrapped).expect("Some in, Some out");
497 assert!(same_fix(&decoded, &fix), "encode == decode");
498
499 let none: OptionLocationFix = Option::<LocationFix>::None.into();
500 assert!(none.is_none());
501 assert_eq!(none.as_ref(), None);
502 assert_eq!(none.map(|f| f.timestamp_ms), None);
503 assert_eq!(none.and_then(|f| f.altitude()), None);
504 assert_eq!(wrapped.map(|f| f.timestamp_ms), Some(1_234));
505 assert_eq!(wrapped.and_then(|f| f.altitude()), Some(171.0));
506
507 let mut slot = OptionLocationFix::None;
509 assert!(slot.replace(fix).is_none());
510 assert_eq!(slot.as_option(), Some(&fix));
511
512 let other = fix_all_absent();
513 let prev = slot.replace(other);
514 assert!(prev.is_some());
515 assert!(same_fix(
516 &Option::<LocationFix>::from(prev).expect("previous fix"),
517 &fix
518 ));
519 assert!(same_fix(
520 slot.as_option().expect("current fix"),
521 &fix_all_absent()
522 ));
523
524 if let Some(inner) = slot.as_mut() {
526 inner.timestamp_ms = u64::MAX;
527 }
528 assert_eq!(slot.as_option().expect("still Some").timestamp_ms, u64::MAX);
529 }
530
531 #[test]
536 fn option_location_fix_roundtrip_survives_absent_fields() {
537 let mut fix = fix_all_absent();
538 fix.latitude_deg = -89.999_999_9;
539 fix.longitude_deg = 179.999_999_9;
540 fix.accuracy_m = f32::MIN_POSITIVE;
541 fix.timestamp_ms = u64::MAX;
542
543 let decoded = Option::<LocationFix>::from(OptionLocationFix::Some(fix))
544 .expect("Some survives the round-trip");
545
546 assert!(same_fix(&decoded, &fix), "encode == decode for a NaN-y fix");
547 assert_eq!(decoded.altitude(), None);
548 assert_eq!(decoded.altitude_accuracy(), None);
549 assert_eq!(decoded.heading(), None);
550 assert_eq!(decoded.speed(), None);
551 assert_eq!(decoded.timestamp_ms, u64::MAX);
552
553 assert!(OptionLocationFix::Some(fix) != OptionLocationFix::Some(fix));
556 assert!(OptionLocationFix::None == OptionLocationFix::None);
557 }
558
559 #[test]
563 fn option_location_fix_is_tagged_not_niche_packed() {
564 use core::mem::{align_of, size_of};
565 assert!(
566 size_of::<OptionLocationFix>() > size_of::<LocationFix>(),
567 "repr(C, u8) must carry an explicit discriminant"
568 );
569 assert!(align_of::<OptionLocationFix>() >= align_of::<LocationFix>());
570 assert!(size_of::<LocationFix>() >= 2 * size_of::<f64>() + 5 * size_of::<f32>());
571 }
572
573 #[test]
580 fn probe_config_default_is_permissive_zero() {
581 let d = GeolocationProbeConfig::default();
582 assert!(!d.high_accuracy);
583 assert!(!d.background);
584 assert!(d.max_accuracy_m == 0.0);
585 assert_eq!(d.max_accuracy_m.to_bits(), 0, "default must be +0.0, not -0.0");
586 assert_eq!(d.min_interval_ms, 0);
587 assert_eq!(d, cfg(false, false, 0.0, 0));
588 assert_eq!(d.cmp(&GeolocationProbeConfig::default()), Ordering::Equal);
589 }
590
591 #[test]
595 fn probe_config_ord_field_precedence() {
596 let low_but_maxed = cfg(false, true, f32::from_bits(u32::MAX), u32::MAX);
597 let high_but_minimal = cfg(true, false, 0.0, 0);
598 assert!(low_but_maxed < high_but_minimal, "high_accuracy dominates");
599
600 let fg_maxed = cfg(true, false, f32::from_bits(u32::MAX), u32::MAX);
601 let bg_minimal = cfg(true, true, 0.0, 0);
602 assert!(fg_maxed < bg_minimal, "background dominates the numeric fields");
603
604 let small_acc = cfg(true, true, 1.0, u32::MAX);
605 let big_acc = cfg(true, true, 2.0, 0);
606 assert!(small_acc < big_acc, "accuracy bits dominate the interval");
607
608 assert!(cfg(true, true, 1.0, 0) < cfg(true, true, 1.0, 1));
609 assert!(cfg(true, true, 1.0, u32::MAX - 1) < cfg(true, true, 1.0, u32::MAX));
610 assert_eq!(
611 cfg(true, true, 1.0, u32::MAX).cmp(&cfg(true, true, 1.0, u32::MAX)),
612 Ordering::Equal
613 );
614 }
615
616 #[test]
621 fn probe_config_ord_is_bitwise_not_numeric() {
622 let neg = cfg(false, false, -1.0, 0);
623 let pos = cfg(false, false, 1.0, 0);
624 assert!(neg.max_accuracy_m < pos.max_accuracy_m, "numerically: -1 < 1");
625 assert_eq!(
626 neg.cmp(&pos),
627 Ordering::Greater,
628 "bitwise: 0xbf80_0000 > 0x3f80_0000"
629 );
630
631 let inf = cfg(false, false, f32::INFINITY, 0);
632 let nan = cfg(false, false, f32::NAN, 0);
633 assert_eq!(nan.cmp(&inf), Ordering::Greater, "NaN bits sort above +inf");
634 assert_eq!(inf.cmp(&pos), Ordering::Greater);
635 }
636
637 #[test]
642 fn probe_config_ord_is_a_total_order_with_nan() {
643 let nan = cfg(false, false, f32::NAN, 7);
644 assert_eq!(nan.cmp(&nan), Ordering::Equal, "cmp must be reflexive");
645 assert_eq!(nan.partial_cmp(&nan), Some(Ordering::Equal));
646
647 let mut list = [
648 cfg(true, true, f32::NAN, u32::MAX),
649 cfg(false, false, -0.0, 0),
650 cfg(false, false, f32::INFINITY, 1),
651 nan,
652 cfg(true, false, f32::NEG_INFINITY, 3),
653 cfg(false, true, 0.0, u32::MAX),
654 GeolocationProbeConfig::default(),
655 ];
656 list.sort_unstable();
657 for w in list.windows(2) {
658 assert_ne!(
659 w[0].cmp(&w[1]),
660 Ordering::Greater,
661 "sort_unstable must leave the slice ordered under cmp"
662 );
663 }
664
665 for a in list {
667 for b in list {
668 assert_eq!(
669 a.cmp(&b),
670 b.cmp(&a).reverse(),
671 "cmp must be antisymmetric"
672 );
673 for c in list {
674 if a.cmp(&b) != Ordering::Greater && b.cmp(&c) != Ordering::Greater {
675 assert_ne!(a.cmp(&c), Ordering::Greater, "cmp must be transitive");
676 }
677 }
678 }
679 }
680 }
681
682 #[test]
688 fn probe_config_eq_ord_hash_agree_on_ordinary_configs() {
689 let configs = [
690 GeolocationProbeConfig::default(),
691 cfg(false, false, 25.0, 1_000),
692 cfg(true, false, 5.0, 0),
693 cfg(true, true, 0.5, u32::MAX),
694 cfg(false, true, f32::MAX, 1),
695 cfg(true, true, f32::MIN_POSITIVE, 16),
696 ];
697
698 for a in configs {
699 for b in configs {
700 assert_eq!(a.partial_cmp(&b), Some(a.cmp(&b)), "PartialOrd must mirror Ord");
701 assert_eq!(
702 a == b,
703 a.cmp(&b) == Ordering::Equal,
704 "`==` must agree with `cmp == Equal`"
705 );
706 if a == b {
707 assert_eq!(hash_of(&a), hash_of(&b), "Eq implies equal hashes");
708 }
709 }
710 assert_eq!(a, a);
712 assert_eq!(a.cmp(&a), Ordering::Equal);
713 assert_eq!(hash_of(&a), hash_of(&a));
714 }
715 }
716
717 #[test]
722 fn probe_config_hash_is_stable_and_discriminating() {
723 let a = cfg(true, false, 12.5, 250);
724 assert_eq!(hash_of(&a), hash_of(&cfg(true, false, 12.5, 250)));
725
726 let nan = cfg(false, false, f32::NAN, 0);
727 assert_eq!(hash_of(&nan), hash_of(&nan), "hashing NaN must be stable");
728
729 for other in [
730 cfg(false, false, 12.5, 250),
731 cfg(true, true, 12.5, 250),
732 cfg(true, false, 12.75, 250),
733 cfg(true, false, 12.5, 251),
734 ] {
735 assert_ne!(a, other);
736 assert_ne!(
737 hash_of(&a),
738 hash_of(&other),
739 "configs differing in one field must not collide: {other:?}"
740 );
741 }
742 }
743
744 #[test]
756 #[ignore = "KNOWN BUG: -0.0 == 0.0 under derived PartialEq, but Hash/Ord use to_bits"]
757 fn probe_config_eq_implies_equal_hash_and_ordering() {
758 let pos = cfg(false, false, 0.0, 0);
759 let neg = cfg(false, false, -0.0, 0);
760
761 assert_eq!(pos, neg, "IEEE: -0.0 == 0.0");
762 assert_eq!(hash_of(&pos), hash_of(&neg), "Eq/Hash contract");
763 assert_eq!(pos.cmp(&neg), Ordering::Equal, "Eq/Ord contract");
764 }
765
766 #[test]
771 #[ignore = "KNOWN BUG: `impl Eq` claims reflexivity, but NaN max_accuracy_m breaks it"]
772 #[allow(clippy::eq_op)]
773 fn probe_config_eq_is_reflexive_with_nan() {
774 let c = cfg(false, false, f32::NAN, 0);
775
776 assert_eq!(c.cmp(&c), Ordering::Equal, "Ord says equal");
777 assert_eq!(c, c, "so Eq must too");
778 }
779}