diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 7c3a413..33b5228 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1059,6 +1059,33 @@ fn tables_and_structured_refs() { } #[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)]); fn table_rename_keeps_a_defined_name_with_the_same_name() { let mut s = s(); s.execute("range.setValues", json!({"range": "A1", "values": [["Qty"], [7]]})).unwrap(); diff --git a/crates/formula/src/parser.rs b/crates/formula/src/parser.rs index 1e3d96b..3ea9708 100644 --- a/crates/formula/src/parser.rs +++ b/crates/formula/src/parser.rs @@ -426,6 +426,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(); @@ -477,7 +480,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 a38e73e..3975a7c 100644 --- a/crates/formula/src/printer.rs +++ b/crates/formula/src/printer.rs @@ -119,7 +119,12 @@ fn write_struct_ref(s: &StructRef, at_shorthand: bool) -> 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 414410e..c8359b1 100644 --- a/crates/formula/src/tests.rs +++ b/crates/formula/src/tests.rs @@ -274,6 +274,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 [