this_algorithm/spatial-coordinate-systems/src/dmm.rs
2023-03-20 21:09:14 -04:00

205 lines
5.3 KiB
Rust

use crate::{
common::{optional_separator, parse_direction, parse_f64},
Direction, LatLon,
};
use nom::{
branch::alt,
character::complete::{self, space0},
combinator::{map, map_opt},
sequence::tuple,
IResult,
};
use std::{fmt, str::FromStr};
#[derive(PartialEq, Debug, Clone)]
pub struct Coordinate(pub DMM, pub DMM);
impl Coordinate {
pub fn parse(i: &str) -> IResult<&str, Self> {
map_opt(
tuple((DMM::parse, optional_separator(','), space0, DMM::parse)),
|(lat, _, _, lon)| {
// Ensure this is a north/south then east/west direction
if lat.direction.is_lat() && lon.direction.is_lon() {
Some(Coordinate(lat, lon))
} else {
None
}
},
)(i)
}
}
impl FromStr for Coordinate {
type Err = ();
fn from_str(i: &str) -> Result<Self, Self::Err> {
Self::parse(i).map_err(|_| ()).map(|(_, ret)| ret)
}
}
#[derive(PartialEq, Debug, Clone)]
pub struct DMM {
pub degrees: i16,
pub minutes: f64,
pub direction: Direction,
}
/// Parse only the numeric portion of the coordinate
fn parse_dmm_numeric(i: &str) -> IResult<&str, (i16, f64)> {
map(
tuple((
// Degrees
complete::i16,
optional_separator('°'),
space0,
// Minutes
parse_f64,
optional_separator('\''),
)),
|(degrees, _, _, minutes, _)| (degrees, minutes),
)(i)
}
impl DMM {
pub fn parse(i: &str) -> IResult<&str, DMM> {
map(
alt((
tuple((
// Degrees/Minutes
parse_dmm_numeric,
space0,
// Direction
parse_direction,
)),
map(
tuple((
// Direction
parse_direction,
space0,
// Degrees/Minutes
parse_dmm_numeric,
)),
|(direction, space, dmm)| (dmm, space, direction),
),
)),
|((degrees, minutes), _, direction)| {
let negate = direction == Direction::West || direction == Direction::South;
Self {
degrees: degrees * if negate { -1 } else { 1 },
minutes,
direction: if direction.is_lat() {
Direction::North
} else {
Direction::East
},
}
},
)(i)
}
pub fn try_from_decimal_degrees(d: f64, is_latitude: bool) -> Result<Self, ()> {
let bounds = if is_latitude {
-90_f64..=90_f64
} else {
-180_f64..=180_f64
};
if !bounds.contains(&d) {
return Err(());
}
let degrees = d as i16;
let minutes = d.fract() * 60_f64;
Ok(Self {
degrees,
minutes,
direction: if is_latitude {
Direction::North
} else {
Direction::East
},
})
}
pub fn to_decimal_degrees(&self) -> f64 {
self.degrees as f64 + self.minutes / 60_f64
}
}
impl fmt::Display for DMM {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}° {}' {}", self.degrees, self.minutes, self.direction)
}
}
impl fmt::Display for Coordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}, {}", self.0, self.1)
}
}
impl TryInto<LatLon> for Coordinate {
type Error = ();
fn try_into(self) -> Result<LatLon, Self::Error> {
LatLon::from(self.0.to_decimal_degrees(), self.1.to_decimal_degrees()).ok_or(())
}
}
impl TryFrom<LatLon> for Coordinate {
type Error = ();
fn try_from(value: LatLon) -> Result<Self, Self::Error> {
Ok(Self(
DMM::try_from_decimal_degrees(value.lat, true)?,
DMM::try_from_decimal_degrees(value.lon, false)?,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_general() {
macro_rules! p {
($tt:tt) => {{
let cvt = Coordinate::from_str($tt);
eprintln!("Testing: {} => {:?}", $tt, cvt);
assert!(cvt.is_ok());
}};
($tt:tt, DMM) => {{
let cvt = DMM::parse($tt);
eprintln!("Testing: {} => {:?}", $tt, cvt);
assert!(cvt.is_ok());
}};
}
// p!(r#"0° 0' 0" N 100° 30' 1" W"#);
// p!(r#"0 0 0 N 100 30 1 W"#);
p!("0° 0' N", DMM);
p!("0° 0'N", DMM);
p!("N 0° 0'", DMM);
p!("0° 0' N", DMM);
p!("0° 0'N", DMM);
p!("N 0° 0' 0", DMM);
p!(r#"E 100° 30'"#, DMM);
p!(r#"N 0° 0' E 100° 30'"#);
// parse_dmm_numeric(r#"38 53.2148425"#).unwrap();
// p!(r#"38 53.2148425"#, DMM);
// p!(r#"38 53.2148425'"#, DMM);
// p!(r#"38° 53.2148425"#, DMM);
// p!(r#"38° 53.2148425'"#, DMM);
// p!(r#"-77° -1.7611312866219464'"#, DMM);
// p!(r#"38° 53.2148425', -77° -1.7611312866219464'"#);
}
}