forked from 1b-agent/opencadstudio-upstream
fix(units): carry a rounded-up angle the same way
dms() rounded the seconds after splitting the degrees and minutes off, so seconds that rounded up had nowhere to go: 45.9999 deg at AUPREC 0 -> 45d59'60" 89.99999 deg -> 89d59'60" Round once, into the smallest unit shown, and split degrees and minutes out of the rounded total, as format_length now does. surveyor() then needed the same treatment one level up: it chose its pole and quadrant from the unrounded direction, so a bearing that rounded to the quarter turn came out as the contradictory `S 90d0'0" E` the function's own doc comment sets out to avoid. Snap the direction to the shown resolution before deciding, so 359.99999 deg at AUPREC 0 is `E` rather than a bearing of ninety degrees.
This commit is contained in:
1 parent
5c81d33027
commit
2a26c8cad1
1 file changed
+85
-5
+85
-5
@@ -178,10 +178,17 @@ pub fn format_angle(value_rad: f64) -> String {
|
||||
fn dms(degrees: f64, prec: usize) -> String {
|
||||
let sign = if degrees < 0.0 { "-" } else { "" };
|
||||
let a = degrees.abs();
|
||||
let d = a.floor();
|
||||
let m_full = (a - d) * 60.0;
|
||||
let m = m_full.floor();
|
||||
let s = (m_full - m) * 60.0;
|
||||
// Round once, into the smallest unit that will be shown, so seconds that
|
||||
// round up carry into the minutes and degrees. Rounding the seconds after
|
||||
// the split wrote the impossible 45d59'60".
|
||||
let scale = 10f64.powi(prec.min(15) as i32);
|
||||
let per_minute = 60.0 * scale;
|
||||
let per_degree = 60.0 * per_minute;
|
||||
let ticks = (a * per_degree).round();
|
||||
let d = (ticks / per_degree).trunc();
|
||||
let rest = ticks - d * per_degree;
|
||||
let m = (rest / per_minute).trunc();
|
||||
let s = (rest - m * per_minute) / scale;
|
||||
format!("{}{:.0}d{:.0}'{:.*}\"", sign, d, m, prec, s)
|
||||
}
|
||||
|
||||
@@ -192,7 +199,12 @@ fn dms(degrees: f64, prec: usize) -> String {
|
||||
/// the single letter, which is also what keeps a 90° angle from reading as the
|
||||
/// contradictory `N 90d0'0" E`.
|
||||
fn surveyor(value_rad: f64, prec: usize) -> String {
|
||||
let deg = value_rad.to_degrees().rem_euclid(360.0);
|
||||
// Snap to the resolution that will be shown before deciding anything, so
|
||||
// a direction that rounds onto a cardinal is written as one -- and so the
|
||||
// bearing handed to `dms` can no longer round up to the quarter turn that
|
||||
// would read as the contradictory `N 90d0'0\" E`.
|
||||
let scale = 3600.0 * 10f64.powi(prec.min(15) as i32);
|
||||
let deg = ((value_rad.to_degrees().rem_euclid(360.0) * scale).round() / scale).rem_euclid(360.0);
|
||||
let near = |target: f64| (deg - target).abs() < 1e-9;
|
||||
if near(0.0) {
|
||||
return "E".into();
|
||||
@@ -866,3 +878,71 @@ mod length_format_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod angle_format_tests {
|
||||
use super::*;
|
||||
|
||||
fn shown(aunits: i16, auprec: i16, degrees: f64) -> String {
|
||||
let mut ctx = unit_context();
|
||||
ctx.aunits = aunits;
|
||||
ctx.auprec = auprec;
|
||||
set_unit_context(ctx);
|
||||
format_angle(degrees.to_radians())
|
||||
}
|
||||
|
||||
/// Seconds that round up are a minute, and sixty minutes are a degree.
|
||||
/// Rounding after the split wrote 45d59'60\", which no drawing produces.
|
||||
#[test]
|
||||
fn degrees_minutes_seconds_carry() {
|
||||
assert_eq!(shown(1, 0, 45.9999), "46d0'0\"");
|
||||
assert_eq!(shown(1, 0, 89.99999), "90d0'0\"");
|
||||
assert_eq!(shown(1, 0, 0.99999), "1d0'0\"");
|
||||
// Values that do not round up are unchanged.
|
||||
assert_eq!(shown(1, 0, 45.5), "45d30'0\"");
|
||||
assert_eq!(shown(1, 2, 45.9999), "45d59'59.64\"");
|
||||
assert_eq!(shown(1, 2, 45.5), "45d30'0.00\"");
|
||||
}
|
||||
|
||||
/// A bearing that rounds onto a cardinal is that cardinal — the quarter
|
||||
/// turn must never be written as `S 90d0'0\" E`.
|
||||
#[test]
|
||||
fn surveyor_bearings_stay_inside_a_quadrant() {
|
||||
assert_eq!(shown(4, 0, 359.99999), "E");
|
||||
assert_eq!(shown(4, 0, 89.99999), "N");
|
||||
assert_eq!(shown(4, 0, 45.0), "N 45d0'0\" E");
|
||||
assert_eq!(shown(4, 0, 180.0), "W");
|
||||
for prec in [0, 2] {
|
||||
for deg in [0.99999f64, 45.9999, 89.99999, 179.99999, 269.99999, 359.99999] {
|
||||
let text = shown(4, prec, deg);
|
||||
assert!(
|
||||
!text.contains("90d"),
|
||||
"prec {prec}: {deg} wrote {text:?}, a quarter turn inside a quadrant"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whatever the readout shows must still read straight back.
|
||||
#[test]
|
||||
fn formatted_angles_read_back() {
|
||||
for aunits in [0, 1, 4] {
|
||||
for prec in [0, 2] {
|
||||
for deg in [0.99999f64, 45.5, 45.9999, 89.99999, 200.25, 359.99999] {
|
||||
let text = shown(aunits, prec, deg);
|
||||
let read = parse_angle(&text).unwrap_or_else(|| {
|
||||
panic!("aunits {aunits} prec {prec} wrote {text:?}, which does not read back")
|
||||
});
|
||||
let delta = (read.to_degrees().rem_euclid(360.0) - deg.rem_euclid(360.0))
|
||||
.rem_euclid(360.0);
|
||||
let delta = delta.min(360.0 - delta);
|
||||
assert!(
|
||||
delta < 1.0,
|
||||
"aunits {aunits} prec {prec}: {deg} wrote {text:?}, read back as {}",
|
||||
read.to_degrees()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user