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
//! The frozen vector table, captured from GeoConvert (GeographicLib 2.7) at
//! stage 0, before any implementation code existed. Every row asserts both the
//! value and the absence of a panic. Rows whose id ends in `raw` were captured
//! as oracle behavior this crate diverged from; the divergence test at the
//! bottom replays the ones that still diverge, and V13raw (latitude 84, UPS)
//! now runs as a normal row because the crate matches the reference there.

mod util;

use mgrs_convert::{Error, LatLon, Mgrs, Precision, UtmUps};
use util::{ground_distance_m, parse_utm_triplet, utmups_from_input};

/// Tolerance for numeric comparisons against the oracle, metres. The normative
/// bound is 1 um per axis; the oracle prints nanometre digits, so print
/// quantization is three orders below the bound.
const TOL_M: f64 = 1e-6;

fn parse_geo(s: &str) -> LatLon {
    let toks: Vec<&str> = s.split_whitespace().collect();
    LatLon::new(toks[0].parse().expect("lat"), toks[1].parse().expect("lon"))
        .expect("valid geo input")
}

fn assert_utm_close(id: &str, got: UtmUps, expected: &str) {
    let (zone, north, e, n) = parse_utm_triplet(expected);
    assert_eq!(got.zone(), zone, "{id}: zone");
    assert_eq!(got.is_north(), north, "{id}: hemisphere");
    assert!(
        (got.easting() - e).abs() <= TOL_M,
        "{id}: easting {} vs {e}",
        got.easting()
    );
    assert!(
        (got.northing() - n).abs() <= TOL_M,
        "{id}: northing {} vs {n}",
        got.northing()
    );
}

#[test]
fn frozen_vector_table() {
    let rows = util::read_tsv("frozen.tsv");
    assert!(rows.len() >= 60, "frozen.tsv is truncated");
    let mut executed = 0usize;
    for row in &rows {
        let (id, kind, input, path, expected) = (&row[0], &row[1], &row[2], &row[3], &row[4]);
        if (id.ends_with("raw") && id != "V13raw") || id == "Z0nu" {
            // Oracle-raw captures of the -0.0 hemisphere divergence; replayed
            // by deliberate_divergences below. V13raw (latitude 84, UPS north)
            // matches this crate since the half-open UTM interval landed, so
            // it runs as a normal row.
            continue;
        }
        if kind == "geoz31" {
            // Zone-forced oracle captures, kept for history: they were the
            // lat-84 oracle while this crate assigned 84 to UTM. The crate
            // has no zone forcing, so no public path reproduces them.
            continue;
        }
        match (kind.as_str(), path.as_str()) {
            ("geo", "utm") => {
                let got = parse_geo(input).to_utmups();
                assert_utm_close(id, got, expected);
            }
            ("geo", p) if p.starts_with("mgrs") => {
                let prec = Precision::new(p[4..].parse().expect("prec")).expect("prec range");
                let got = parse_geo(input).to_mgrs(prec).to_string();
                assert_eq!(&got, expected, "{id}");
            }
            ("utm" | "ups", p) if p.starts_with("mgrs") => {
                let prec = Precision::new(p[4..].parse().expect("prec")).expect("prec range");
                let u = utmups_from_input(input).expect("wide range accepts the input");
                match u.to_mgrs(prec) {
                    Ok(m) => assert_eq!(&m.to_string(), expected, "{id}"),
                    Err(Error::OutsideMgrsExtent { .. }) => {
                        assert!(
                            expected.starts_with("ERROR"),
                            "{id}: unexpected OutsideMgrsExtent"
                        );
                    }
                    Err(e) => panic!("{id}: unexpected error {e}"),
                }
            }
            ("utm" | "ups", "geo") => {
                let u = utmups_from_input(input).expect("wide range accepts the input");
                let got = u.to_latlon();
                let toks: Vec<&str> = expected.split_whitespace().collect();
                let (elat, elon): (f64, f64) =
                    (toks[0].parse().expect("lat"), toks[1].parse().expect("lon"));
                let d = ground_distance_m(got.lat(), got.lon(), elat, elon);
                assert!(d <= TOL_M, "{id}: ground distance {d} m");
            }
            ("mgrs", "geo") => {
                let m: Mgrs = input.parse().expect("valid MGRS input");
                let got = m.to_latlon();
                let toks: Vec<&str> = expected.split_whitespace().collect();
                let (elat, elon): (f64, f64) =
                    (toks[0].parse().expect("lat"), toks[1].parse().expect("lon"));
                let d = ground_distance_m(got.lat(), got.lon(), elat, elon);
                assert!(d <= TOL_M, "{id}: ground distance {d} m");
            }
            ("mgrs", "utm") => {
                let m: Mgrs = input.parse().expect("valid MGRS input");
                assert_utm_close(id, m.to_utmups(), expected);
            }
            other => panic!("unhandled row shape {other:?} in {id}"),
        }
        executed += 1;
    }
    assert!(executed >= 55, "only {executed} rows executed");
}

