fix(units): carry rounded values
Round before splitting display units while preserving large lengths and high-precision cardinal bearings.
This commit is contained in:
commit
9d60c2a2ef
1 changed files with 159 additions and 27 deletions
|
|
@ -104,31 +104,31 @@ pub fn format_length(value: f64) -> String {
|
||||||
match ctx.lunits {
|
match ctx.lunits {
|
||||||
1 => format!("{:.*e}", prec, value),
|
1 => format!("{:.*e}", prec, value),
|
||||||
3 => {
|
3 => {
|
||||||
// Engineering: ft-inches, decimal inches.
|
// Round before splitting so twelve inches carries into feet.
|
||||||
let sign = if value < 0.0 { "-" } else { "" };
|
let sign = if value < 0.0 { "-" } else { "" };
|
||||||
let abs = value.abs();
|
let abs = value.abs();
|
||||||
let feet = (abs / 12.0).trunc();
|
let scale = 10f64.powi(prec.min(15) as i32);
|
||||||
let rem = abs - feet * 12.0;
|
let total = (abs * scale).round();
|
||||||
|
let per_foot = 12.0 * scale;
|
||||||
|
let feet = (total / per_foot).trunc();
|
||||||
|
let rem = (total - feet * per_foot) / scale;
|
||||||
format!("{}{:.0}'-{:.*}\"", sign, feet, prec, rem)
|
format!("{}{:.0}'-{:.*}\"", sign, feet, prec, rem)
|
||||||
}
|
}
|
||||||
4 | 5 => {
|
4 | 5 => {
|
||||||
// Architectural / Fractional — n + fraction with 1/2^p denom (1
|
// Architectural and fractional formats use 1/64-inch resolution.
|
||||||
// unit = 1 inch). Use 6 as a moderate denominator power so the
|
|
||||||
// result reads like 1/64".
|
|
||||||
let sign = if value < 0.0 { "-" } else { "" };
|
let sign = if value < 0.0 { "-" } else { "" };
|
||||||
let abs = value.abs();
|
let abs = value.abs();
|
||||||
let (feet, in_rem) = if ctx.lunits == 4 {
|
let denom = 64.0;
|
||||||
let f = (abs / 12.0).trunc();
|
let ticks = (abs * denom).round();
|
||||||
(Some(f as i64), abs - f * 12.0)
|
let (feet, rest) = if ctx.lunits == 4 {
|
||||||
|
let per_foot = 12.0 * denom;
|
||||||
|
(Some((ticks / per_foot).floor()), ticks.rem_euclid(per_foot))
|
||||||
} else {
|
} else {
|
||||||
(None, abs)
|
(None, ticks)
|
||||||
};
|
};
|
||||||
let whole = in_rem.trunc();
|
let whole = (rest / denom).floor();
|
||||||
let frac = in_rem - whole;
|
let mut n = (rest - whole * denom).round() as u64;
|
||||||
let denom = 64u64;
|
let mut d = denom as u64;
|
||||||
let numer = (frac * denom as f64).round() as i64;
|
|
||||||
let mut n = numer as u64;
|
|
||||||
let mut d = denom;
|
|
||||||
while d > 1 && n % 2 == 0 && d % 2 == 0 {
|
while d > 1 && n % 2 == 0 && d % 2 == 0 {
|
||||||
n /= 2;
|
n /= 2;
|
||||||
d /= 2;
|
d /= 2;
|
||||||
|
|
@ -140,7 +140,7 @@ pub fn format_length(value: f64) -> String {
|
||||||
};
|
};
|
||||||
let unit_suffix = if ctx.lunits == 4 { "\"" } else { "" };
|
let unit_suffix = if ctx.lunits == 4 { "\"" } else { "" };
|
||||||
match feet {
|
match feet {
|
||||||
Some(f) => format!("{}{}'-{:.0}{}{}", sign, f, whole, frac_str, unit_suffix),
|
Some(f) => format!("{}{:.0}'-{:.0}{}{}", sign, f, whole, frac_str, unit_suffix),
|
||||||
None => format!("{}{:.0}{}", sign, whole, frac_str),
|
None => format!("{}{:.0}{}", sign, whole, frac_str),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,10 +169,15 @@ pub fn format_angle(value_rad: f64) -> String {
|
||||||
fn dms(degrees: f64, prec: usize) -> String {
|
fn dms(degrees: f64, prec: usize) -> String {
|
||||||
let sign = if degrees < 0.0 { "-" } else { "" };
|
let sign = if degrees < 0.0 { "-" } else { "" };
|
||||||
let a = degrees.abs();
|
let a = degrees.abs();
|
||||||
let d = a.floor();
|
// Round before splitting so seconds carry into minutes and degrees.
|
||||||
let m_full = (a - d) * 60.0;
|
let scale = 10f64.powi(prec.min(15) as i32);
|
||||||
let m = m_full.floor();
|
let per_minute = 60.0 * scale;
|
||||||
let s = (m_full - m) * 60.0;
|
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)
|
format!("{}{:.0}d{:.0}'{:.*}\"", sign, d, m, prec, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,20 +188,26 @@ fn dms(degrees: f64, prec: usize) -> String {
|
||||||
/// the single letter, which is also what keeps a 90° angle from reading as the
|
/// the single letter, which is also what keeps a 90° angle from reading as the
|
||||||
/// contradictory `N 90d0'0" E`.
|
/// contradictory `N 90d0'0" E`.
|
||||||
fn surveyor(value_rad: f64, prec: usize) -> String {
|
fn surveyor(value_rad: f64, prec: usize) -> String {
|
||||||
let deg = value_rad.to_degrees().rem_euclid(360.0);
|
// Quantize before choosing a cardinal or quadrant.
|
||||||
let near = |target: f64| (deg - target).abs() < 1e-9;
|
let scale = 3600.0 * 10f64.powi(prec.min(15) as i32);
|
||||||
if near(0.0) {
|
let turn = 360.0 * scale;
|
||||||
|
let ticks = (value_rad.to_degrees().rem_euclid(360.0) * scale)
|
||||||
|
.round()
|
||||||
|
.rem_euclid(turn);
|
||||||
|
let at = |degrees: f64| (ticks - degrees * scale).abs() < 0.5;
|
||||||
|
if at(0.0) {
|
||||||
return "E".into();
|
return "E".into();
|
||||||
}
|
}
|
||||||
if near(90.0) {
|
if at(90.0) {
|
||||||
return "N".into();
|
return "N".into();
|
||||||
}
|
}
|
||||||
if near(180.0) {
|
if at(180.0) {
|
||||||
return "W".into();
|
return "W".into();
|
||||||
}
|
}
|
||||||
if near(270.0) {
|
if at(270.0) {
|
||||||
return "S".into();
|
return "S".into();
|
||||||
}
|
}
|
||||||
|
let deg = ticks / scale;
|
||||||
// Measured from the nearer pole, toward the side the angle falls on.
|
// Measured from the nearer pole, toward the side the angle falls on.
|
||||||
let (pole, bearing, side) = if deg < 90.0 {
|
let (pole, bearing, side) = if deg < 90.0 {
|
||||||
("N", 90.0 - deg, "E")
|
("N", 90.0 - deg, "E")
|
||||||
|
|
@ -842,3 +853,124 @@ pub(crate) fn polyline_segment_fill(
|
||||||
Some(boundary)
|
Some(boundary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod length_format_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn with_units(lunits: i16, luprec: i16, value: f64) -> String {
|
||||||
|
let mut ctx = unit_context();
|
||||||
|
ctx.lunits = lunits;
|
||||||
|
ctx.luprec = luprec;
|
||||||
|
set_unit_context(ctx);
|
||||||
|
format_length(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fractional_carries_a_rounded_up_fraction() {
|
||||||
|
assert_eq!(with_units(5, 4, 5.99), "5 63/64");
|
||||||
|
assert_eq!(with_units(5, 4, 5.995), "6");
|
||||||
|
assert_eq!(with_units(5, 4, 0.995), "1");
|
||||||
|
assert_eq!(with_units(5, 4, 11.999), "12");
|
||||||
|
assert_eq!(with_units(5, 4, -5.995), "-6");
|
||||||
|
assert_eq!(with_units(5, 4, 0.5), "0 1/2");
|
||||||
|
assert_eq!(with_units(5, 4, 9.25), "9 1/4");
|
||||||
|
assert_eq!(with_units(5, 4, 3.0e17), "300000000000000000");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn architectural_carries_into_feet() {
|
||||||
|
assert_eq!(with_units(4, 4, 11.99), "0'-11 63/64\"");
|
||||||
|
assert_eq!(with_units(4, 4, 11.999), "1'-0\"");
|
||||||
|
assert_eq!(with_units(4, 4, 23.999), "2'-0\"");
|
||||||
|
assert_eq!(with_units(4, 4, 66.5), "5'-6 1/2\"");
|
||||||
|
assert_eq!(with_units(4, 4, -11.999), "-1'-0\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn engineering_carries_into_feet() {
|
||||||
|
assert_eq!(with_units(3, 1, 11.94), "0'-11.9\"");
|
||||||
|
assert_eq!(with_units(3, 1, 11.99), "1'-0.0\"");
|
||||||
|
assert_eq!(with_units(3, 1, 23.99), "2'-0.0\"");
|
||||||
|
assert_eq!(with_units(3, 2, 11.999), "1'-0.00\"");
|
||||||
|
assert_eq!(with_units(3, 1, 66.5), "5'-6.5\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formatted_lengths_read_back() {
|
||||||
|
for lunits in [3, 4, 5] {
|
||||||
|
for value in [0.995f64, 5.995, 11.999, 23.999, 66.5, 9.25] {
|
||||||
|
let shown = with_units(lunits, if lunits == 3 { 4 } else { 4 }, value);
|
||||||
|
let read = parse_length(&shown)
|
||||||
|
.unwrap_or_else(|| panic!("lunits {lunits} wrote {shown:?}, which does not read back"));
|
||||||
|
assert!(
|
||||||
|
(read - value).abs() < 0.02,
|
||||||
|
"lunits {lunits}: {value} wrote {shown:?}, read back as {read}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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\"");
|
||||||
|
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\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
assert_ne!(shown(4, 8, 0.0000000005), "E");
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue