mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 02:46:51 -06:00
Found while testing crates/engine/src/units.rs:176-179 (parse_mixed) Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
511 lines
19 KiB
Rust
511 lines
19 KiB
Rust
//! Linear and angular unit formatting and parsing (LUNITS/LUPREC, AUNITS/AUPREC).
|
|
|
|
use std::f64::consts::PI;
|
|
|
|
/// Largest scaled value (below `i64::MAX`) that the integer feet/inch splitting handles; larger
|
|
/// distances fall back to decimal.
|
|
const I64_SAFE_MAX: f64 = 9.0e18;
|
|
|
|
/// Format a distance per LUNITS (1 scientific, 2 decimal, 3 engineering, 4 architectural,
|
|
/// 5 fractional) and LUPREC.
|
|
pub fn format_distance(v: f64, lunits: i64, luprec: i64) -> String {
|
|
if !v.is_finite() {
|
|
return "0".into();
|
|
}
|
|
let prec = luprec.clamp(0, 8) as usize;
|
|
match lunits {
|
|
1 => scientific(v, prec),
|
|
3 => {
|
|
let neg = v < 0.0;
|
|
// Round at the displayed precision before splitting, so 11.999 carries to 1'-0.00".
|
|
let scale = 10f64.powi(prec as i32);
|
|
let rounded = (v.abs() * scale).round() / scale;
|
|
let a = if rounded.is_finite() { rounded } else { v.abs() };
|
|
let ft = (a / 12.0).floor();
|
|
let inch = a - ft * 12.0;
|
|
format!("{}{}'-{:.*}\"", if neg { "-" } else { "" }, ft as i64, prec, inch)
|
|
}
|
|
4 => {
|
|
let neg = v < 0.0;
|
|
let a = v.abs();
|
|
let denom = 1i64 << prec.min(8);
|
|
let scaled = (a * denom as f64).round();
|
|
if scaled >= I64_SAFE_MAX {
|
|
return format!("{:.*}", prec, v);
|
|
}
|
|
let total = scaled as i64;
|
|
let ft = total / (12 * denom);
|
|
let rem = total - ft * 12 * denom;
|
|
let inch = rem / denom;
|
|
let frac = rem % denom;
|
|
let fs = if frac == 0 {
|
|
String::new()
|
|
} else {
|
|
let g = gcd(frac, denom);
|
|
format!(" {}/{}", frac / g, denom / g)
|
|
};
|
|
format!("{}{}'-{}{}\"", if neg { "-" } else { "" }, ft, inch, fs)
|
|
}
|
|
5 => {
|
|
let neg = v < 0.0;
|
|
let a = v.abs();
|
|
let denom = 1i64 << prec.min(8);
|
|
let scaled = (a * denom as f64).round();
|
|
if scaled >= I64_SAFE_MAX {
|
|
return format!("{:.*}", prec, v);
|
|
}
|
|
let total = scaled as i64;
|
|
let whole = total / denom;
|
|
let frac = total % denom;
|
|
let fs = if frac == 0 {
|
|
String::new()
|
|
} else {
|
|
let g = gcd(frac, denom);
|
|
format!(" {}/{}", frac / g, denom / g)
|
|
};
|
|
format!("{}{}{}", if neg { "-" } else { "" }, whole, fs)
|
|
}
|
|
_ => format!("{:.*}", prec, v),
|
|
}
|
|
}
|
|
|
|
/// Scientific notation with a signed exponent of at least two digits (`1.7500E+01`, `1.00E-05`).
|
|
fn scientific(v: f64, prec: usize) -> String {
|
|
let s = format!("{:.*E}", prec, v);
|
|
match s.split_once('E') {
|
|
Some((mantissa, exp)) => {
|
|
let (sign, digits) = match exp.strip_prefix('-') {
|
|
Some(d) => ('-', d),
|
|
None => ('+', exp),
|
|
};
|
|
format!("{mantissa}E{sign}{digits:0>2}")
|
|
}
|
|
None => s,
|
|
}
|
|
}
|
|
|
|
fn gcd(a: i64, b: i64) -> i64 {
|
|
if b == 0 { a.abs().max(1) } else { gcd(b, a % b) }
|
|
}
|
|
|
|
/// Degrees (`[0, 360]`) as degrees/minutes/seconds, shown to the AUPREC precision (`12d34'57"`).
|
|
fn format_dms(deg: f64, prec: usize) -> String {
|
|
// Round at the displayed precision before splitting, carrying through a full turn.
|
|
let scale = 10i64.pow(prec.saturating_sub(4) as u32);
|
|
let seconds_per_unit = match prec {
|
|
0 => 3600,
|
|
1 | 2 => 60,
|
|
_ => 1,
|
|
};
|
|
let units = (deg * (3600 / seconds_per_unit * scale) as f64).round() as i64;
|
|
let total = units * seconds_per_unit % (360 * 3600 * scale);
|
|
let (d, m) = (total / (3600 * scale), (total / (60 * scale)) % 60);
|
|
match prec {
|
|
0 => format!("{d}d"),
|
|
1 | 2 => format!("{d}d{m}'"),
|
|
_ => {
|
|
let s = (total % (60 * scale)) as f64 / scale as f64;
|
|
format!("{d}d{m}'{:.*}\"", prec.saturating_sub(4), s)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Format an angle (radians) per AUNITS (0 decimal degrees, 1 deg/min/sec, 2 grads, 3 radians,
|
|
/// 4 surveyor's) and AUPREC.
|
|
pub fn format_angle(a: f64, aunits: i64, auprec: i64) -> String {
|
|
let prec = auprec.clamp(0, 8) as usize;
|
|
let deg = cadcraft_geom::norm_angle(a).to_degrees();
|
|
match aunits {
|
|
1 => format_dms(deg, prec),
|
|
2 => format!("{:.*}g", prec, wrap_full_turn(deg / 0.9, 400.0, prec)),
|
|
3 => format!("{:.*}r", prec, wrap_full_turn(cadcraft_geom::norm_angle(a), 2.0 * PI, prec)),
|
|
4 => {
|
|
// N/S <angle from N/S toward E/W> E/W
|
|
let (ns, ew, ang) = if deg <= 90.0 {
|
|
("N", "E", 90.0 - deg)
|
|
} else if deg <= 180.0 {
|
|
("N", "W", deg - 90.0)
|
|
} else if deg <= 270.0 {
|
|
("S", "W", 270.0 - deg)
|
|
} else {
|
|
("S", "E", deg - 270.0)
|
|
};
|
|
format!("{ns} {} {ew}", format_dms(ang, prec))
|
|
}
|
|
_ => format!("{:.*}", prec, wrap_full_turn(deg, 360.0, prec)),
|
|
}
|
|
}
|
|
|
|
/// `v` (in `[0, full)`), or 0 when it rounds at `prec` decimals to the same value as a full turn.
|
|
fn wrap_full_turn(v: f64, full: f64, prec: usize) -> f64 {
|
|
if format!("{v:.prec$}") == format!("{full:.prec$}") { 0.0 } else { v }
|
|
}
|
|
|
|
/// Parse a distance typed by the user: decimal, `1'6"`, `1'-6 1/2"`, `6 1/2`, `3/4`, `1e3`.
|
|
pub fn parse_distance(s: &str) -> Option<f64> {
|
|
let t = s.trim();
|
|
if t.is_empty() {
|
|
return None;
|
|
}
|
|
if let Ok(v) = t.parse::<f64>() {
|
|
return v.is_finite().then_some(v);
|
|
}
|
|
let (neg, t) = match t.strip_prefix('-') {
|
|
Some(r) => (true, r.trim()),
|
|
None => (false, t),
|
|
};
|
|
if neg && t.starts_with(['-', '+']) {
|
|
return None;
|
|
}
|
|
let mut total = 0.0;
|
|
let mut rest = t;
|
|
let mut parsed = false;
|
|
if let Some(i) = rest.find('\'') {
|
|
let ft: f64 = rest.get(..i)?.trim().parse().ok()?;
|
|
total += ft * 12.0;
|
|
parsed = true;
|
|
rest = rest.get(i + 1..)?.trim_start_matches('-').trim();
|
|
}
|
|
let rest = rest.trim_end_matches('"').trim();
|
|
if !rest.is_empty() {
|
|
total += parse_mixed(rest)?;
|
|
parsed = true;
|
|
}
|
|
if !parsed {
|
|
return None;
|
|
}
|
|
let v = if neg { -total } else { total };
|
|
v.is_finite().then_some(v)
|
|
}
|
|
|
|
fn parse_mixed(s: &str) -> Option<f64> {
|
|
let parts: Vec<&str> = s.split_whitespace().collect();
|
|
let frac = |p: &str| -> Option<f64> {
|
|
if let Some((a, b)) = p.split_once('/') {
|
|
let d: f64 = b.trim().parse().ok()?;
|
|
if d == 0.0 {
|
|
return None;
|
|
}
|
|
Some(a.trim().parse::<f64>().ok()? / d)
|
|
} else {
|
|
p.parse().ok()
|
|
}
|
|
};
|
|
match parts.as_slice() {
|
|
[a] => frac(a),
|
|
[a, b] => {
|
|
let (n, d) = b.split_once('/')?;
|
|
if n.trim().starts_with(['-', '+']) || d.trim().starts_with(['-', '+']) {
|
|
return None;
|
|
}
|
|
Some(a.parse::<f64>().ok()? + frac(b)?)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// How typed angles are read: the drawing's angle settings (UNITS). Bare numbers are in AUNITS,
|
|
/// and directions are measured from ANGBASE, turning as ANGDIR says.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
pub struct AngleSettings {
|
|
/// AUNITS: 0 decimal degrees, 1 degrees/minutes/seconds, 2 grads, 3 radians, 4 surveyor's.
|
|
pub aunits: i64,
|
|
/// ANGBASE: the direction of angle 0, in radians counterclockwise from +X (`$ANGBASE` in DXF is in degrees).
|
|
pub angbase: f64,
|
|
/// ANGDIR 1: positive angles turn clockwise.
|
|
pub clockwise: bool,
|
|
}
|
|
|
|
impl AngleSettings {
|
|
pub fn from_header(h: &cadcraft_doc::Header) -> Self {
|
|
AngleSettings { aunits: h.i64("AUNITS", 0), angbase: h.f64("ANGBASE", 0.0), clockwise: h.i64("ANGDIR", 0) != 0 }
|
|
}
|
|
|
|
/// A typed angle size in radians (included angles, sweeps, angles from an axis): a bare
|
|
/// number in AUNITS, or with an explicit unit: `30d`, `45d30'15"`, `0.5r`, `100g`.
|
|
pub fn amount(&self, s: &str) -> Option<f64> {
|
|
parse_units(s, self.aunits).map(|(a, _)| a)
|
|
}
|
|
|
|
/// A typed rotation in radians counterclockwise: the amount, turned clockwise when ANGDIR is 1.
|
|
pub fn rotation(&self, s: &str) -> Option<f64> {
|
|
self.amount(s).map(|a| if self.clockwise { -a } else { a })
|
|
}
|
|
|
|
/// A typed direction in radians counterclockwise from +X: the amount measured from ANGBASE in
|
|
/// the ANGDIR sense. Surveyor's bearings (`N45dE`) are absolute. The overrides measure from
|
|
/// +X counterclockwise whatever ANGBASE and ANGDIR say: `<<a` in degrees, `<<<a` in AUNITS.
|
|
pub fn direction(&self, s: &str) -> Option<f64> {
|
|
let t = s.trim();
|
|
if let Some(r) = t.strip_prefix("<<<") {
|
|
return parse_units(r, self.aunits).map(|(a, _)| a);
|
|
}
|
|
if let Some(r) = t.strip_prefix("<<") {
|
|
return parse_units(r, 0).map(|(a, _)| a);
|
|
}
|
|
let (a, absolute) = parse_units(t, self.aunits)?;
|
|
if absolute {
|
|
return Some(a);
|
|
}
|
|
let v = self.angbase + if self.clockwise { -a } else { a };
|
|
v.is_finite().then_some(v)
|
|
}
|
|
|
|
/// Direction `dir` (radians counterclockwise from +X) as the drawing shows directions, in
|
|
/// radians: from ANGBASE in the ANGDIR sense, 0 up to a full turn. (A block turned 2.23°
|
|
/// shows as 87.77 in a drawing measured clockwise from north, as in AutoCAD's Properties.)
|
|
pub fn shown(&self, dir: f64) -> f64 {
|
|
let a = if self.clockwise { self.angbase - dir } else { dir - self.angbase };
|
|
a.rem_euclid(std::f64::consts::TAU)
|
|
}
|
|
|
|
/// The direction (radians counterclockwise from +X) the drawing shows as `v` (radians): the
|
|
/// inverse of [`AngleSettings::shown`].
|
|
pub fn unshown(&self, v: f64) -> f64 {
|
|
(self.angbase + if self.clockwise { -v } else { v }).rem_euclid(std::f64::consts::TAU)
|
|
}
|
|
}
|
|
|
|
/// Parse a direction typed with the default angle settings (degrees, 0 = +X, counterclockwise):
|
|
/// `45`, `45d30'`, `0.5r`, `100g`, `N45dE`. Typed input reads the drawing's settings instead
|
|
/// (`Session::angle_settings`).
|
|
pub fn parse_angle(s: &str) -> Option<f64> {
|
|
AngleSettings::default().direction(s)
|
|
}
|
|
|
|
/// A typed angle in radians and whether it is absolute (a surveyor's bearing).
|
|
fn parse_units(s: &str, aunits: i64) -> Option<(f64, bool)> {
|
|
let t = s.trim().to_ascii_lowercase();
|
|
if t.is_empty() {
|
|
return None;
|
|
}
|
|
let num = |v: &str| v.trim().parse::<f64>().ok();
|
|
let v = if let Some(r) = t.strip_suffix('r') {
|
|
(num(r)?, false)
|
|
} else if let Some(g) = t.strip_suffix('g') {
|
|
((num(g)? * 0.9).to_radians(), false)
|
|
} else if let Some(b) = bearing(&t) {
|
|
(b, true)
|
|
} else if let Some(v) = num(&t) {
|
|
let a = match aunits {
|
|
2 => (v * 0.9).to_radians(),
|
|
3 => v,
|
|
_ => v.to_radians(),
|
|
};
|
|
(a, false)
|
|
} else {
|
|
(parse_dms(&t)?.to_radians(), false)
|
|
};
|
|
v.0.is_finite().then_some(v)
|
|
}
|
|
|
|
/// A surveyor's bearing (`n45d30'e`, lower case) as radians counterclockwise from +X.
|
|
fn bearing(t: &str) -> Option<f64> {
|
|
let north = t.starts_with('n');
|
|
let rest = t.strip_prefix('n').or_else(|| t.strip_prefix('s'))?;
|
|
let (ang_s, east) = match rest.strip_suffix('e') {
|
|
Some(a) => (a, true),
|
|
None => (rest.strip_suffix('w')?, false),
|
|
};
|
|
let a = parse_dms(ang_s.trim())?;
|
|
if !(0.0..=90.0).contains(&a) {
|
|
return None;
|
|
}
|
|
let deg = match (north, east) {
|
|
(true, true) => 90.0 - a,
|
|
(true, false) => 90.0 + a,
|
|
(false, false) => 270.0 - a,
|
|
(false, true) => 270.0 + a,
|
|
};
|
|
Some(deg.to_radians())
|
|
}
|
|
|
|
/// Degrees, `45d`, `45d30'` or `45d30'15"` (a leading `-` negates the whole angle).
|
|
fn parse_dms(t: &str) -> Option<f64> {
|
|
let (neg, t) = match t.trim().strip_prefix('-') {
|
|
Some(r) => (true, r.trim()),
|
|
None => (false, t.trim()),
|
|
};
|
|
let (d, rest) = t.split_once('d').unwrap_or((t, ""));
|
|
let mut deg: f64 = d.trim().parse().ok()?;
|
|
let rest = rest.trim();
|
|
if !rest.is_empty() {
|
|
let (m, rest2) = rest.split_once('\'').unwrap_or((rest, ""));
|
|
if !m.trim().is_empty() {
|
|
deg += m.trim().parse::<f64>().ok()? / 60.0;
|
|
}
|
|
let s = rest2.trim().trim_end_matches('"');
|
|
if !s.is_empty() {
|
|
deg += s.parse::<f64>().ok()? / 3600.0;
|
|
}
|
|
}
|
|
let deg = if neg { -deg } else { deg };
|
|
deg.is_finite().then_some(deg)
|
|
}
|
|
|
|
impl crate::Session {
|
|
/// How typed angles are read in the current drawing (the defaults without one).
|
|
pub fn angle_settings(&self) -> AngleSettings {
|
|
self.doc().map(|d| AngleSettings::from_header(&d.header)).unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
pub const DEG: f64 = PI / 180.0;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn mixed_distance_needs_a_positive_fraction_after_the_whole_number() {
|
|
assert_eq!(parse_distance("6 1/2"), Some(6.5));
|
|
assert_eq!(parse_distance("5 5"), None);
|
|
assert_eq!(parse_distance("1 -1/2"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn huge_architectural_and_fractional_distances_do_not_saturate() {
|
|
for lunits in [4, 5] {
|
|
for v in [1e30, -1e30] {
|
|
assert_eq!(format_distance(v, lunits, 4), format!("{:.4}", v), "lunits {lunits}");
|
|
}
|
|
}
|
|
assert_eq!(format_distance(1e15, 5, 4), "1000000000000000");
|
|
assert_eq!(format_distance(12.5, 4, 4), "1'-0 1/2\"");
|
|
}
|
|
|
|
#[test]
|
|
fn directions_show_from_angbase_in_the_angdir_sense() {
|
|
let north_clockwise = AngleSettings { aunits: 0, angbase: 90f64.to_radians(), clockwise: true };
|
|
// AutoCAD shows a block turned 2.22729505553431° as 87.77 in such a drawing.
|
|
let shown = north_clockwise.shown(2.22729505553431f64.to_radians()).to_degrees();
|
|
assert!((shown - 87.77270494446569).abs() < 1e-9, "{shown}");
|
|
let back = north_clockwise.unshown(87.77270494446569f64.to_radians()).to_degrees();
|
|
assert!((back - 2.22729505553431).abs() < 1e-9, "{back}");
|
|
// The defaults show directions as they are.
|
|
let plain = AngleSettings::default();
|
|
assert!((plain.shown(0.5) - 0.5).abs() < 1e-12 && (plain.unshown(0.5) - 0.5).abs() < 1e-12);
|
|
// Counterclockwise from a base of 30°: 45° shows as 15.
|
|
let based = AngleSettings { aunits: 0, angbase: 30f64.to_radians(), clockwise: false };
|
|
assert!((based.shown(45f64.to_radians()).to_degrees() - 15.0).abs() < 1e-9);
|
|
}
|
|
|
|
#[test]
|
|
fn decimal_and_arch() {
|
|
assert_eq!(format_distance(28.98524, 2, 4), "28.9852");
|
|
assert_eq!(format_distance(18.5, 4, 4), "1'-6 1/2\"");
|
|
assert_eq!(format_distance(6.25, 5, 2), "6 1/4");
|
|
assert_eq!(format_distance(30.0, 3, 2), "2'-6.00\"");
|
|
}
|
|
|
|
#[test]
|
|
fn engineering_rounding_carries_to_feet() {
|
|
assert_eq!(format_distance(11.999, 3, 2), "1'-0.00\"");
|
|
assert_eq!(format_distance(23.999, 3, 2), "2'-0.00\"");
|
|
assert_eq!(format_distance(-11.999, 3, 2), "-1'-0.00\"");
|
|
assert_eq!(format_distance(11.5, 3, 2), "0'-11.50\"");
|
|
assert_eq!(format_distance(12.0, 3, 2), "1'-0.00\"");
|
|
assert_eq!(format_distance(11.6, 3, 0), "1'-0\"");
|
|
}
|
|
|
|
#[test]
|
|
fn scientific_exponent_is_signed_and_padded() {
|
|
assert_eq!(format_distance(17.5, 1, 4), "1.7500E+01");
|
|
assert_eq!(format_distance(0.00001, 1, 2), "1.00E-05");
|
|
assert_eq!(format_distance(1234.5, 1, 3), "1.234E+03");
|
|
assert_eq!(format_distance(12345678.0, 1, 3), "1.235E+07");
|
|
assert_eq!(format_distance(-17.5, 1, 4), "-1.7500E+01");
|
|
assert_eq!(format_distance(0.0, 1, 2), "0.00E+00");
|
|
assert_eq!(format_distance(1e100, 1, 1), "1.0E+100");
|
|
}
|
|
|
|
#[test]
|
|
fn dms_display_precision() {
|
|
let angle = (12.0 + 34.0 / 60.0 + 56.78912 / 3600.0_f64).to_radians();
|
|
let expected = ["13d", "12d35'", "12d35'", "12d34'57\"", "12d34'57\"", "12d34'56.8\"", "12d34'56.79\"", "12d34'56.789\"", "12d34'56.7891\""];
|
|
for (prec, expected) in expected.into_iter().enumerate() {
|
|
assert_eq!(format_angle(angle, 1, prec as i64), expected, "AUPREC {prec}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dms_rounding_carries() {
|
|
let dms = |d: f64, m: f64, s: f64| (d + m / 60.0 + s / 3600.0).to_radians();
|
|
let negative = (-0.0001_f64).to_radians();
|
|
let cases = [
|
|
(dms(29.0, 59.0, 59.6), 0, "30d"),
|
|
(dms(29.0, 59.0, 59.6), 2, "30d0'"),
|
|
(dms(29.0, 59.0, 59.6), 4, "30d0'0\""),
|
|
(dms(29.0, 0.0, 59.99996), 2, "29d1'"),
|
|
(dms(29.0, 0.0, 59.99996), 4, "29d1'0\""),
|
|
(dms(29.0, 59.0, 59.99996), 5, "30d0'0.0\""),
|
|
(dms(29.0, 59.0, 59.99996), 8, "30d0'0.0000\""),
|
|
(negative, 0, "0d"),
|
|
(negative, 4, "0d0'0\""),
|
|
(negative, 5, "359d59'59.6\""),
|
|
];
|
|
for (angle, prec, expected) in cases {
|
|
assert_eq!(format_angle(angle, 1, prec), expected, "AUPREC {prec}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rounding_to_a_full_turn_wraps_to_zero() {
|
|
let almost_turn = 359.9999_f64.to_radians();
|
|
assert_eq!(format_angle(almost_turn, 0, 2), "0.00");
|
|
assert_eq!(format_angle(almost_turn, 2, 2), "0.00g");
|
|
assert_eq!(format_angle(2.0 * PI - 1e-9, 3, 2), "0.00r");
|
|
assert_eq!(format_angle(359.99_f64.to_radians(), 0, 1), "0.0");
|
|
assert_eq!(format_angle(359.9_f64.to_radians(), 0, 1), "359.9");
|
|
assert_eq!(format_angle(1.0, 3, 2), "1.00r");
|
|
assert_eq!(format_angle(6.25, 3, 1), "6.2r");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_distances() {
|
|
assert_eq!(parse_distance("12.5"), Some(12.5));
|
|
assert_eq!(parse_distance("1'6\""), Some(18.0));
|
|
assert_eq!(parse_distance("1'-6 1/2\""), Some(18.5));
|
|
assert_eq!(parse_distance("3/4"), Some(0.75));
|
|
assert_eq!(parse_distance("1/0"), None);
|
|
assert_eq!(parse_distance("abc"), None);
|
|
assert_eq!(parse_distance(""), None);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_distance_rejects_doubled_minus() {
|
|
assert_eq!(parse_distance("--5"), None);
|
|
assert_eq!(parse_distance("- -3/4"), None);
|
|
assert_eq!(parse_distance("-5"), Some(-5.0));
|
|
assert_eq!(parse_distance("-3/4"), Some(-0.75));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_distance_needs_a_number() {
|
|
assert_eq!(parse_distance("-"), None);
|
|
assert_eq!(parse_distance("\""), None);
|
|
assert_eq!(parse_distance("-\""), None);
|
|
assert_eq!(parse_distance("1'"), Some(12.0));
|
|
}
|
|
|
|
#[test]
|
|
fn angles() {
|
|
assert!((parse_angle("90").unwrap() - PI / 2.0).abs() < 1e-12);
|
|
assert!((parse_angle("45d30'").unwrap().to_degrees() - 45.5).abs() < 1e-9);
|
|
assert!((parse_angle("N45dE").unwrap().to_degrees() - 45.0).abs() < 1e-9);
|
|
assert!((parse_angle("1.5r").unwrap() - 1.5).abs() < 1e-12);
|
|
assert_eq!(format_angle(PI / 2.0, 0, 0), "90");
|
|
assert_eq!(format_angle(PI / 4.0, 4, 0), "N 45d E");
|
|
}
|
|
|
|
#[test]
|
|
fn surveyor_display_is_degrees_minutes_seconds() {
|
|
// 73.6975 degrees south of east: 73d41'51" (0.6975 deg = 41.85' = 41'51")
|
|
let angle = (270.0 + 73.6975_f64).to_radians();
|
|
assert_eq!(format_angle(angle, 4, 4), "S 73d41'51\" E");
|
|
assert_eq!(format_angle(angle, 4, 2), "S 73d42' E");
|
|
assert_eq!(format_angle(angle, 4, 0), "S 74d E");
|
|
assert_eq!(format_angle(angle, 4, 6), "S 73d41'51.00\" E");
|
|
}
|
|
}
|