Merge pull request #869 from marcoarnulfo:fix/this-row-structured-refs

This commit is contained in:
Brandon Thomas 2026-10-12 23:50:09 -04:00
commit d14269a001
3 changed files with 65 additions and 1 deletions

View File

@ -91,6 +91,16 @@ fn range_r1c1(r: &Reference, at: CellRef, sep: &str) -> String {
}
pub fn struct_ref(s: &StructRef) -> String {
write_struct_ref(s, true)
}
/// Structured reference as files spell it: `[@Col]` is only Excel's display shorthand, files
/// hold `[[#This Row],[Col]]` (and `[#This Row]` for `[@]`).
pub fn struct_ref_file(s: &StructRef) -> String {
write_struct_ref(s, false)
}
fn write_struct_ref(s: &StructRef, at_shorthand: bool) -> String {
let esc = |c: &str| c.replace('\'', "''").replace('[', "'[").replace(']', "']").replace('#', "'#");
let item = |i: &StructItem| match i {
StructItem::All => "#All",
@ -99,7 +109,7 @@ pub fn struct_ref(s: &StructRef) -> String {
StructItem::Totals => "#Totals",
StructItem::ThisRow => "#This Row",
};
let this_row = s.specifiers == [StructItem::ThisRow];
let this_row = at_shorthand && s.specifiers == [StructItem::ThisRow];
let mut out = s.table.clone();
let simple_col = |c: &str| !c.chars().any(|ch| matches!(ch, ' ' | '[' | ']' | '#' | '\'' | ',' | ':'));
match (&s.col_start, &s.col_end) {

View File

@ -82,6 +82,9 @@ fn expr_to_file(e: Expr) -> String {
Expr::Unary(gridcraft_formula::UnOp::Spill, r) => Expr::Call("_xlfn.ANCHORARRAY".into(), vec![*r]),
// Nor an `@` operator: Excel writes `@x` as `_xlfn.SINGLE(x)`.
Expr::Unary(gridcraft_formula::UnOp::At, x) => Expr::Call("_xlfn.SINGLE".into(), vec![*x]),
// Nor the `[@Col]` shorthand: files spell it `[[#This Row],[Col]]`, or Excel repairs
// the file and drops the table. Carried as a name, which prints verbatim.
Expr::Struct(st) => Expr::Name(gridcraft_formula::printer::struct_ref_file(&st)),
// Nor trim-reference operators: written as the equivalent `TRIMRANGE(range,n,n)`, which
// every Excel with trim references reads. (Excel's own file spelling of `A1:.E10` is
// not publicly documented; the `_xlfn._TRO_*` call forms are accepted on read.)
@ -152,6 +155,20 @@ mod tests {
assert_eq!(from_file("Sales[[#This Row],Units]*[[#This Row],Price]"), "Sales[@Units]*[@Price]");
}
#[test]
fn this_row_long_form() {
// `[@Col]` is only Excel's display shorthand; a file holding it is repaired by Excel,
// which drops the table.
assert_eq!(to_file(&Formula::new("Sales[@Units]*Sales[@[Unit Price]]")), "Sales[[#This Row],[Units]]*Sales[[#This Row],[Unit Price]]");
assert_eq!(text_to_file("=[@Units]&COUNTA(Sales[@])"), "[[#This Row],[Units]]&COUNTA(Sales[#This Row])");
assert_eq!(text_to_file("=SUM(Sales[Units])+[#Totals]"), "SUM(Sales[Units])+[#Totals]");
for f in ["Sales[@Units]*[@[Unit Price]]", "Sales[@]", "[@[Unit Price]]+1"] {
let file = to_file(&Formula::new(f));
assert!(!file.contains("[@"), "{file}");
assert_eq!(from_file(&file), f);
}
}
#[test]
fn prefixes() {
assert_eq!(to_file(&Formula::new("XLOOKUP(1,A:A,B:B)+SUM(A1)")), "_xlfn.XLOOKUP(1,A:A,B:B)+SUM(A1)");

View File

@ -200,6 +200,43 @@ fn calculated_table_column_keeps_formula_and_cell_value() {
assert_eq!(sh.value(at("B2")), Value::Number(6.0));
}
#[test]
fn this_row_references_are_written_in_long_form() {
let mut wb = Workbook::new();
let sh = wb.sheet_mut(0).unwrap();
sh.cells.set(at("A2"), Cell::value(Value::Number(3.0)));
sh.cells.set(at("B2"), formula("Data[@Input]*2", 6.0));
sh.tables.push(Table {
id: 1,
name: "Data".into(),
range: range("A1:B2"),
header_row: true,
totals_row: false,
columns: vec![
TableColumn { name: "Input".into(), totals: TotalsFn::None, totals_label: None, formula: None },
TableColumn { name: "Output".into(), totals: TotalsFn::None, totals_label: None, formula: Some("[@Input]*2".into()) },
],
style: "TableStyleLight1".into(),
banded_rows: true,
banded_cols: false,
first_col: false,
last_col: false,
filter_button: true,
filter: vec![],
});
let bytes = write_xlsx(&wb).unwrap();
let mut pkg = Package::open(&bytes).unwrap();
let sheet = pkg.read_xml("xl/worksheets/sheet1.xml").unwrap().unwrap();
assert_eq!(cell(&sheet, "B2").child("f").unwrap().text, "Data[[#This Row],[Input]]*2");
let table = pkg.read_xml("xl/tables/table1.xml").unwrap().unwrap();
let column = table.child("tableColumns").unwrap().kids("tableColumn").nth(1).unwrap();
assert_eq!(column.child("calculatedColumnFormula").unwrap().text, "[[#This Row],[Input]]*2");
let (back, _) = read_xlsx(&bytes).unwrap();
let sh = back.sheet(0).unwrap();
assert_eq!(sh.tables[0].columns[1].formula.as_deref(), Some("[@Input]*2"));
assert_eq!(sh.cell(at("B2")).unwrap().formula.as_ref().unwrap().text, "Data[@Input]*2");
}
/// Compare both cell and formula metadata, collecting all boundaries in the group on failure.
fn check_formula_metadata(mut wb: Workbook, sheet_index: usize, cases: &[(&str, bool)]) {
for (i, (text, _)) in cases.iter().enumerate() {