diff --git a/crates/engine/src/lisp/drawing.rs b/crates/engine/src/lisp/drawing.rs index 88da7fd85..238551094 100644 --- a/crates/engine/src/lisp/drawing.rs +++ b/crates/engine/src/lisp/drawing.rs @@ -1055,7 +1055,7 @@ pub fn rtos(ctx: &mut Ctx<'_>, a: Vec) -> Result { } fn decimal(v: f64, prec: usize, dimzin: i64) -> String { - let mut s = format!("{v:.prec$}"); + let mut s = crate::units::fixed(v, prec); if s.starts_with("-0") && s.trim_start_matches(['-', '0', '.']).is_empty() { s = s.trim_start_matches('-').to_string(); } diff --git a/crates/engine/src/units.rs b/crates/engine/src/units.rs index 0a18a0039..3c6f3456e 100644 --- a/crates/engine/src/units.rs +++ b/crates/engine/src/units.rs @@ -68,23 +68,62 @@ pub fn format_distance(v: f64, lunits: i64, luprec: i64) -> String { }; format!("{}{}{}", if neg { "-" } else { "" }, whole, fs) } - _ => format!("{:.*}", prec, v), + _ => fixed(v, prec), } } -/// 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, +/// `v` with `prec` decimals, rounded as AutoCAD rounds a number it shows (RTOS, LUNITS): at 15 +/// significant digits, with a half rounded away from zero. AutoCAD 2024 gives 1.01 for 1.005 to two +/// places (its binary value is just below 1.005), 3 for 2.5 and -0.13 for -0.125, where Rust's +/// formatting rounds the binary value and a half to even (1.00, 2 and -0.12). +pub fn fixed(v: f64, prec: usize) -> String { + let plain = || format!("{v:.prec$}"); + if !v.is_finite() { + return plain(); } + let sci = format!("{:.14e}", v.abs()); + let Some((mantissa, exp)) = sci.split_once('e') else { return plain() }; + let Ok(exp) = exp.parse::() else { return plain() }; + let sig: Vec = mantissa.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect(); + // Digit `k` of `sig` is worth 10^(exp - k); keep those down to 10^-prec, in units of 10^-prec. + let digit = |k: i64| usize::try_from(k).ok().and_then(|k| sig.get(k)).copied().unwrap_or(0); + let keep = exp + 1 + prec as i64; + let mut units: Vec = (0..keep).map(digit).collect(); + if keep >= 0 && digit(keep) >= 5 { + let mut carry = true; + for d in units.iter_mut().rev() { + if *d == 9 { + *d = 0; + } else { + *d += 1; + carry = false; + break; + } + } + if carry { + units.insert(0, 1); + } + } + let n: String = units.iter().map(|d| char::from(b'0' + d)).collect(); + let n = format!("{:0>width$}", n.trim_start_matches('0'), width = prec + 1); + let (int, frac) = n.split_at(n.len() - prec); + let body = if prec == 0 { int.to_string() } else { format!("{int}.{frac}") }; + if v < 0.0 && n.bytes().any(|b| b != b'0') { format!("-{body}") } else { body } +} + +/// Scientific notation with a signed exponent of at least two digits (`1.7500E+01`, `1.00E-05`), +/// the mantissa rounded as [`fixed`] rounds. +fn scientific(v: f64, prec: usize) -> String { + let s = format!("{:.14e}", v); + let Some((m, e)) = s.split_once('e') else { return s }; + let (Ok(m), Ok(mut e)) = (m.parse::(), e.parse::()) else { return s }; + let mut mantissa = fixed(m, prec); + // 9.99… rounded up to 10: one more power of ten. + if mantissa.trim_start_matches('-').starts_with("10") { + e += 1; + mantissa = fixed(m / 10.0, prec); + } + format!("{mantissa}E{}{:0>2}", if e < 0 { '-' } else { '+' }, e.abs()) } fn gcd(a: i64, b: i64) -> i64 { @@ -435,11 +474,38 @@ mod tests { assert_eq!(format_distance(11.6, 3, 0), "1'-0\""); } + /// RTOS in AutoCAD 2024 (accoreconsole, DIMZIN 0): a half rounds away from zero, at 15 + /// significant digits. + #[test] + fn numbers_round_as_autocad_rounds_them() { + for (v, prec, want) in [ + (1500.5, 0, "1501"), + (2.5, 0, "3"), + (-2.5, 0, "-3"), + (0.125, 2, "0.13"), + (0.5, 0, "1"), + (1.005, 2, "1.01"), + (3.5, 0, "4"), + (0.0625, 3, "0.063"), + (-0.125, 2, "-0.13"), + (1234.5678, 3, "1234.568"), + ] { + assert_eq!(format_distance(v, 2, prec), want, "{v} to {prec}"); + } + assert_eq!(format_distance(2.5, 1, 0), "3E+00"); + assert_eq!(format_distance(0.125, 1, 1), "1.3E-01"); + assert_eq!(format_distance(9.96, 1, 1), "1.0E+01"); + assert_eq!(fixed(-0.004, 2), "0.00"); + assert_eq!(fixed(1e-20, 3), "0.000"); + assert_eq!(fixed(123456789012.0, 1), "123456789012.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"); + // 1.2345 is halfway: away from zero, as AutoCAD rounds 2.5 to 3E+00. + assert_eq!(format_distance(1234.5, 1, 3), "1.235E+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");