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 error enum and the parse-failure kinds.

use core::fmt;

/// The reasons a conversion or a parse can fail.
///
/// Every variant is reachable and has a test that reaches it.
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub enum Error {
    /// Latitude is NaN, infinite, or outside [-90, 90].
    LatOutOfRange {
        /// The rejected latitude.
        found: f64,
    },
    /// Longitude is NaN or infinite.
    LonOutOfRange {
        /// The rejected longitude.
        found: f64,
    },
    /// UTM zone outside 1 to 60, or 0 passed to [`UtmUps::utm`](crate::UtmUps::utm).
    ZoneOutOfRange {
        /// The rejected zone number.
        found: u8,
    },
    /// Easting outside the wide transfer range for the stated grid.
    EastingOutOfRange {
        /// The rejected easting in metres.
        found: f64,
    },
    /// Northing outside the wide transfer range for the stated grid.
    NorthingOutOfRange {
        /// The rejected northing in metres.
        found: f64,
    },
    /// Precision outside 0 to 11.
    PrecisionOutOfRange {
        /// The rejected digit-pair count.
        found: u8,
    },
    /// The coordinate is valid UTM or UPS but outside the MGRS grid coverage.
    OutsideMgrsExtent {
        /// The easting of the rejected coordinate in metres.
        easting: f64,
        /// The northing of the rejected coordinate in metres.
        northing: f64,
    },
    /// The string is not an MGRS reference. The kind names the first rule it broke.
    Parse(ParseErrorKind),
}

/// The first parse rule an invalid MGRS string broke.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ParseErrorKind {
    /// The string is empty after trimming ASCII whitespace.
    Empty,
    /// A zone-only reference such as "18T". Deliberately unsupported: a cell six
    /// degrees wide has no useful center.
    GridZoneOnly,
    /// The leading digits are not a zone number in 1 to 60.
    BadZoneNumber,
    /// The letter after the zone is not a band letter C to X (`I` and `O` excluded).
    BadBandLetter,
    /// A 100 km square letter is not in the MGRS alphabets.
    BadSquareLetters,
    /// Square letters valid in the alphabet but not for this zone.
    SquareZoneMismatch,
    /// The implied northing is inconsistent with the band letter.
    SquareBandMismatch,
    /// The digit block has an odd number of digits.
    OddDigitCount,
    /// The digit block has more than 22 digits.
    TooManyDigits,
    /// The string contains a non-ASCII byte or internal whitespace.
    NonAsciiOrWhitespace,
    /// A character that is neither a letter nor a digit where digits were expected.
    TrailingInput,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::LatOutOfRange { found } => {
                write!(f, "latitude {found} is not a finite value in [-90, 90]")
            }
            Error::LonOutOfRange { found } => {
                write!(f, "longitude {found} is not finite")
            }
            Error::ZoneOutOfRange { found } => {
                write!(f, "zone {found} is outside 1 to 60")
            }
            Error::EastingOutOfRange { found } => {
                write!(f, "easting {found} m is outside the transfer range")
            }
            Error::NorthingOutOfRange { found } => {
                write!(f, "northing {found} m is outside the transfer range")
            }
            Error::PrecisionOutOfRange { found } => {
                write!(f, "precision {found} is outside 0 to 11")
            }
            Error::OutsideMgrsExtent { easting, northing } => {
                write!(
                    f,
                    "easting {easting} m, northing {northing} m is outside the MGRS grid coverage"
                )
            }
            Error::Parse(kind) => write!(f, "invalid MGRS string: {kind}"),
        }
    }
}

impl fmt::Display for ParseErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let msg = match self {
            ParseErrorKind::Empty => "empty string",
            ParseErrorKind::GridZoneOnly => "grid-zone-only reference is not supported",
            ParseErrorKind::BadZoneNumber => "zone number is not in 1 to 60",
            ParseErrorKind::BadBandLetter => "band letter is not C to X (I and O excluded)",
            ParseErrorKind::BadSquareLetters => "100 km square letters are not in the alphabets",
            ParseErrorKind::SquareZoneMismatch => "square letters do not belong to this zone",
            ParseErrorKind::SquareBandMismatch => {
                "square letters imply a northing outside this band"
            }
            ParseErrorKind::OddDigitCount => "digit block has an odd number of digits",
            ParseErrorKind::TooManyDigits => "digit block has more than 22 digits",
            ParseErrorKind::NonAsciiOrWhitespace => "non-ASCII byte or internal whitespace",
            ParseErrorKind::TrailingInput => "unexpected character after the square letters",
        };
        f.write_str(msg)
    }
}

impl std::error::Error for Error {}