Units and rtos: round a half away from zero, at 15 significant digits (#2484)

Decimal and scientific numbers went through Rust's formatting, which
rounds the exact binary value and a half to even: (rtos 1500.5 2 0)
gave 1500, (rtos 0.125 2 2) 0.12 and (rtos 1.005 2 2) 1.00.

AutoCAD 2024 gives 1501, 0.13 and 1.01: it rounds at 15 significant
digits, with a half away from zero, for scientific notation too
(2.5 to none is 3E+00). units::fixed does that on the decimal digits;
format_distance's decimal and scientific forms and rtos use it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2ez4gcx
2026-10-30 15:45:57 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 6ec6d97ea0
commit 2be869a154
2 changed files with 81 additions and 15 deletions
+1 -1
View File
@@ -1055,7 +1055,7 @@ pub fn rtos(ctx: &mut Ctx<'_>, a: Vec<Value>) -> Result<Flow, LispError> {
}
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();
}
+80 -14
View File
@@ -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::<i64>() else { return plain() };
let sig: Vec<u8> = 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<u8> = (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::<f64>(), e.parse::<i64>()) 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");