Numfmt: a negative number that rounds to zero shows no minus sign (fixes #729)

This commit is contained in:
Marco Arnulfo 2026-10-12 18:35:01 +02:00
parent fc45b7f6d8
commit ea5db94fe3
3 changed files with 33 additions and 2 deletions

View File

@ -261,7 +261,8 @@ impl NumberFormat {
let Some(mut out) = out else {
return (Formatted { text: OVERFLOW.into(), color: sec.color, fill: None, numeric: true }, true);
};
if minus && sec.kind != SecKind::Text {
// Excel drops the minus sign when the first section shows a negative number as all zeros.
if minus && sec.kind != SecKind::Text && !out.zero {
out.prepend("-");
}
(Formatted { text: out.text, color: sec.color, fill: out.fill, numeric: true }, false)

View File

@ -11,6 +11,8 @@ use crate::parse::{DatePart, Section, Tok};
pub(crate) struct Out {
pub text: String,
pub fill: Option<(char, usize)>,
/// True when every digit shown is zero (a number section that rounded to zero).
pub zero: bool,
}
impl Out {
@ -247,6 +249,7 @@ pub(crate) fn render_number(sec: &Section, v: f64) -> Out {
let mut pieces: Vec<String> = vec![String::new(); n];
let mut before: Vec<String> = vec![String::new(); n + 1];
let mut lits: Vec<Option<String>> = Vec::new();
let mut zero = false;
if sec.fraction {
lits = vec![None; n];
render_fraction(sec, x, &mut pieces, &mut lits);
@ -310,6 +313,7 @@ pub(crate) fn render_number(sec: &Section, v: f64) -> Out {
} else {
fixed(x, frac_idx.len())
};
zero = int_s.chars().chain(frac_s.chars()).all(|c| c == '0');
fill_int(toks, &int_idx, &int_s, sec.thousands, false, &mut pieces);
if int_idx.is_empty()
&& !int_s.is_empty()
@ -338,6 +342,7 @@ pub(crate) fn render_number(sec: &Section, v: f64) -> Out {
_ => out.push(pieces.get(i).map(String::as_str).unwrap_or("")),
}
}
out.zero = zero;
out
}

View File

@ -24,7 +24,7 @@ fn numbers() {
assert_eq!(f("0", -2.5), "-3");
assert_eq!(f("0.00", 2.675), "2.68");
assert_eq!(f("0.00", 1.005), "1.01");
assert_eq!(f("0.0", -0.04), "-0.0");
assert_eq!(f("0.0", -0.04), "0.0");
assert_eq!(f("00000", 123.0), "00123");
assert_eq!(f("000-00-0000", 123456789.0), "123-45-6789");
assert_eq!(f("#.##", 0.5), ".5");
@ -105,6 +105,29 @@ fn scientific() {
assert_eq!(f("0.00E+00", -12345.0), "-1.23E+04");
}
/// A negative number that rounds to zero in the first section shows no minus sign; a negative
/// section of its own keeps its text (observed Excel results).
#[test]
fn negative_rounding_to_zero_drops_the_sign() {
let cases: &[(&str, f64, &str)] = &[
("0.00", -0.0033, "0.00"),
("0.00", -2.89e-9, "0.00"),
("0.00", -0.005, "-0.01"),
("0", -0.4, "0"),
("0", -0.5, "-1"),
("0.0", -0.004, "0.0"),
("#,##0.00", -0.004, "0.00"),
("0.00%", -2.89e-9, "0.00%"),
("0.00%", -0.0033, "-0.33%"),
("0.00;(0.00)", -0.0033, "(0.00)"),
("0.00;-0.00;\"zero\"", -0.0033, "-0.00"),
("0.00E+00", -2.89e-9, "-2.89E-09"),
];
for &(code, n, want) in cases {
assert_eq!(f(code, n), want, "{code} with {n}");
}
}
#[test]
fn fractions() {
assert_eq!(f("# ?/?", 1.5), "1 1/2");
@ -340,6 +363,8 @@ fn text_fn() {
assert_eq!(text_function(&Value::Number(5.0), "", S), Ok("".into()));
assert_eq!(text_function(&Value::Number(-1.0), "m/d/yyyy", S), Err(CellError::Value));
assert_eq!(text_function(&Value::Bool(true), "0", S), Ok("TRUE".into()));
assert_eq!(text_function(&Value::Number(-0.0033), "0", S), Ok("0".into()));
assert_eq!(text_function(&Value::Number(-0.4), "0", S), Ok("0".into()));
// Booleans go through the text section as TRUE/FALSE (observed Excel results).
let cases: &[(bool, &str, &str)] = &[
(true, ";;;@", "TRUE"),