XLSX: write this-row structured references as [[#This Row],[Col]] (fixes #864)
[@Col] is only Excel's display shorthand. Written verbatim into cell formulas and calculatedColumnFormula, it made Excel repair the file and drop the tables.
This commit is contained in:
parent
fc45b7f6d8
commit
dab4aa5e79
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)");
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue