use crate::{Coordinate, Error, Latitude, Longitude, inner};
use ordered_float::OrderedFloat;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Parsed {
Angle(Value),
Coordinate(Coordinate),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Value {
Unknown(OrderedFloat<f64>),
Latitude(Latitude),
Longitude(Longitude),
}
pub fn parse_str(s: &str) -> Result<Parsed, Error> {
if s.starts_with(|c: char| c.is_ascii_whitespace())
|| s.ends_with(|c: char| c.is_ascii_whitespace())
{
return Err(Error::InvalidWhitespace(s.to_string()));
}
if s.is_empty() {
return Err(Error::InvalidNumericFormat(s.to_string()));
}
match find_comma(s) {
Some(comma_pos) => parse_pair(s, comma_pos),
None => parse_single(s).map(Parsed::Angle),
}
}
fn find_comma(s: &str) -> Option<usize> {
s.find(',')
}
fn parse_single(s: &str) -> Result<Value, Error> {
if let Some(result) = try_labeled_dms(s) {
return result;
}
if let Some(result) = try_signed_dms(s) {
return result.map(Value::Unknown);
}
if let Some(result) = try_bare_dms(s) {
return result.map(Value::Unknown);
}
try_decimal(s).map(Value::Unknown)
}
fn parse_pair(s: &str, comma_pos: usize) -> Result<Parsed, Error> {
let lat_src = &s[..comma_pos];
let after_comma = &s[comma_pos + 1..];
let has_pre_ws = lat_src.ends_with(|c: char| c.is_ascii_whitespace());
let has_post_ws = after_comma.starts_with(|c: char| c.is_ascii_whitespace());
let comma_ws = has_pre_ws || has_post_ws;
let lat_src = lat_src.trim_end();
let lon_src = after_comma.trim_start();
if lon_src.contains(',') {
return Err(Error::InvalidCharacter(',', s.to_string()));
}
let lat_is_bare = is_bare_dms(lat_src);
let lon_is_bare = is_bare_dms(lon_src);
if lat_is_bare && lon_is_bare && comma_ws {
return Err(Error::InvalidWhitespace(s.to_string()));
}
let lat: Latitude = parse_as_latitude(lat_src)?;
let lon: Longitude = parse_as_longitude(lon_src)?;
Ok(Parsed::Coordinate(Coordinate::new(lat, lon)))
}
fn parse_as_latitude(s: &str) -> Result<Latitude, Error> {
if let Some(result) = try_labeled_dms(s) {
return match result? {
Value::Latitude(lat) => Ok(lat),
Value::Longitude(_) => {
let dir_char = s.chars().last().unwrap_or('?');
Err(Error::InvalidCharacter(dir_char, s.to_string()))
}
Value::Unknown(_) => unreachable!(),
};
}
let f = parse_as_float(s)?;
Latitude::try_from(f).map_err(|_| {
let deg = inner::to_degrees_minutes_seconds(f).0;
Error::InvalidLatitudeDegrees(deg)
})
}
fn parse_as_longitude(s: &str) -> Result<Longitude, Error> {
if let Some(result) = try_labeled_dms(s) {
return match result? {
Value::Longitude(lon) => Ok(lon),
Value::Latitude(_) => {
let dir_char = s.chars().last().unwrap_or('?');
Err(Error::InvalidCharacter(dir_char, s.to_string()))
}
Value::Unknown(_) => unreachable!(),
};
}
let f = parse_as_float(s)?;
Longitude::try_from(f).map_err(|_| {
let deg = inner::to_degrees_minutes_seconds(f).0;
Error::InvalidLongitudeDegrees(deg)
})
}
fn parse_as_float(s: &str) -> Result<OrderedFloat<f64>, Error> {
if let Some(result) = try_signed_dms(s) {
return result;
}
if let Some(result) = try_bare_dms(s) {
return result;
}
try_decimal(s)
}
fn is_bare_dms(s: &str) -> bool {
matches!(s.as_bytes().first(), Some(b'+') | Some(b'-')) && s.contains(':')
}
fn try_labeled_dms(s: &str) -> Option<Result<Value, Error>> {
if !s.contains('°') {
return None;
}
if matches!(s.as_bytes().first(), Some(b'+') | Some(b'-')) {
return None;
}
let (deg_str, rest) = consume_up_to(s, '°')?;
if deg_str.ends_with(|c: char| c.is_ascii_whitespace()) {
return Some(Err(Error::InvalidWhitespace(s.to_string())));
}
let rest = skip_whitespace(rest);
let (min_str, rest) = consume_up_to(rest, '′')?;
if min_str.ends_with(|c: char| c.is_ascii_whitespace()) {
return Some(Err(Error::InvalidWhitespace(s.to_string())));
}
let rest = skip_whitespace(rest);
let (sec_str, rest) = consume_up_to(rest, '″')?;
if sec_str.ends_with(|c: char| c.is_ascii_whitespace()) {
return Some(Err(Error::InvalidWhitespace(s.to_string())));
}
let rest = rest.trim();
if rest.is_empty() {
return None;
}
let direction = match rest {
"N" | "S" | "E" | "W" => rest,
other => {
let bad = other.chars().next().unwrap_or('?');
return Some(Err(Error::InvalidCharacter(bad, s.to_string())));
}
};
let degrees = match parse_degrees(deg_str, 1, 3, false) {
Some(d) => d,
None => {
return Some(Err(Error::InvalidNumericFormat(deg_str.to_string())));
}
};
let minutes = match parse_minutes(min_str) {
Some(m) => m,
None => {
return Some(Err(Error::InvalidNumericFormat(min_str.to_string())));
}
};
let seconds = match parse_seconds(sec_str) {
Some(t) => t,
None => {
return Some(Err(Error::InvalidNumericFormat(sec_str.to_string())));
}
};
let neg = matches!(direction, "S" | "W");
let signed_degrees = if neg { -degrees } else { degrees };
let float = match inner::from_degrees_minutes_seconds(signed_degrees, minutes, seconds) {
Ok(f) => f,
Err(e) => return Some(Err(e)),
};
match direction {
"N" | "S" => match Latitude::try_from(float) {
Ok(lat) => Some(Ok(Value::Latitude(lat))),
Err(_) => Some(Err(Error::InvalidLatitudeDegrees(
inner::to_degrees_minutes_seconds(float).0,
))),
},
"E" | "W" => match Longitude::try_from(float) {
Ok(lon) => Some(Ok(Value::Longitude(lon))),
Err(_) => Some(Err(Error::InvalidLongitudeDegrees(
inner::to_degrees_minutes_seconds(float).0,
))),
},
_ => unreachable!(),
}
}
fn try_signed_dms(s: &str) -> Option<Result<OrderedFloat<f64>, Error>> {
if !s.contains('°') {
return None;
}
let (neg, s_inner) = consume_sign(s);
if neg && s_inner.starts_with(|c: char| c.is_ascii_whitespace()) {
return Some(Err(Error::InvalidWhitespace(s.to_string())));
}
let (deg_str, rest) = consume_up_to(s_inner, '°')?;
if deg_str.ends_with(|c: char| c.is_ascii_whitespace()) {
return Some(Err(Error::InvalidWhitespace(s.to_string())));
}
let rest = skip_whitespace(rest);
let (min_str, rest) = consume_up_to(rest, '′')?;
if min_str.ends_with(|c: char| c.is_ascii_whitespace()) {
return Some(Err(Error::InvalidWhitespace(s.to_string())));
}
let rest = skip_whitespace(rest);
let (sec_str, rest) = consume_up_to(rest, '″')?;
if sec_str.ends_with(|c: char| c.is_ascii_whitespace()) {
return Some(Err(Error::InvalidWhitespace(s.to_string())));
}
if !rest.trim().is_empty() {
return None;
}
let degrees = match parse_degrees(deg_str, 1, 3, neg) {
Some(d) => d,
None => return Some(Err(Error::InvalidNumericFormat(deg_str.to_string()))),
};
let minutes = match parse_minutes(min_str) {
Some(m) => m,
None => return Some(Err(Error::InvalidNumericFormat(min_str.to_string()))),
};
let seconds = match parse_seconds(sec_str) {
Some(t) => t,
None => return Some(Err(Error::InvalidNumericFormat(sec_str.to_string()))),
};
Some(inner::from_degrees_minutes_seconds(
degrees, minutes, seconds,
))
}
fn try_bare_dms(s: &str) -> Option<Result<OrderedFloat<f64>, Error>> {
let neg = match s.as_bytes().first()? {
b'+' => false,
b'-' => true,
_ => return None,
};
let s_inner = &s[1..];
if !s_inner.contains(':') {
return None;
}
let (deg_str, rest) = consume_up_to(s_inner, ':')?;
let (min_str, sec_str) = consume_up_to(rest, ':')?;
if deg_str.len() != 3 || min_str.len() != 2 {
return Some(Err(Error::InvalidNumericFormat(s.to_string())));
}
let dot_pos = sec_str.find('.')?;
if dot_pos != 2 || sec_str.len() < dot_pos + 1 + 4 {
return Some(Err(Error::InvalidNumericFormat(s.to_string())));
}
let degrees = match parse_degrees(deg_str, 3, 3, neg) {
Some(d) => d,
None => return Some(Err(Error::InvalidNumericFormat(s.to_string()))),
};
let minutes = match parse_minutes(min_str) {
Some(m) => m,
None => return Some(Err(Error::InvalidNumericFormat(s.to_string()))),
};
let seconds = match parse_seconds(sec_str) {
Some(t) => t,
None => return Some(Err(Error::InvalidNumericFormat(s.to_string()))),
};
Some(inner::from_degrees_minutes_seconds(
degrees, minutes, seconds,
))
}
fn try_decimal(s: &str) -> Result<OrderedFloat<f64>, Error> {
let (neg, rest) = consume_sign(s);
if neg && rest.starts_with(|c: char| c.is_ascii_whitespace()) {
return Err(Error::InvalidWhitespace(s.to_string()));
}
let dot = rest
.find('.')
.ok_or_else(|| Error::InvalidNumericFormat(s.to_string()))?;
let int_part = &rest[..dot];
let frac_part = &rest[dot + 1..];
if int_part.is_empty() || int_part.len() > 3 || !int_part.bytes().all(|b| b.is_ascii_digit()) {
return Err(Error::InvalidNumericFormat(s.to_string()));
}
if frac_part.is_empty() || !frac_part.bytes().all(|b| b.is_ascii_digit()) {
return Err(Error::InvalidNumericFormat(s.to_string()));
}
let int_val = parse_u32_digits(int_part.as_bytes())
.ok_or_else(|| Error::InvalidNumericFormat(s.to_string()))?;
let frac_val = parse_fraction(frac_part.as_bytes())
.ok_or_else(|| Error::InvalidNumericFormat(s.to_string()))?;
let magnitude = int_val as f64 + frac_val;
let signed = if neg { -magnitude } else { magnitude };
if signed.is_infinite() || signed.is_nan() {
Err(Error::InvalidNumericValue(signed))
} else {
Ok(OrderedFloat(signed))
}
}
fn consume_sign(s: &str) -> (bool, &str) {
match s.as_bytes().first() {
Some(b'+') => (false, &s[1..]),
Some(b'-') => (true, &s[1..]),
_ => (false, s),
}
}
fn consume_up_to(s: &str, delim: char) -> Option<(&str, &str)> {
let pos = s.find(delim)?;
Some((&s[..pos], &s[pos + delim.len_utf8()..]))
}
fn skip_whitespace(s: &str) -> &str {
s.trim_start_matches(|c: char| c.is_ascii_whitespace())
}
fn parse_degrees(s: &str, min_len: usize, max_len: usize, neg: bool) -> Option<i32> {
if s.len() < min_len || s.len() > max_len || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let v = parse_u32_digits(s.as_bytes())? as i32;
Some(if neg { -v } else { v })
}
fn parse_minutes(s: &str) -> Option<u32> {
if s.is_empty() || s.len() > 2 || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
parse_u32_digits(s.as_bytes())
}
fn parse_seconds(s: &str) -> Option<f32> {
let dot = s.find('.')?;
let int_part = &s[..dot];
let frac_part = &s[dot + 1..];
if int_part.is_empty()
|| int_part.len() > 2
|| frac_part.is_empty()
|| !int_part.bytes().all(|b| b.is_ascii_digit())
|| !frac_part.bytes().all(|b| b.is_ascii_digit())
{
return None;
}
let int_val = parse_u32_digits(int_part.as_bytes())?;
let frac_val = parse_fraction(frac_part.as_bytes())?;
Some((int_val as f64 + frac_val) as f32)
}
fn parse_u32_digits(bytes: &[u8]) -> Option<u32> {
let mut acc: u32 = 0;
for &b in bytes {
if !b.is_ascii_digit() {
return None;
}
acc = acc.checked_mul(10)?.checked_add((b - b'0') as u32)?;
}
Some(acc)
}
fn parse_fraction(bytes: &[u8]) -> Option<f64> {
let mut acc: f64 = 0.0;
let mut place: f64 = 0.1;
for &b in bytes {
if !b.is_ascii_digit() {
return None;
}
acc += (b - b'0') as f64 * place;
place *= 0.1;
}
Some(acc)
}