From 5c81d33027dcffd34e21aae3c5d2ed2c74a8b914 Mon Sep 17 00:00:00 2001 From: NgoQuocViet2001 Date: Wed, 19 Aug 2026 21:41:14 +0700 Subject: [PATCH] fix(units): carry a length that rounds up to the next unit format_length rounded the fraction after splitting the whole part off, so a value that rounded up to a full unit had nowhere to go: fractional 5.995 -> 5 (64/64 reduced to 1/1, then dropped) fractional 0.995 -> 0 architectural 11.999 -> 0'-11" architectural 23.999 -> 1'-11" engineering 11.99 -> 0'-12.0" (twelve inches is a foot) engineering 23.99 -> 1'-12.0" Round once, into the smallest unit that will be shown, then split feet and whole inches out of the rounded total. The carry reaches every column, so the same values now read 6, 1, 1'-0", 2'-0", 1'-0.0" and 2'-0.0". --- src/entities/common.rs | 105 +++++++++++++++++++++++++++++++++++------ 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/src/entities/common.rs b/src/entities/common.rs index 09a416c46..6a7c53d99 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -104,11 +104,16 @@ pub fn format_length(value: f64) -> String { match ctx.lunits { 1 => format!("{:.*e}", prec, value), 3 => { - // Engineering: ft-inches, decimal inches. + // Engineering: ft-inches, decimal inches. Round to the shown + // precision BEFORE splitting off the feet, so an inch count that + // rounds up to twelve carries instead of printing 0'-12.0". let sign = if value < 0.0 { "-" } else { "" }; let abs = value.abs(); - let feet = (abs / 12.0).trunc(); - let rem = abs - feet * 12.0; + let scale = 10f64.powi(prec.min(15) as i32); + 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) } 4 | 5 => { @@ -117,17 +122,21 @@ pub fn format_length(value: f64) -> String { // result reads like 1/64". let sign = if value < 0.0 { "-" } else { "" }; let abs = value.abs(); - let (feet, in_rem) = if ctx.lunits == 4 { - let f = (abs / 12.0).trunc(); - (Some(f as i64), abs - f * 12.0) - } else { - (None, abs) - }; - let whole = in_rem.trunc(); - let frac = in_rem - whole; let denom = 64u64; - let numer = (frac * denom as f64).round() as i64; - let mut n = numer as u64; + // Round once, into the smallest unit that will be shown, and + // split the feet and whole inches out of the rounded total. A + // fraction that rounds up to 64/64 then carries, instead of + // reducing to 1/1 and being dropped as "no fraction" — 5.999 + // read as 5, and 11.999 as 0'-11". + let sixtyfourths = (abs * denom as f64).round() as u64; + let (feet, rest) = if ctx.lunits == 4 { + let per_foot = 12 * denom; + (Some((sixtyfourths / per_foot) as i64), sixtyfourths % per_foot) + } else { + (None, sixtyfourths) + }; + let whole = rest / denom; + let mut n = rest % denom; let mut d = denom; while d > 1 && n % 2 == 0 && d % 2 == 0 { n /= 2; @@ -140,8 +149,8 @@ pub fn format_length(value: f64) -> String { }; let unit_suffix = if ctx.lunits == 4 { "\"" } else { "" }; match feet { - Some(f) => format!("{}{}'-{:.0}{}{}", sign, f, whole, frac_str, unit_suffix), - None => format!("{}{:.0}{}", sign, whole, frac_str), + Some(f) => format!("{}{}'-{}{}{}", sign, f, whole, frac_str, unit_suffix), + None => format!("{}{}{}", sign, whole, frac_str), } } _ => format!("{:.*}", prec, value), @@ -791,3 +800,69 @@ pub(crate) fn polyline_segment_fill( 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) + } + + /// A fraction that rounds up to a whole unit has to carry. It used to + /// reduce 64/64 to 1/1, which reads as "no fraction", so the fractional + /// part was dropped and the length shown a whole unit short. + #[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"); + // Values that do not round up are unchanged. + assert_eq!(with_units(5, 4, 0.5), "0 1/2"); + assert_eq!(with_units(5, 4, 9.25), "9 1/4"); + } + + /// The same carry has to reach the feet, or a foot's worth of inches + /// stays in the inches column. + #[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\""); + } + + /// Engineering rounds to the shown precision, so twelve inches after + /// rounding is a foot. It printed the impossible 0'-12.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\""); + } + + /// Whatever the drawing writes, it must read back the same. + #[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}" + ); + } + } + } +}