/// The recorded divergences from GeoConvert, each replayed: the oracle's raw
/// behavior is pinned so a change in either side shows up here.
#[test]
fn deliberate_divergences() {
    let rows = util::read_tsv("frozen.tsv");
    let raw = |id: &str| -> String {
        rows.iter()
            .find(|r| r[0] == id)
            .unwrap_or_else(|| panic!("{id} missing"))[4]
            .clone()
    };

    // Divergence 1, rule 1: -0.0 equals 0.0 on every path. GeoConvert reads
    // the sign and formats the southern equator cell.
    assert_eq!(raw("V2mraw"), "31MAV660214999999");
    let p6 = Precision::new(6).unwrap();
    let zero = LatLon::new(0.0, 0.0).unwrap().to_mgrs(p6).to_string();
    let negzero = LatLon::new(-0.0, 0.0).unwrap().to_mgrs(p6).to_string();
    assert_eq!(zero, "31NAA660214000000");
    assert_eq!(negzero, zero);
    let u = LatLon::new(-0.0, 0.0).unwrap().to_utmups();
    assert!(u.is_north(), "-0.0 is northern");
    assert_eq!(u.northing(), 0.0);
    // The oracle's raw numeric view of -0.0 is the southern frame.
    assert!(
        raw("Z0nu").starts_with("31s"),
        "oracle raw -0.0 is southern"
    );

    // Divergence 2: grid-zone-only references are rejected.
    assert!(matches!(
        "18T".parse::<Mgrs>(),
        Err(Error::Parse(mgrs_convert::ParseErrorKind::GridZoneOnly))
    ));

    // The divergence file itself stays in sync: exactly two entries.
    let divs = util::read_tsv("divergences.tsv");
    assert_eq!(divs.len(), 2, "divergence file grew without a test");
}

/// Latitude 84 exactly matches the reference: GeoConvert 2.7's standard zone
/// interval for UTM is [-80, 84), half-open, so 84 is UPS north. Pinned
/// against live-captured GeoConvert 2.7 strings so a change on either side of
/// the crossover shows up here.
#[test]
fn latitude_84_matches_the_reference() {
    let p11 = Precision::new(11).unwrap();
    let at84 = LatLon::new(84.0, 0.0).unwrap();
    let u = at84.to_utmups();
    assert_eq!(u.zone(), 0, "84 exactly is UPS");
    assert!(u.is_north(), "84 exactly is the north cap");
    assert_eq!(at84.to_mgrs(p11).to_string(), "ZAA0000000000033272296316");

    // One ulp below 84 is the last UTM latitude: zone 31, band X.
    let below = f64::from_bits(84.0_f64.to_bits() - 1);
    let last_utm = LatLon::new(below, 0.0).unwrap();
    assert_eq!(last_utm.to_utmups().zone(), 31);
    assert_eq!(
        last_utm.to_mgrs(p11).to_string(),
        "31XDP6500534493829005182447"
    );
}