mgrs-convert 0.1.0

Convert WGS84 latitude-longitude to and from UTM, UPS, and MGRS grid references, round-tripping exactly at every precision from 100 km to 1 um
Documentation
//! Property tests for invariants 1 through 6, plus the deterministic
//! boundary-fuzz ladders: nextafter walks across every normative boundary.
//! cargo-fuzz is not part of the gate; the string-totality and boundary
//! properties here are bounded proptest sweeps instead, biased to the
//! boundary clusters where mechanisms M4 and M5 lived.

mod util;

use mgrs_convert::{Error, LatLon, Mgrs, Precision, UtmUps};
use proptest::prelude::*;

fn ulps(x: f64, steps: i64) -> f64 {
    // Walk `steps` representable values away from x, sign-aware.
    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
}

/// A latitude strategy biased to the boundary clusters.
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! {
    /// Invariant 1: every f64 pair either constructs or errs; every
    /// constructed value converts on every path at every precision.
    #[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);
        }
    }

    /// Invariant 1 for the parser: arbitrary strings never panic.
    #[test]
    fn parser_is_total(s in "\\PC{0,40}") {
        let _ = s.parse::<Mgrs>();
    }

    /// Invariant 1 for byte-level ASCII noise near valid shapes.
    #[test]
    fn parser_is_total_on_ascii_noise(s in "[0-9A-Za-z \t]{0,30}") {
        let _ = s.parse::<Mgrs>();
    }

    /// Invariants 2 and 6: parse-format identity on generated cells, and
    /// every reachable Mgrs formats.
    #[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);
    }

    /// Invariant 3: the parsed cell of format(x) contains x, within
    /// BOUNDARY_EPS at excluded upper edges, compared in grid space.
    #[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();
        // Frame-align the northings before differencing.
        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}");
    }

    /// Invariant 4: the precision-p string is the per-axis prefix of the
    /// precision-11 string.
    #[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]);
        }
    }

    /// Invariant 5: -0.0 and 0.0 latitude are indistinguishable on every path.
    #[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());
    }

    /// Invariant 6 over the accepted UtmUps space: to_mgrs is the only
    /// fallible step, and its Ok value always formats and re-parses.
    #[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),
        }
    }

    /// The UPS half of invariant 6.
    #[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),
            }
        }
    }
}

/// Boundary fuzz: 32-step nextafter ladders across every normative boundary,
/// asserting invariants 1, 3, and 5 hold on both sides.
#[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,
                };
                // Invariant 1: no panic anywhere on the ladder.
                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);
                    // Invariant 3 in grid space.
                    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);
                    }
                }
            }
        }
    }

    // The normative crossovers, exact: the UTM interval is [-80, 84), so 84
    // exactly is UPS and one ulp below is UTM band X; -80 is UTM band C and
    // one ulp below is UPS.
    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
    );

    // UtmUps-space ladders around the strict MGRS edges.
    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}"),
            }
        }
    }

    // UPS edges.
    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}"),
                }
            }
        }
    }
}