From 94014c5594058eb3dad64cbc9a4745a1c56e450f Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Mon, 12 Oct 2026 18:51:40 +0200 Subject: [PATCH] Tables: structured references keep the spaces at the edges of a column name (fixes #732) --- crates/engine/src/tests.rs | 30 ++++++++++++++++++++++++++++++ crates/formula/src/parser.rs | 5 ++++- crates/formula/src/printer.rs | 7 ++++++- crates/formula/src/tests.rs | 14 ++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 33fdc11..66ef9d7 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1018,6 +1018,36 @@ fn tables_and_structured_refs() { assert_eq!(v(&s, "B4"), Value::Number(7.0)); } +#[test] +fn structured_refs_to_columns_with_edge_spaces() { + let mut s = s(); + s.execute("range.setValues", json!({"range": "A1", "values": [["Item", "Qty "], ["a", 2], ["b", 3]]})).unwrap(); + s.execute("insert.table", json!({"range": "A1:B3", "hasHeaders": true})).unwrap(); + s.execute("table.totalRow", json!({"on": true, "table": "Table1"})).unwrap(); + assert_eq!(v(&s, "B4"), Value::Number(5.0)); + for (cell, input) in [ + ("F1", "=SUM(Table1[Qty ])"), + ("F2", "=Table1[[#Totals],[Qty ]]"), + ("F3", "=SUBTOTAL(109,Table1[Qty ])"), + ("F4", "=SUM(Table1[[Qty ]])"), + ("F5", "=SUM(Table1[Qty])"), + ("C2", "=Table1[@[Qty ]]*10"), + ] { + s.execute("cell.set", json!({"cell": cell, "input": input})).unwrap(); + } + let n = Value::Number; + assert_eq!([v(&s, "F1"), v(&s, "F2"), v(&s, "F3"), v(&s, "F4")], [n(5.0), n(5.0), n(5.0), n(5.0)]); + assert_eq!(v(&s, "F5"), Value::Error(gridcraft_core::CellError::Ref)); + assert_eq!(v(&s, "C2"), n(20.0)); + assert_eq!(s.execute("cell.get", json!({"cell": "F3"})).unwrap()["formula"], "=SUBTOTAL(109,Table1[[Qty ]])"); + + let r = s.execute("file.saveBytes", json!({"format": "xlsx"})).unwrap(); + let b64 = r["base64"].as_str().unwrap().to_string(); + s.execute("file.open", json!({"name": "x.xlsx", "base64": b64})).unwrap(); + assert_eq!(s.execute("cell.get", json!({"cell": "F3"})).unwrap()["formula"], "=SUBTOTAL(109,Table1[[Qty ]])"); + assert_eq!([v(&s, "F1"), v(&s, "F2"), v(&s, "F3")], [n(5.0), n(5.0), n(5.0)]); +} + #[test] fn slicer_filters_table() { let mut s = s(); diff --git a/crates/formula/src/parser.rs b/crates/formula/src/parser.rs index 4fd3b7b..8fc4de9 100644 --- a/crates/formula/src/parser.rs +++ b/crates/formula/src/parser.rs @@ -417,6 +417,9 @@ fn combine(op: BinOp, lhs: Expr, rhs: Expr) -> Expr { pub fn parse_struct(table: &str, body: &str) -> Result { let mut specifiers = Vec::new(); let mut cols: Vec = Vec::new(); + // Spaces at the edges of a column name are part of it (`Table1[Qty ]` names `Qty `), so the + // untrimmed body is what a lone column is read from. + let raw = body; let body = body.trim(); let unescape = |s: &str| { let mut out = String::new(); @@ -468,7 +471,7 @@ pub fn parse_struct(table: &str, body: &str) -> Result { } else if let Some(i) = item(body) { specifiers.push(i); } else if !body.is_empty() { - cols.push(column(body)?); + cols.push(column(raw)?); } } else { // [[#Headers],[Col1]:[Col3]], [[#This Row],[Col 1]] or [[#This Row],Col1] diff --git a/crates/formula/src/printer.rs b/crates/formula/src/printer.rs index a63599b..3673036 100644 --- a/crates/formula/src/printer.rs +++ b/crates/formula/src/printer.rs @@ -109,7 +109,12 @@ pub fn struct_ref(s: &StructRef) -> String { let _ = write!(out, "[{}]", s.specifiers.first().map(item).unwrap_or("")); } (Some(c), None) if s.specifiers.is_empty() => { - let _ = write!(out, "[{}]", esc(c)); + // Excel writes a name with spaces at its edges in the double-bracket form. + if c.trim() == c { + let _ = write!(out, "[{}]", esc(c)); + } else { + let _ = write!(out, "[[{}]]", esc(c)); + } } (Some(c), None) if this_row => { if simple_col(c) { diff --git a/crates/formula/src/tests.rs b/crates/formula/src/tests.rs index d3dd4c2..aef2649 100644 --- a/crates/formula/src/tests.rs +++ b/crates/formula/src/tests.rs @@ -248,6 +248,20 @@ fn unbracketed_this_row_column() { } } +#[test] +fn struct_column_keeps_edge_spaces() { + // The spaces are part of the column name: `Table1[Qty ]` names `Qty `, never `Qty`. + let Expr::Struct(s) = parse("Table1[Qty ]").unwrap() else { panic!() }; + assert_eq!(s.col_start.as_deref(), Some("Qty ")); + let Expr::Struct(s) = parse("Table1[ Qty]").unwrap() else { panic!() }; + assert_eq!(s.col_start.as_deref(), Some(" Qty")); + // Excel writes such a column in the double-bracket form. + assert_eq!(rt("Table1[Qty ]"), "Table1[[Qty ]]"); + for f in ["Table1[[Qty ]]", "Table1[[ Sum for Q2 ]]", "Table1[[#Totals],[Qty ]]", "Table1[@[Qty ]]", "Table1[Unit Price]"] { + assert_eq!(rt(f), f); + } +} + #[test] fn trim_references() { for f in [