mod util;
use mgrs_convert::{Error, LatLon, Mgrs, ParseErrorKind, Precision, UtmUps};
#[test]
fn defect_m1_precision_8_to_11_parse_without_overflow() {
for p in 8usize..=11 {
let digits = "9".repeat(p);
let s = format!("18TWL{digits}{digits}");
let cell: Mgrs = s.parse().unwrap_or_else(|e| panic!("{s}: {e}"));
assert_eq!(cell.to_string(), s, "identity at p{p}");
let c = cell.to_utmups();
assert!(c.easting() > 599_999.0 && c.easting() < 600_000.0);
assert!(c.northing() > 4_599_999.0 && c.northing() < 4_600_000.0);
}
for extra in ["7", "77", "777"] {
let s = format!("18TWL85664121{extra}11315421{extra}");
let cell: Mgrs = s.parse().unwrap_or_else(|e| panic!("{s}: {e}"));
assert_eq!(cell.to_string(), s);
let c = cell.to_utmups();
assert!((c.easting() - 585_664.121).abs() < 0.01);
assert!((c.northing() - 4_511_315.421).abs() < 0.01);
}
let p8 = Precision::new(8).unwrap();
let cell = LatLon::new(40.748333, -73.985278).unwrap().to_mgrs(p8);
let reparsed: Mgrs = cell.to_string().parse().unwrap();
assert_eq!(reparsed, cell);
}
#[test]
fn defect_m2_transfer_ring_errs_instead_of_panicking() {
let p5 = Precision::ONE_METER;
for &e in &[
0.0,
50_000.0,
99_999.999,
900_000.001,
950_000.0,
1_000_000.0,
] {
for &north in &[true, false] {
let n = if north { 4_511_315.0 } else { 5_488_685.0 };
let u = UtmUps::utm(18, north, e, n).unwrap();
assert!(
matches!(u.to_mgrs(p5), Err(Error::OutsideMgrsExtent { .. })),
"easting {e} north {north}"
);
let _ = u.to_latlon(); }
}
for &(north, n) in &[
(true, 9_500_000.001),
(true, 9_600_000.0),
(true, -9_000_000.001),
(true, -9_100_000.0),
(false, 999_999.999),
(false, 900_000.0),
(false, 19_500_000.001),
(false, 19_600_000.0),
] {
let u = UtmUps::utm(18, north, 500_000.0, n).unwrap();
assert!(
matches!(u.to_mgrs(p5), Err(Error::OutsideMgrsExtent { .. })),
"northing {n} north {north}"
);
}
for &(north, e, n) in &[
(true, 1_250_000.0, 2_000_000.0),
(true, 2_750_000.0, 2_000_000.0),
(true, 2_000_000.0, 1_299_999.999),
(true, 2_000_000.0, 2_700_000.001),
(false, 750_000.0, 2_000_000.0),
(false, 3_250_000.0, 2_000_000.0),
(false, 2_000_000.0, 799_999.999),
(false, 2_000_000.0, 3_200_000.001),
] {
let u = UtmUps::ups(north, e, n).unwrap();
assert!(
matches!(u.to_mgrs(p5), Err(Error::OutsideMgrsExtent { .. })),
"ups {e} {n} north {north}"
);
let _ = u.to_latlon();
}
let mut formatted = 0u32;
let mut ringed = 0u32;
let mut e = 0.0;
while e <= 1_000_000.0 {
let mut n = -9_100_000.0;
while n <= 9_600_000.0 {
for &north in &[true, false] {
let frame = if north { n } else { n + 10_000_000.0 };
let u = UtmUps::utm(31, north, e, frame).unwrap();
match u.to_mgrs(p5) {
Ok(m) => {
let s = m.to_string();
let back: Mgrs = s.parse().unwrap();
assert_eq!(back, m, "{s}");
formatted += 1;
}
Err(Error::OutsideMgrsExtent { .. }) => ringed += 1,
Err(other) => panic!("unexpected {other}"),
}
}
n += 471_119.0;
}
e += 61_237.0;
}
assert!(formatted > 500 && ringed > 100, "{formatted} {ringed}");
}
#[test]
fn defect_m3_precision_is_validated_at_construction() {
for p in 0u8..=11 {
assert_eq!(Precision::new(p).map(Precision::get), Ok(p));
}
for p in [12u8, 13, 27, 255] {
assert_eq!(
Precision::new(p),
Err(Error::PrecisionOutOfRange { found: p })
);
}
let p0 = Precision::new(0).unwrap();
let cell = LatLon::new(40.748333, -73.985278).unwrap().to_mgrs(p0);
assert_eq!(cell.to_string(), "18TWL");
assert_eq!("18TWL".parse::<Mgrs>().unwrap(), cell);
let too_many = format!("18TWL{}", "4".repeat(24));
assert_eq!(
too_many.parse::<Mgrs>(),
Err(Error::Parse(ParseErrorKind::TooManyDigits))
);
}
#[test]
fn defect_m4_boundary_epsilon_behavior() {
let p8 = Precision::new(8).unwrap();
let at = UtmUps::utm(18, true, 900_000.0, 4_511_315.422).unwrap();
assert_eq!(at.to_mgrs(p8).unwrap().to_string(), "18TZL9999999911315422");
let below = UtmUps::utm(
18,
true,
f64::from_bits(900_000.0_f64.to_bits() - 1),
4_511_315.422,
)
.unwrap();
assert_eq!(
below.to_mgrs(p8).unwrap().to_string(),
"18TZL9999999911315422"
);
let above = UtmUps::utm(
18,
true,
f64::from_bits(900_000.0_f64.to_bits() + 1),
4_511_315.422,
)
.unwrap();
assert!(matches!(
above.to_mgrs(p8),
Err(Error::OutsideMgrsExtent { .. })
));
let s1 = UtmUps::utm(18, false, 500_000.0, 9_999_999.999_5).unwrap();
assert_eq!(s1.to_mgrs(p8).unwrap().to_string(), "18MWE0000000099999999");
let s2 = UtmUps::utm(18, false, 500_000.0, 10_000_000.0).unwrap();
assert_eq!(s2.to_mgrs(p8).unwrap().to_string(), "18MWE0000000099999999");
}
#[test]
fn defect_m5_latitude_zero_is_band_n() {
let p6 = Precision::new(6).unwrap();
for lon in [-180.0, -73.985278, 0.0, 0.5, 77.0, 179.999999] {
let plus = LatLon::new(0.0, lon).unwrap().to_mgrs(p6).to_string();
let minus = LatLon::new(-0.0, lon).unwrap().to_mgrs(p6).to_string();
assert_eq!(plus, minus, "lon {lon}");
let band = plus.chars().nth(2).unwrap();
assert_eq!(band, 'N', "lon {lon}: {plus}");
}
assert_eq!(
LatLon::new(0.0, 0.0).unwrap().to_mgrs(p6).to_string(),
"31NAA660214000000"
);
let up = LatLon::new(f64::from_bits(1), 0.0).unwrap();
assert!(up.to_utmups().is_north());
let down = LatLon::new(-f64::from_bits(1), 0.0).unwrap();
assert!(!down.to_utmups().is_north());
let _ = up.to_mgrs(p6);
let _ = down.to_mgrs(p6);
let u = UtmUps::utm(31, true, 166_021.443, 0.0).unwrap();
let m = u.to_mgrs(p6).unwrap().to_string();
assert!(m.starts_with("31NAA"), "{m}");
}
#[test]
fn review_southern_limit_cancellation_is_a_transfer_ring_error() {
let precisions = [0u8, 5, 8, 11].map(|p| Precision::new(p).unwrap());
let mut n = 1_000_000.0_f64;
for step in 1..=8u32 {
n = f64::from_bits(n.to_bits() - 1);
let u = UtmUps::utm(18, false, 500_000.0, n).unwrap();
for p in precisions {
assert!(
matches!(u.to_mgrs(p), Err(Error::OutsideMgrsExtent { .. })),
"step {step} northing {n} p{}",
p.get()
);
}
}
let p11 = Precision::new(11).unwrap();
for (north, top) in [(true, 9_500_000.0_f64), (false, 19_500_000.0_f64)] {
let above = f64::from_bits(top.to_bits() + 1);
let u = UtmUps::utm(18, north, 500_000.0, above).unwrap();
assert!(
matches!(u.to_mgrs(p11), Err(Error::OutsideMgrsExtent { .. })),
"above {top}"
);
}
for (north, n, want) in [
(false, 1_000_000.0, "18CWR0000000000000000000000"),
(true, -9_000_000.0, "18CWR0000000000000000000000"),
(true, 9_500_000.0, "18XWV0000000000099999999999"),
(false, 19_500_000.0, "18XWV0000000000099999999999"),
] {
let u = UtmUps::utm(18, north, 500_000.0, n).unwrap();
assert_eq!(u.to_mgrs(p11).unwrap().to_string(), want, "northing {n}");
}
}
#[test]
fn every_error_variant_is_reachable() {
assert!(matches!(
LatLon::new(90.5, 0.0),
Err(Error::LatOutOfRange { .. })
));
assert!(matches!(
LatLon::new(f64::NAN, 0.0),
Err(Error::LatOutOfRange { .. })
));
assert!(matches!(
LatLon::new(0.0, f64::INFINITY),
Err(Error::LonOutOfRange { .. })
));
assert!(matches!(
UtmUps::utm(0, true, 500_000.0, 0.0),
Err(Error::ZoneOutOfRange { found: 0 })
));
assert!(matches!(
UtmUps::utm(61, true, 500_000.0, 0.0),
Err(Error::ZoneOutOfRange { found: 61 })
));
assert!(matches!(
UtmUps::utm(18, true, -0.001, 0.0),
Err(Error::EastingOutOfRange { .. })
));
assert!(matches!(
UtmUps::utm(18, true, f64::NAN, 0.0),
Err(Error::EastingOutOfRange { .. })
));
assert!(matches!(
UtmUps::utm(18, true, 500_000.0, 9_600_000.001),
Err(Error::NorthingOutOfRange { .. })
));
assert!(matches!(
UtmUps::ups(true, 1_199_999.999, 2_000_000.0),
Err(Error::EastingOutOfRange { .. })
));
assert!(matches!(
UtmUps::ups(false, 2_000_000.0, 3_300_000.001),
Err(Error::NorthingOutOfRange { .. })
));
assert!(matches!(
Precision::new(12),
Err(Error::PrecisionOutOfRange { found: 12 })
));
let ring = UtmUps::utm(18, true, 50_000.0, 4_511_315.0).unwrap();
assert!(matches!(
ring.to_mgrs(Precision::ONE_METER),
Err(Error::OutsideMgrsExtent { .. })
));
}
#[test]
fn every_parse_error_kind_is_reachable() {
use ParseErrorKind as K;
let kind = |s: &str| -> K {
match s.parse::<Mgrs>() {
Err(Error::Parse(k)) => k,
other => panic!("{s}: expected parse error, got {other:?}"),
}
};
assert_eq!(kind(""), K::Empty);
assert_eq!(kind(" "), K::Empty);
assert_eq!(kind("18T"), K::GridZoneOnly);
assert_eq!(kind("Y"), K::GridZoneOnly);
assert_eq!(kind("00AAA00"), K::BadZoneNumber);
assert_eq!(kind("61CAA00"), K::BadZoneNumber);
assert_eq!(kind("123ABC"), K::BadZoneNumber);
assert_eq!(kind("18IAA00"), K::BadBandLetter);
assert_eq!(kind("18"), K::BadBandLetter);
assert_eq!(kind("Q"), K::BadBandLetter);
assert_eq!(kind("18TIL8566411315"), K::BadSquareLetters);
assert_eq!(kind("18TW"), K::BadSquareLetters);
assert_eq!(kind("18TW1"), K::BadSquareLetters);
assert_eq!(kind("18TAL0000000000"), K::SquareZoneMismatch);
assert_eq!(kind("YAA00"), K::SquareZoneMismatch);
assert_eq!(kind("18TWA0000000000"), K::SquareBandMismatch);
assert_eq!(kind("18TWL856641131"), K::OddDigitCount);
assert_eq!(kind(&format!("18TWL{}", "4".repeat(24))), K::TooManyDigits);
assert_eq!(kind("18TWL 85664 11315"), K::NonAsciiOrWhitespace);
assert_eq!(kind("18TWл8566411315"), K::NonAsciiOrWhitespace);
assert_eq!(kind("18TWL85664x1315"), K::TrailingInput);
assert!(" 18twl8566411315\t".parse::<Mgrs>().is_ok());
assert!("08UNC00".parse::<Mgrs>().is_ok());
}
#[test]
fn error_display_is_useful() {
let e = Error::OutsideMgrsExtent {
easting: 50_000.0,
northing: 4_511_315.0,
};
let msg = format!("{e}");
assert!(msg.contains("50000"), "{msg}");
let dynerr: &dyn std::error::Error = &e;
assert!(!dynerr.to_string().is_empty());
let p = Error::Parse(ParseErrorKind::GridZoneOnly);
assert!(format!("{p}").contains("grid-zone-only"));
}