From 4e7379bd6801b935448183f052109993bc6cea06 Mon Sep 17 00:00:00 2001 From: nihilist-narwhal <240798405+nihilist-narwhal@users.noreply.github.com> Date: Sat, 10 Oct 2026 03:55:17 -0500 Subject: [PATCH] Conditional formatting: cell value rules on blank and error cells Excel compares a blank cell as 0 (or empty text) in Cell Value rules, so "less than 5" or "equal to 0" also formats blank cells, and a cell holding an error never matches (the comparison is an error). GridCraft skipped blank cells and matched errors as if they sorted above every number ("greater than 5" formatted #N/A). The No Blanks rule also counted a cell holding only spaces as not blank, while the Blanks rule (like Excel's LEN(TRIM())) counted it as blank, so such a cell matched both. --- crates/engine/src/cf.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cf.rs b/crates/engine/src/cf.rs index f7d3623df..6640c7ae8 100644 --- a/crates/engine/src/cf.rs +++ b/crates/engine/src/cf.rs @@ -144,7 +144,8 @@ impl CfCache { CfOperator::GreaterOrEqual => compare(&v, &av) != std::cmp::Ordering::Less, CfOperator::LessOrEqual => compare(&v, &av) != std::cmp::Ordering::Greater, }; - (ok && !v.is_empty()).then_some(style.as_ref()) + // As in Excel, a blank cell compares as 0 (or ""), and an error never matches. + (ok && !v.is_error()).then_some(style.as_ref()) } CfRule::Expression { formula, style } => { gridcraft_calc::evaluate(wb, sheet, c, &relative(formula, first, c)).to_bool().unwrap_or(false).then_some(style.as_ref()) @@ -154,7 +155,7 @@ impl CfCache { CfRule::BeginsWith { text: t, style } => text.to_lowercase().starts_with(&t.to_lowercase()).then_some(style.as_ref()), CfRule::EndsWith { text: t, style } => text.to_lowercase().ends_with(&t.to_lowercase()).then_some(style.as_ref()), CfRule::Blanks { style } => (v.is_empty() || text.trim().is_empty()).then_some(style.as_ref()), - CfRule::NoBlanks { style } => (!v.is_empty()).then_some(style.as_ref()), + CfRule::NoBlanks { style } => (!v.is_empty() && !text.trim().is_empty()).then_some(style.as_ref()), CfRule::Errors { style } => v.is_error().then_some(style.as_ref()), CfRule::NoErrors { style } => (!v.is_error()).then_some(style.as_ref()), CfRule::Duplicate { style } => { @@ -367,4 +368,28 @@ mod tests { assert_eq!(both.fill, only_fill.fill); assert!(!both.borders.top.is_none()); } + + #[test] + fn cell_value_rules_on_blanks_and_errors() { + let mut s = crate::Session::new(); + s.new_workbook(); + s.execute("range.setValues", json!({"range": "A1", "values": [[3, "=NA()"], [null, 7], ["=NA()", null], [" ", null], [7, null]]})).unwrap(); + let rule = |range: &str, r: serde_json::Value| json!({"range": range, "rule": r}); + s.execute("home.conditionalFormat", rule("A1:A5", json!({"type": "cellIs", "operator": "less", "value": "5", "preset": "lightRedFill"}))) + .unwrap(); + s.execute("home.conditionalFormat", rule("A1:A5", json!({"type": "noBlanks", "preset": "redBorder"}))).unwrap(); + s.execute("home.conditionalFormat", rule("B1:B2", json!({"type": "cellIs", "operator": "greater", "value": "5", "preset": "lightRedFill"}))) + .unwrap(); + let d = s.doc().unwrap(); + let mut cache = CfCache::new(); + let mut look = |a: &str| cache.look(&d.wb, 0, CellRef::parse(a).unwrap()).and_then(|l| l.style); + let fill = |st: Option