diff --git a/crates/numfmt/src/lib.rs b/crates/numfmt/src/lib.rs index 674e844..9fcc146 100644 --- a/crates/numfmt/src/lib.rs +++ b/crates/numfmt/src/lib.rs @@ -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) diff --git a/crates/numfmt/src/render.rs b/crates/numfmt/src/render.rs index 7befe59..d6c7d67 100644 --- a/crates/numfmt/src/render.rs +++ b/crates/numfmt/src/render.rs @@ -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 = vec![String::new(); n]; let mut before: Vec = vec![String::new(); n + 1]; let mut lits: Vec> = 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 } diff --git a/crates/numfmt/src/tests.rs b/crates/numfmt/src/tests.rs index 6c2306c..1cd88ef 100644 --- a/crates/numfmt/src/tests.rs +++ b/crates/numfmt/src/tests.rs @@ -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"),