Conditional formatting: show a rule's number format (fixes #1163) (#1769)

The grid's look() and print's cell_look() layered a rule's fill, font and
borders onto the cell style but not its num_fmt, so a cell under a rule with a
number format kept its own. Take the rule's code when it isn't General (in a
differential style General means "not set", as layer_under treats it).

Print also re-formatted every non-General cell from its own style through
display::cell_text; format with the look's code instead, through a new
display::value_text_as that value_text now calls (CSV/Text output unchanged).
Show Formulas keeps cell_text.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dante Bertuzzi
2026-10-17 18:37:09 -06:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 83189c0ccd
commit 19f4387cca
3 changed files with 54 additions and 2 deletions
+21 -1
View File
@@ -650,6 +650,10 @@ fn cell_look(wb: &Workbook, si: usize, sh: &Sheet, c: CellRef, cf: &mut Option<c
if st.font.underline != Underline::None {
style.font.underline = st.font.underline;
}
// In a differential style General means "not set" (#1163).
if st.num_fmt.as_str() != "General" {
style.num_fmt = st.num_fmt.clone();
}
for (dst, src) in [
(&mut style.borders.left, st.borders.left),
(&mut style.borders.right, st.borders.right),
@@ -1076,7 +1080,8 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3
let max_chars = (avail / digit).floor().max(1.0) as usize;
gridcraft_numfmt::format_general_fit(n, max_chars.min(11)).unwrap_or_else(|| "#".repeat(max_chars.min(255)))
} else {
crate::display::cell_text(wb, sh, c)
// The look's code, which a conditional format can override (#1163).
crate::display::value_text_as(wb, &v, code)
};
if !wrap {
text = text.replace(['\n', '\r'], " ");
@@ -1932,6 +1937,21 @@ mod tests {
assert_eq!(expand_header_footer("&", &cx)[1].text, "");
}
#[test]
fn conditional_number_format_prints() {
// #1163: a rule's number format replaces the cell's own, whether that is General or not;
// cells the rule doesn't match keep theirs.
let mut s = s();
s.execute("range.setValues", json!({"range": "A1", "values": [[1], [3], [4]]})).unwrap();
s.execute("home.numberFormat", json!({"range": "A3", "code": "0.0"})).unwrap();
let rule = json!({"type": "cellIs", "operator": "greater", "value": "2", "style": {"num_fmt": "0.00"}});
s.execute("home.conditionalFormat", json!({"range": "A1:A3", "rule": rule})).unwrap();
let (pdf, _) = pdf_of(&mut s, json!({}));
assert!(contains(&pdf, "(1) Tj"), "an unmatched cell keeps General");
assert!(contains(&pdf, "(3.00) Tj") && !contains(&pdf, "(3) Tj"), "General cell under the rule");
assert!(contains(&pdf, "(4.00) Tj") && !contains(&pdf, "(4.0) Tj"), "formatted cell under the rule");
}
#[test]
fn export_writes_file() {
let mut s = s();
+7 -1
View File
@@ -95,7 +95,13 @@ pub fn cell_text(wb: &Workbook, sheet: &Sheet, c: CellRef) -> String {
/// and no General narrowing (Save As CSV/Text writes this). Formulas give their result.
pub fn value_text(wb: &Workbook, sheet: &Sheet, c: CellRef) -> String {
let style = wb.styles.get(sheet.style_id(c));
let f = format(&sheet.value(c), style.num_fmt.as_str(), wb);
value_text_as(wb, &sheet.value(c), style.num_fmt.as_str())
}
/// [`value_text`] for a value and a format code given apart from a cell (print takes the code
/// from the cell's effective look, which a conditional format can override).
pub fn value_text_as(wb: &Workbook, v: &Value, code: &str) -> String {
let f = format(v, code, wb);
match f.fill {
Some((_, pos)) => {
let mut t = f.text;
+26
View File
@@ -482,6 +482,10 @@ fn look(wb: &Workbook, si: usize, sh: &Sheet, c: CellRef, cf: &mut CfCache) -> (
if st.font.strike {
style.font.strike = true;
}
// In a differential style General means "not set" (#1163).
if st.num_fmt.as_str() != "General" {
style.num_fmt = st.num_fmt.clone();
}
for (dst, src) in [
(&mut style.borders.left, st.borders.left),
(&mut style.borders.right, st.borders.right),
@@ -3406,6 +3410,28 @@ mod tests {
assert_eq!(center_across_end(0, 1, |_| true), 1);
}
#[test]
fn conditional_number_format_reaches_the_cell_text() {
// #1163: a rule's number format replaces the cell's own; unmatched cells keep theirs.
let mut session = gridcraft_engine::Session::new();
session.new_workbook();
session.execute("range.setValues", json!({"range": "A1", "values": [[1], [3], [4]]})).unwrap();
session.execute("home.numberFormat", json!({"range": "A3", "code": "0.0"})).unwrap();
let rule = json!({"type": "cellIs", "operator": "greater", "value": "2", "style": {"num_fmt": "0.00"}});
session.execute("home.conditionalFormat", json!({"range": "A1:A3", "rule": rule})).unwrap();
let wb = session.active().unwrap().wb.clone();
let sh = wb.sheet(0).unwrap();
let mut cf = CfCache::new();
let text = |row: u32, cf: &mut CfCache| {
let c = CellRef::new(row, 0);
let (lk, _) = look(&wb, 0, sh, c, cf);
display_text(&wb, sh, c, &sh.value(c), &lk.style, 200.0, 7.0).0
};
assert_eq!(text(0, &mut cf), "1");
assert_eq!(text(1, &mut cf), "3.00");
assert_eq!(text(2, &mut cf), "4.00");
}
#[test]
fn filter_menu_uses_the_table_whose_header_was_clicked() {
use gridcraft_engine::model::Table;