#[cfg(feature = "no_std")]
use alloc::format;
#[cfg(feature = "no_std")]
use alloc::vec::Vec;
use crate::consts::DEG_TO_RAD;
use crate::error::{ProjError, ProjResult};
const SYM: &[u8] = b"NnEeSsWw";
const VM: [f64; 3] = [
DEG_TO_RAD,
0.000_290_888_208_665_721_6,
0.000_004_848_136_811_095_36,
];
const DEG_SIGN1: u8 = 0xC2;
const DEG_SIGN2: u8 = 0xB0;
pub fn parse_leading_f64(s: &str) -> Option<(f64, usize)> {
let bytes = s.as_bytes();
let mut i = 0usize;
let n = bytes.len();
if i < n && (bytes[i] == b'+' || bytes[i] == b'-') {
i += 1;
}
let mut saw_digit = false;
while i < n && bytes[i].is_ascii_digit() {
i += 1;
saw_digit = true;
}
if i < n && bytes[i] == b'.' {
i += 1;
while i < n && bytes[i].is_ascii_digit() {
i += 1;
saw_digit = true;
}
}
if !saw_digit {
return None;
}
if i < n && (bytes[i] == b'e' || bytes[i] == b'E') {
let mut j = i + 1;
if j < n && (bytes[j] == b'+' || bytes[j] == b'-') {
j += 1;
}
let mut exp_digit = false;
while j < n && bytes[j].is_ascii_digit() {
j += 1;
exp_digit = true;
}
if exp_digit {
i = j;
} }
let slice = s.get(..i)?;
match slice.parse::<f64>() {
Ok(v) => Some((v, i)),
Err(_) => None,
}
}
pub fn dmstor(s: &str) -> ProjResult<f64> {
dmstor_with_rest(s).map(|(v, _)| v)
}
pub fn dmstor_with_rest(s: &str) -> ProjResult<(f64, usize)> {
let raw = s.as_bytes();
let mut lead = 0usize;
while lead < raw.len() && raw[lead].is_ascii_whitespace() {
lead += 1;
}
const MAX_WORK: usize = 64;
let mut work: Vec<u8> = Vec::with_capacity(MAX_WORK);
let mut p = lead;
while p < raw.len() {
let b = raw[p];
let is_graph = b.is_ascii_graphic();
if !(is_graph || b == DEG_SIGN1 || b == DEG_SIGN2) {
break;
}
if work.len() >= MAX_WORK - 1 {
break;
}
work.push(b);
p += 1;
}
let mut cur = 0usize;
let mut neg = false;
if let Some(&first) = work.first() {
if first == b'+' {
cur += 1;
} else if first == b'-' {
neg = true;
cur += 1;
}
}
let mut v = 0.0_f64;
let mut parsed_any = false;
let mut nl = 0i32;
while nl < 3 {
match work.get(cur) {
Some(&b) if b.is_ascii_digit() || b == b'.' => {}
_ => break,
}
let work_slice = work.get(cur..).ok_or(ProjError::IllegalArgValue)?;
let tail = core::str::from_utf8(work_slice).map_err(|_| ProjError::IllegalArgValue)?;
let (tv, consumed) = parse_leading_f64(tail).ok_or(ProjError::IllegalArgValue)?;
cur += consumed;
parsed_any = true;
let mut adv = 1usize;
let n: i32;
let c0 = work.get(cur).copied();
let c1 = work.get(cur + 1).copied();
if c0 == Some(b'D') || c0 == Some(b'd') || c0 == Some(DEG_SIGN2) {
n = 0;
} else if c0 == Some(b'\'') {
n = 1;
} else if c0 == Some(b'"') {
n = 2;
} else if c0 == Some(DEG_SIGN1) && c1 == Some(DEG_SIGN2) {
n = 0;
adv = 2;
} else if c0 == Some(b'r') || c0 == Some(b'R') {
if nl != 0 {
return Err(ProjError::IllegalArgValue);
}
cur += 1;
v = tv;
break;
} else {
let idx = usize::try_from(nl).map_err(|_| ProjError::IllegalArgValue)?;
let mult = VM.get(idx).ok_or(ProjError::IllegalArgValue)?;
v += tv * mult;
break;
}
if n < nl {
return Err(ProjError::IllegalArgValue);
}
let idx = usize::try_from(n).map_err(|_| ProjError::IllegalArgValue)?;
let mult = VM.get(idx).ok_or(ProjError::IllegalArgValue)?;
v += tv * mult;
cur += adv;
nl = n + 1;
}
if !parsed_any {
return Err(ProjError::IllegalArgValue);
}
if let Some(&c) = work.get(cur) {
if let Some(pos) = SYM.iter().position(|&x| x == c) {
neg = pos >= 4;
cur += 1;
}
}
if neg {
v = -v;
}
Ok((v, lead + cur))
}
pub fn decode_sexagesimal_dms(value: f64) -> f64 {
let sign: f64 = if value < 0.0 { -1.0 } else { 1.0 };
let abs = value.abs();
let degrees = abs.floor();
let formatted = format!("{:.12}", abs);
let dot_pos = formatted.find('.').unwrap_or(formatted.len());
let frac_start = dot_pos + 1;
let minutes: f64 = formatted
.get(frac_start..frac_start + 2)
.and_then(|s| s.parse().ok())
.unwrap_or(0.0);
let sec_str = formatted.get(frac_start + 2..).unwrap_or("0");
let sec_raw: f64 = sec_str.parse().unwrap_or(0.0);
let sec_denom: f64 = 10_f64.powi(sec_str.len() as i32 - 2);
let seconds = sec_raw / sec_denom;
sign * (degrees + minutes / 60.0 + seconds / 3600.0)
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-12;
#[test]
fn dms_deg_min() {
let got = dmstor("10d30'").expect("parse");
let expected = 10.5 * DEG_TO_RAD;
assert!(
(got - expected).abs() < TOL,
"got {got} expected {expected}"
);
}
#[test]
fn dms_deg_min_sec_west_negative() {
let got = dmstor("10d30'00\"W").expect("parse");
let expected = -10.5 * DEG_TO_RAD;
assert!(
(got - expected).abs() < TOL,
"got {got} expected {expected}"
);
}
#[test]
fn dms_zero_east() {
let got = dmstor("0dE").expect("parse");
assert_eq!(got, 0.0);
}
#[test]
fn dms_radian_suffix() {
let got = dmstor("45r").expect("parse");
assert!((got - 45.0).abs() < TOL, "got {got}");
}
#[test]
fn dms_deg_min_west_negative() {
let got = dmstor("17d40'W").expect("parse");
let expected = -17.666_666_666_7 * DEG_TO_RAD;
assert!(
(got - expected).abs() < 1e-9,
"got {got} expected {expected}"
);
}
#[test]
fn dms_bare_negative_degrees() {
let got = dmstor("-10.5").expect("parse");
let expected = -10.5 * DEG_TO_RAD;
assert!(
(got - expected).abs() < TOL,
"got {got} expected {expected}"
);
}
#[test]
fn dms_north_south_suffix() {
let got_n = dmstor("90dN").expect("parse");
let got_s = dmstor("90dS").expect("parse");
assert!((got_n - 90.0 * DEG_TO_RAD).abs() < TOL, "got {got_n}");
assert!((got_s + 90.0 * DEG_TO_RAD).abs() < TOL, "got {got_s}");
}
#[test]
fn parse_leading_f64_stops_at_d() {
assert_eq!(parse_leading_f64("12.5d30"), Some((12.5, 4)));
}
#[test]
fn parse_leading_f64_no_number() {
assert_eq!(parse_leading_f64("abc"), None);
}
#[test]
fn parse_leading_f64_basic_forms() {
assert_eq!(parse_leading_f64("0"), Some((0.0, 1)));
assert_eq!(parse_leading_f64("-3.25"), Some((-3.25, 5)));
assert_eq!(parse_leading_f64("+7"), Some((7.0, 2)));
assert_eq!(parse_leading_f64(".5'"), Some((0.5, 2)));
assert_eq!(parse_leading_f64("1e3d"), Some((1000.0, 3)));
assert_eq!(parse_leading_f64("12e"), Some((12.0, 2)));
}
#[test]
fn dms_degree_sign_utf8() {
let got = dmstor("10\u{00b0}30'").expect("parse");
let expected = 10.5 * DEG_TO_RAD;
assert!(
(got - expected).abs() < TOL,
"got {got} expected {expected}"
);
}
#[test]
fn dms_degree_sign_single_byte_latin1() {
let got = dmstor("10d").expect("parse");
let expected = 10.0 * DEG_TO_RAD;
assert!(
(got - expected).abs() < TOL,
"got {got} expected {expected}"
);
}
#[test]
fn dms_with_rest_reports_consumed() {
let (v, used) = dmstor_with_rest(" 10d30' rest").expect("parse");
let expected = 10.5 * DEG_TO_RAD;
assert!((v - expected).abs() < TOL, "got {v} expected {expected}");
assert_eq!(used, 8);
}
#[test]
fn dms_radian_after_segment_is_error() {
assert!(dmstor("10d30'5r").is_err());
}
#[test]
fn dms_out_of_order_units_is_error() {
assert!(dmstor("10'30d").is_err());
}
#[test]
fn dms_empty_is_error() {
assert!(dmstor("").is_err());
assert!(dmstor(" ").is_err());
assert!(dmstor("abc").is_err());
}
#[test]
fn dms_manual_sweep_matches_formula() {
for d in 0..30 {
let mut m = 0;
while m < 60 {
for s in [0.0_f64, 15.5, 45.25] {
let st = format!("{}d{}'{:.3}\"", d, m, s);
let expected = (d as f64 + m as f64 / 60.0 + s / 3600.0) * DEG_TO_RAD;
let got = dmstor(&st).expect("parse");
assert!(
(got - expected).abs() < 1e-9,
"{} -> {} expected {}",
st,
got,
expected
);
}
m += 5;
}
}
}
#[test]
fn decode_sexagesimal_dms_basic() {
let tol = 1e-10;
assert!(
(decode_sexagesimal_dms(46.3) - 46.5).abs() < tol,
"46.3 should decode to 46.5, got {}",
decode_sexagesimal_dms(46.3)
);
let expected_10_20 = 10.0 + 20.0 / 60.0;
assert!(
(decode_sexagesimal_dms(10.2) - expected_10_20).abs() < tol,
"10.2 should decode to {expected_10_20}, got {}",
decode_sexagesimal_dms(10.2)
);
let expected_52 = 52.0 + 9.0 / 60.0 + 22.178 / 3600.0;
assert!(
(decode_sexagesimal_dms(52.0922178) - expected_52).abs() < 1e-9,
"52.0922178 should decode to {expected_52}, got {}",
decode_sexagesimal_dms(52.0922178)
);
let expected_5 = 5.0 + 23.0 / 60.0 + 15.5 / 3600.0;
assert!(
(decode_sexagesimal_dms(5.23155) - expected_5).abs() < 1e-9,
"5.23155 should decode to {expected_5}, got {}",
decode_sexagesimal_dms(5.23155)
);
assert!(
(decode_sexagesimal_dms(3.0) - 3.0).abs() < tol,
"3.0 must decode unchanged"
);
assert!(
(decode_sexagesimal_dms(-71.0) - (-71.0)).abs() < tol,
"-71.0 should decode to -71.0"
);
assert_eq!(decode_sexagesimal_dms(0.0), 0.0);
}
}