1#![deny(clippy::arithmetic_side_effects)]
13
14use core::fmt;
15use core::str::FromStr;
16
17use crate::error::{Error, ParseErrorKind};
18use crate::grid;
19use crate::latlon::LatLon;
20use crate::precision::Precision;
21use crate::utmups::UtmUps;
22
23const BAND_LETTERS: &str = "CDEFGHJKLMNPQRSTUVWX";
25const ROW_LETTERS: &str = "ABCDEFGHJKLMNPQRSTUV";
27const COL_SETS: [&str; 3] = ["STUVWXYZ", "ABCDEFGH", "JKLMNPQR"];
29
30const UPS_COLS: [(u8, i64, &str); 4] = [
33 (b'Y', 13, "RSTUXYZ"),
34 (b'Z', 20, "ABCFGHJ"),
35 (b'A', 8, "JKLPQRSTUXYZ"),
36 (b'B', 20, "ABCFGHJKLPQR"),
37];
38const UPS_N_ROWS: (i64, &str) = (13, "ABCDEFGHJKLMNP");
40const UPS_S_ROWS: (i64, &str) = (8, "ABCDEFGHJKLMNPQRSTUVWXYZ");
42
43const BAND_ROW_WINDOW: [(i64, i64); 20] = [
47 (-90, -81), (-80, -72), (-71, -63), (-63, -54), (-54, -45), (-45, -36), (-36, -27), (-27, -18), (-18, -9), (-9, -1), (0, 8), (8, 17), (17, 26), (26, 35), (35, 44), (44, 53), (53, 62), (62, 70), (71, 79), (80, 94), ];
68
69fn letter_at(table: &str, pos: i64) -> u8 {
70 usize::try_from(pos)
71 .ok()
72 .and_then(|i| table.as_bytes().get(i))
73 .copied()
74 .unwrap_or(b'?')
75}
76
77fn pos_of(table: &str, c: u8) -> Option<i64> {
78 table
79 .bytes()
80 .position(|b| b == c)
81 .and_then(|p| i64::try_from(p).ok())
82}
83
84fn is_mgrs_letter(c: u8) -> bool {
86 c.is_ascii_uppercase() && c != b'I' && c != b'O'
87}
88
89fn row_shift(zone: u8) -> i64 {
91 if zone.rem_euclid(2) == 0 {
92 5
93 } else {
94 0
95 }
96}
97
98fn band_window(ib: i64) -> (i64, i64) {
99 usize::try_from(ib.saturating_add(10))
100 .ok()
101 .and_then(|i| BAND_ROW_WINDOW.get(i))
102 .copied()
103 .unwrap_or((0, 0))
104}
105
106fn row_exception(cand: i64, ib: i64, col: i64) -> bool {
110 let sband = if ib >= 0 {
111 ib
112 } else {
113 ib.saturating_add(1).saturating_neg()
114 };
115 let srow = if cand >= 0 {
116 cand
117 } else {
118 cand.saturating_add(1).saturating_neg()
119 };
120 let c0 = col.saturating_sub(1); let scol = if c0 <= 3 {
122 c0
123 } else {
124 7_i64.saturating_sub(c0)
125 };
126 match (srow, sband) {
127 (70, 8) => scol >= 2,
128 (71, 7) => scol <= 2,
129 (79, 9) => scol >= 1,
130 (80, 8) => scol <= 1,
131 _ => false,
132 }
133}
134
135#[derive(Clone, Debug, PartialEq, Eq, Hash)]
142pub struct Mgrs {
143 zone: u8,
145 north: bool,
146 band: u8,
148 col: i64,
150 row: i64,
153 e_digits: i64,
155 n_digits: i64,
157 precision: Precision,
158}
159
160impl Mgrs {
161 pub fn precision(&self) -> Precision {
163 self.precision
164 }
165
166 pub fn to_utmups(&self) -> UtmUps {
170 let p = self.precision.get();
171 let e = grid::cell_center_axis(self.col, self.e_digits, p);
172 let n = grid::cell_center_axis(self.row, self.n_digits, p);
173 if self.zone == 0 {
174 UtmUps::raw(0, self.north, e, n)
175 } else {
176 UtmUps::raw(
177 self.zone,
178 self.north,
179 e,
180 grid::frame_northing(n, self.north),
181 )
182 }
183 }
184
185 pub fn to_latlon(&self) -> LatLon {
195 self.to_utmups().to_latlon()
196 }
197
198 fn square_letters(&self) -> (u8, u8) {
199 if self.zone == 0 {
200 let col = UPS_COLS
201 .iter()
202 .find(|(b, _, _)| *b == self.band)
203 .map(|&(_, base, table)| letter_at(table, self.col.saturating_sub(base)))
204 .unwrap_or(b'?');
205 let (rbase, rtable) = if self.north { UPS_N_ROWS } else { UPS_S_ROWS };
206 (col, letter_at(rtable, self.row.saturating_sub(rbase)))
207 } else {
208 let set = COL_SETS
209 .get(usize::from(self.zone.rem_euclid(3)))
210 .copied()
211 .unwrap_or("");
212 let col = letter_at(set, self.col.saturating_sub(1));
213 let row = letter_at(
214 ROW_LETTERS,
215 self.row.saturating_add(row_shift(self.zone)).rem_euclid(20),
216 );
217 (col, row)
218 }
219 }
220}
221
222impl fmt::Display for Mgrs {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 if self.zone > 0 {
227 write!(f, "{:02}", self.zone)?;
228 }
229 let (col, row) = self.square_letters();
230 write!(f, "{}{}{}", self.band as char, col as char, row as char)?;
231 let width = usize::from(self.precision.get());
232 if width > 0 {
233 write!(f, "{:0width$}", self.e_digits)?;
234 write!(f, "{:0width$}", self.n_digits)?;
235 }
236 Ok(())
237 }
238}
239
240pub(crate) fn from_latlon(ll: LatLon, precision: Precision) -> Mgrs {
244 let (zone, north, e, n) = ll.utmups_parts();
245 if zone == 0 {
246 let (e, n) = grid::ups_nudge(north, e, n);
247 make_ups(north, e, n, precision)
248 } else {
249 let (e, n) = grid::utm_nudge(north, e, n);
250 make_utm(zone, north, grid::band_index(ll.lat()), e, n, precision)
251 }
252}
253
254pub(crate) fn from_utmups(u: UtmUps, precision: Precision) -> Result<Mgrs, Error> {
257 let outside = Error::OutsideMgrsExtent {
258 easting: u.easting(),
259 northing: u.northing(),
260 };
261 if u.zone() == 0 {
262 let (e, n) = grid::ups_extent(u.is_north(), u.easting(), u.northing()).ok_or(outside)?;
263 Ok(make_ups(u.is_north(), e, n, precision))
264 } else {
265 let (e, n) = grid::utm_extent(u.is_north(), u.easting(), u.northing()).ok_or(outside)?;
266 let lat = UtmUps::raw(u.zone(), u.is_north(), e, n).latlon_parts().0;
269 Ok(make_utm(
270 u.zone(),
271 u.is_north(),
272 grid::band_index(lat),
273 e,
274 n,
275 precision,
276 ))
277 }
278}
279
280fn make_utm(zone: u8, north: bool, ib: i64, e: f64, n_frame: f64, precision: Precision) -> Mgrs {
281 let (col, off_e) = grid::axis_fields(e);
282 let (row_frame, off_n) = grid::axis_fields(n_frame);
283 let row = row_frame.saturating_sub(if north { 0 } else { 100 });
284 let ib = if row < 0 { ib.min(-1) } else { ib.max(0) };
289 let band = letter_at(BAND_LETTERS, ib.saturating_add(10));
290 Mgrs {
291 zone,
292 north: row >= 0,
296 band,
297 col: col.clamp(1, 8),
302 row: row.clamp(-90, 94),
303 e_digits: grid::digits_at(off_e, precision.get()),
304 n_digits: grid::digits_at(off_n, precision.get()),
305 precision,
306 }
307}
308
309fn make_ups(north: bool, e: f64, n: f64, precision: Precision) -> Mgrs {
310 let (col, off_e) = grid::axis_fields(e);
311 let (row, off_n) = grid::axis_fields(n);
312 let (lo, hi) = if north { (13, 26) } else { (8, 31) };
313 let east_half = e >= 2_000_000.0;
314 let band = match (north, east_half) {
315 (true, true) => b'Z',
316 (true, false) => b'Y',
317 (false, true) => b'B',
318 (false, false) => b'A',
319 };
320 Mgrs {
321 zone: 0,
322 north,
323 band,
324 col: col.clamp(lo, hi),
325 row: row.clamp(lo, hi),
326 e_digits: grid::digits_at(off_e, precision.get()),
327 n_digits: grid::digits_at(off_n, precision.get()),
328 precision,
329 }
330}
331
332impl FromStr for Mgrs {
333 type Err = Error;
334
335 fn from_str(s: &str) -> Result<Mgrs, Error> {
339 parse_mgrs(s)
340 }
341}
342
343fn parse_mgrs(s: &str) -> Result<Mgrs, Error> {
344 use ParseErrorKind as K;
345 let t = s.trim_matches(|c: char| c.is_ascii_whitespace());
346 if t.is_empty() {
347 return Err(Error::Parse(K::Empty));
348 }
349 if !t.is_ascii() {
350 return Err(Error::Parse(K::NonAsciiOrWhitespace));
351 }
352 let bytes = t.as_bytes();
353 if bytes.iter().any(|b| b.is_ascii_whitespace()) {
354 return Err(Error::Parse(K::NonAsciiOrWhitespace));
355 }
356
357 let zone_len = bytes.iter().take_while(|b| b.is_ascii_digit()).count();
359 if zone_len > 2 {
360 return Err(Error::Parse(K::BadZoneNumber));
361 }
362 let zone: u8 = {
363 let mut z: u8 = 0;
364 for b in bytes.iter().take(zone_len) {
365 z = z.saturating_mul(10).saturating_add(b.saturating_sub(b'0'));
366 }
367 if zone_len > 0 && !(1..=60).contains(&z) {
368 return Err(Error::Parse(K::BadZoneNumber));
369 }
370 z
371 };
372
373 let mut letters = bytes.iter().skip(zone_len);
375 let band = match letters.next() {
376 Some(b) => b.to_ascii_uppercase(),
377 None => return Err(Error::Parse(K::BadBandLetter)),
378 };
379 if zone > 0 {
380 if pos_of(BAND_LETTERS, band).is_none() {
381 return Err(Error::Parse(K::BadBandLetter));
382 }
383 } else if !matches!(band, b'A' | b'B' | b'Y' | b'Z') {
384 return Err(Error::Parse(K::BadBandLetter));
385 }
386
387 let (col_c, row_c) = match (letters.next(), letters.next()) {
389 (None, _) => return Err(Error::Parse(K::GridZoneOnly)),
390 (Some(_), None) => return Err(Error::Parse(K::BadSquareLetters)),
391 (Some(c), Some(r)) => (c.to_ascii_uppercase(), r.to_ascii_uppercase()),
392 };
393 if !col_c.is_ascii_alphabetic() || !row_c.is_ascii_alphabetic() {
394 return Err(Error::Parse(K::BadSquareLetters));
395 }
396
397 let digits = bytes.get(zone_len.saturating_add(3)..).unwrap_or(&[]);
399 if let Some(_bad) = digits.iter().find(|b| !b.is_ascii_digit()) {
400 return Err(Error::Parse(K::TrailingInput));
401 }
402 if digits.len() > 22 {
403 return Err(Error::Parse(K::TooManyDigits));
404 }
405 if digits.len().rem_euclid(2) != 0 {
406 return Err(Error::Parse(K::OddDigitCount));
407 }
408 let half = digits.len().div_euclid(2);
409 let precision = Precision::new(half as u8).map_err(|_| Error::Parse(K::TooManyDigits))?;
410 let accumulate = |ds: &[u8]| -> Result<i64, Error> {
413 ds.iter()
414 .try_fold(0_i64, |acc, b| {
415 acc.checked_mul(10)?
416 .checked_add(i64::from(b.saturating_sub(b'0')))
417 })
418 .ok_or(Error::Parse(K::TooManyDigits))
419 };
420 let e_digits = accumulate(digits.get(..half).unwrap_or(&[]))?;
421 let n_digits = accumulate(digits.get(half..).unwrap_or(&[]))?;
422
423 if zone > 0 {
424 let set = COL_SETS
426 .get(usize::from(zone.rem_euclid(3)))
427 .copied()
428 .unwrap_or("");
429 let col = match pos_of(set, col_c) {
430 Some(p) => p.saturating_add(1),
431 None if is_mgrs_letter(col_c) => {
432 return Err(Error::Parse(K::SquareZoneMismatch));
433 }
434 None => return Err(Error::Parse(K::BadSquareLetters)),
435 };
436 let row_pos = match pos_of(ROW_LETTERS, row_c) {
438 Some(p) => p,
439 None => return Err(Error::Parse(K::BadSquareLetters)),
440 };
441 let ib = pos_of(BAND_LETTERS, band)
442 .map(|p| p.saturating_sub(10))
443 .unwrap_or(0);
444 let residue = row_pos.saturating_sub(row_shift(zone)).rem_euclid(20);
445 let (minrow, maxrow) = band_window(ib);
446 let start = minrow.saturating_sub(1);
447 let row = start.saturating_add(residue.saturating_sub(start).rem_euclid(20));
448 let in_window = row >= minrow && row <= maxrow;
449 if !in_window && !row_exception(row, ib, col) {
450 return Err(Error::Parse(K::SquareBandMismatch));
451 }
452 Ok(Mgrs {
453 zone,
454 north: ib >= 0,
455 band,
456 col,
457 row,
458 e_digits,
459 n_digits,
460 precision,
461 })
462 } else {
463 let north = matches!(band, b'Y' | b'Z');
464 let col = match UPS_COLS
465 .iter()
466 .find(|(b, _, _)| *b == band)
467 .and_then(|&(_, base, table)| pos_of(table, col_c).map(|p| base.saturating_add(p)))
468 {
469 Some(c) => c,
470 None if is_mgrs_letter(col_c) => {
471 return Err(Error::Parse(K::SquareZoneMismatch));
472 }
473 None => return Err(Error::Parse(K::BadSquareLetters)),
474 };
475 let (rbase, rtable) = if north { UPS_N_ROWS } else { UPS_S_ROWS };
476 let row = match pos_of(rtable, row_c) {
477 Some(p) => rbase.saturating_add(p),
478 None if is_mgrs_letter(row_c) => {
479 return Err(Error::Parse(K::SquareZoneMismatch));
480 }
481 None => return Err(Error::Parse(K::BadSquareLetters)),
482 };
483 Ok(Mgrs {
484 zone: 0,
485 north,
486 band,
487 col,
488 row,
489 e_digits,
490 n_digits,
491 precision,
492 })
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn letter_tables_pinned() {
502 assert_eq!(BAND_LETTERS.len(), 20);
504 assert_eq!(ROW_LETTERS.len(), 20);
505 for set in COL_SETS {
506 assert_eq!(set.len(), 8);
507 }
508 assert_eq!(UPS_N_ROWS.1.len(), 14);
509 assert_eq!(UPS_S_ROWS.1.len(), 24);
510 let widths: Vec<usize> = UPS_COLS.iter().map(|(_, _, t)| t.len()).collect();
511 assert_eq!(widths, vec![7, 7, 12, 12]);
512 }
513
514 #[test]
515 fn band_windows_cover_the_grid_contiguously() {
516 for pair in BAND_ROW_WINDOW.windows(2) {
519 if let [(_, hi), (lo2, _)] = pair {
520 assert!(*lo2 == *hi || *lo2 == hi.saturating_add(1));
521 }
522 }
523 assert_eq!(BAND_ROW_WINDOW.first(), Some(&(-90, -81)));
524 assert_eq!(BAND_ROW_WINDOW.last(), Some(&(80, 94)));
525 }
526}