mod util;
use mgrs_convert::{Error, LatLon, Mgrs, Precision, UtmUps};
use proptest::prelude::*;
fn ulps(x: f64, steps: i64) -> f64 {
let mut v = x;
for _ in 0..steps.unsigned_abs() {
v = if steps >= 0 {
f64::from_bits(if v >= 0.0 {
v.to_bits() + 1
} else {
v.to_bits() - 1
})
} else {
f64::from_bits(if v > 0.0 {
v.to_bits() - 1
} else {
v.to_bits() + 1
})
};
if v == 0.0 && steps < 0 {
v = -f64::from_bits(1);
}
}
v
}
fn lat_strategy() -> impl Strategy<Value = f64> {
prop_oneof![
4 => -90.0..90.0f64,
1 => prop_oneof![
Just(0.0), Just(-0.0), Just(90.0), Just(-90.0),
Just(84.0), Just(-80.0), Just(8.0), Just(-8.0), Just(56.0),
Just(64.0), Just(72.0),
],
1 => (-11i64..=11).prop_map(|s| ulps(84.0, s)),
1 => (-11i64..=11).prop_map(|s| ulps(-80.0, s)),
1 => (-11i64..=11).prop_map(|s| ulps(0.0, s)),
]
}
fn lon_strategy() -> impl Strategy<Value = f64> {
prop_oneof![
4 => -180.0..180.0f64,
1 => prop_oneof![
Just(-180.0), Just(180.0), Just(0.0), Just(3.0), Just(9.0),
Just(21.0), Just(33.0), Just(42.0), Just(6.0), Just(-6.0),
],
1 => (-11i64..=11).prop_map(|s| ulps(6.0, s)),
1 => (-11i64..=11).prop_map(|s| ulps(180.0, s)),
]
}
proptest! {
#[test]
fn no_panic_on_any_input(lat in proptest::num::f64::ANY, lon in proptest::num::f64::ANY, p in 0u8..=11) {
if let Ok(point) = LatLon::new(lat, lon) {
let u = point.to_utmups();
let _ = u.to_latlon();
let m = point.to_mgrs(Precision::new(p).unwrap());
let s = m.to_string();
let back: Mgrs = s.parse().unwrap();
prop_assert_eq!(back.to_string(), s);
}
}
#[test]
fn parser_is_total(s in "\\PC{0,40}") {
let _ = s.parse::<Mgrs>();
}
#[test]
fn parser_is_total_on_ascii_noise(s in "[0-9A-Za-z \t]{0,30}") {
let _ = s.parse::<Mgrs>();
}
#[test]
fn parse_format_identity(lat in lat_strategy(), lon in lon_strategy(), p in 0u8..=11) {
prop_assume!(lat.is_finite() && (-90.0..=90.0).contains(&lat));
let point = LatLon::new(lat, lon).unwrap();
let s = point.to_mgrs(Precision::new(p).unwrap()).to_string();
let cell: Mgrs = s.parse().unwrap();
prop_assert_eq!(cell.to_string(), s);
}
#[test]
fn format_parse_containment(lat in lat_strategy(), lon in lon_strategy(), p in 0u8..=11) {
prop_assume!(lat.is_finite() && (-90.0..=90.0).contains(&lat));
let point = LatLon::new(lat, lon).unwrap();
let u = point.to_utmups();
let cell = point.to_mgrs(Precision::new(p).unwrap());
let center = cell.to_utmups();
prop_assert_eq!(center.zone(), u.zone());
let half = 0.5 * 10f64.powi(5 - i32::from(p));
let eps = 2f64.powi(-28) + 1e-9;
let de = (center.easting() - u.easting()).abs();
let fold = |n: f64, north: bool| if north { n } else { n - 10_000_000.0 };
let dn = (fold(center.northing(), center.is_north())
- fold(u.northing(), u.is_north()))
.abs();
prop_assert!(de <= half + eps, "easting {de} > {half}");
prop_assert!(dn <= half + eps, "northing {dn} > {half}");
}
#[test]
fn truncation_nesting(lat in lat_strategy(), lon in lon_strategy()) {
prop_assume!(lat.is_finite() && (-90.0..=90.0).contains(&lat));
let point = LatLon::new(lat, lon).unwrap();
let s11 = point.to_mgrs(Precision::MAX).to_string();
let stem = s11.len() - 22;
for p in 0u8..=11 {
let s = point.to_mgrs(Precision::new(p).unwrap()).to_string();
let w = usize::from(p);
prop_assert_eq!(&s[..stem], &s11[..stem]);
prop_assert_eq!(&s[stem..stem + w], &s11[stem..stem + w]);
prop_assert_eq!(&s[stem + w..], &s11[stem + 11..stem + 11 + w]);
}
}
#[test]
fn zero_equivalence(lon in lon_strategy(), p in 0u8..=11) {
let a = LatLon::new(0.0, lon).unwrap();
let b = LatLon::new(-0.0, lon).unwrap();
let (ua, ub) = (a.to_utmups(), b.to_utmups());
prop_assert_eq!(ua.zone(), ub.zone());
prop_assert_eq!(ua.is_north(), ub.is_north());
prop_assert_eq!(ua.easting(), ub.easting());
prop_assert_eq!(ua.northing(), ub.northing());
let prec = Precision::new(p).unwrap();
prop_assert_eq!(a.to_mgrs(prec).to_string(), b.to_mgrs(prec).to_string());
}
#[test]
fn display_total_over_accepted_space(
zone in 1u8..=60,
north in proptest::bool::ANY,
e in 0.0..=1_000_000.0f64,
nabs in -9_100_000.0..=9_600_000.0f64,
p in 0u8..=11,
) {
let n = if north { nabs } else { nabs + 10_000_000.0 };
let u = UtmUps::utm(zone, north, e, n).unwrap();
match u.to_mgrs(Precision::new(p).unwrap()) {
Ok(m) => {
let s = m.to_string();
let back: Mgrs = s.parse().unwrap();
prop_assert_eq!(back, m);
}
Err(Error::OutsideMgrsExtent { .. }) => {}
Err(other) => prop_assert!(false, "unexpected error {}", other),
}
}
#[test]
fn display_total_over_accepted_ups_space(
north in proptest::bool::ANY,
e in 700_000.0..=3_300_000.0f64,
n in 700_000.0..=3_300_000.0f64,
p in 0u8..=11,
) {
if let Ok(u) = UtmUps::ups(north, e, n) {
match u.to_mgrs(Precision::new(p).unwrap()) {
Ok(m) => {
let s = m.to_string();
let back: Mgrs = s.parse().unwrap();
prop_assert_eq!(back, m);
}
Err(Error::OutsideMgrsExtent { .. }) => {}
Err(other) => prop_assert!(false, "unexpected error {}", other),
}
}
}
}
#[test]
fn boundary_ladders() {
let p3 = Precision::new(3).unwrap();
let p11 = Precision::MAX;
let band_edges: Vec<f64> = (-10..=10).map(|b| f64::from(b) * 8.0).collect();
let mut lats: Vec<f64> = vec![-90.0, -80.0, 0.0, 84.0, 90.0, 56.0, 64.0, 72.0];
lats.extend(band_edges);
let lons = [
-180.0, -174.0, -6.0, 0.0, 3.0, 6.0, 9.0, 12.0, 21.0, 33.0, 42.0, 174.0,
];
for &lat0 in &lats {
for &lon0 in &lons {
for step in -32i64..=32 {
let lat = ulps(lat0, step).clamp(-90.0, 90.0);
let point = match LatLon::new(lat, lon0) {
Ok(p) => p,
Err(_) => continue,
};
let u = point.to_utmups();
let _ = u.to_latlon();
for &p in &[p3, p11] {
let cell = point.to_mgrs(p);
let s = cell.to_string();
let back: Mgrs = s
.parse()
.unwrap_or_else(|e| panic!("lat {lat} lon {lon0} p{}: {s}: {e}", p.get()));
assert_eq!(back.to_string(), s);
let c = back.to_utmups();
if c.zone() == u.zone() {
let half = 0.5 * 10f64.powi(5 - i32::from(p.get()));
let eps = 2f64.powi(-28) + 1e-9;
assert!((c.easting() - u.easting()).abs() <= half + eps);
}
}
}
}
}
assert_eq!(LatLon::new(84.0, 0.0).unwrap().to_utmups().zone(), 0);
assert_eq!(
LatLon::new(ulps(84.0, -1), 0.0).unwrap().to_utmups().zone(),
31
);
assert_eq!(LatLon::new(-80.0, 0.0).unwrap().to_utmups().zone(), 31);
assert_eq!(
LatLon::new(ulps(-80.0, -1), 0.0)
.unwrap()
.to_utmups()
.zone(),
0
);
let p5 = Precision::ONE_METER;
for &(e0, n0, north) in &[
(100_000.0, 4_511_315.0, true),
(900_000.0, 4_511_315.0, true),
(500_000.0, 0.0, true),
(500_000.0, 9_500_000.0, true),
(500_000.0, 10_000_000.0, false),
(500_000.0, 1_000_000.0, false),
] {
for step in -32i64..=32 {
let e = ulps(e0, step);
let u = match UtmUps::utm(18, north, e, n0) {
Ok(u) => u,
Err(_) => continue,
};
match u.to_mgrs(p5) {
Ok(m) => {
let s = m.to_string();
assert_eq!(s.parse::<Mgrs>().unwrap().to_string(), s);
}
Err(Error::OutsideMgrsExtent { .. }) => {}
Err(other) => panic!("unexpected {other}"),
}
let un = match UtmUps::utm(18, north, e0, ulps(n0, step)) {
Ok(u) => u,
Err(_) => continue,
};
match un.to_mgrs(p5) {
Ok(m) => {
let s = m.to_string();
assert_eq!(s.parse::<Mgrs>().unwrap().to_string(), s);
}
Err(Error::OutsideMgrsExtent { .. }) => {}
Err(other) => panic!("unexpected {other}"),
}
}
}
for &(north, edge) in &[
(true, 1_300_000.0),
(true, 2_700_000.0),
(false, 800_000.0),
(false, 3_200_000.0),
] {
for step in -32i64..=32 {
let e = ulps(edge, step);
if let Ok(u) = UtmUps::ups(north, e, 2_000_000.0) {
match u.to_mgrs(p5) {
Ok(m) => {
let s = m.to_string();
assert_eq!(s.parse::<Mgrs>().unwrap().to_string(), s);
}
Err(Error::OutsideMgrsExtent { .. }) => {}
Err(other) => panic!("unexpected {other}"),
}
}
}
}
}