mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Merge pull request #267 from alexanderalber/fix/64-write-at-as-single
XLSX: write `@` as `_xlfn.SINGLE` in a dynamic-array formula (#64) # Conflicts: # crates/xlsx/src/tests/roundtrip.rs
This commit is contained in:
@@ -236,6 +236,13 @@ impl Parser {
|
||||
{
|
||||
return Ok(Expr::Unary(UnOp::Spill, Box::new(r)));
|
||||
}
|
||||
// Files write `@x` as `_xlfn.SINGLE(x)`.
|
||||
if name == "SINGLE"
|
||||
&& args.len() == 1
|
||||
&& let Some(x) = args.pop()
|
||||
{
|
||||
return Ok(Expr::Unary(UnOp::At, Box::new(x)));
|
||||
}
|
||||
Ok(Expr::Call(name, args))
|
||||
}
|
||||
Tok::Sheet(a, b) => {
|
||||
|
||||
@@ -24,7 +24,10 @@ pub(crate) fn needs_array(wb: &Workbook, sheet: usize, at: CellRef, formula: &Fo
|
||||
let Some(expr) = formula.expr_arc() else { return false };
|
||||
let mut scan = Scan { wb, at, remaining: 4096, locals: Vec::new() };
|
||||
let shape = scan.expr(&expr, sheet, false, 0);
|
||||
shape.array || shape.affected
|
||||
// An explicit `@` is written as `_xlfn.SINGLE`, which only dynamic-array formulas carry.
|
||||
let mut at = false;
|
||||
expr.walk(&mut |e| at |= matches!(e, Expr::Unary(UnOp::At, _)));
|
||||
shape.array || shape.affected || at
|
||||
}
|
||||
|
||||
struct Scan<'a> {
|
||||
|
||||
@@ -78,6 +78,8 @@ fn expr_to_file(e: Expr) -> String {
|
||||
},
|
||||
// Files have no `#` operator: Excel writes `A1#` as `_xlfn.ANCHORARRAY(A1)`.
|
||||
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]),
|
||||
other => other,
|
||||
});
|
||||
gridcraft_formula::print(&e)
|
||||
@@ -115,6 +117,15 @@ mod tests {
|
||||
assert_eq!(from_file("SUM(_xlfn.ANCHORARRAY(Sheet1!$D$1))+_xlfn.ANCHORARRAY(B2)"), "SUM(Sheet1!$D$1#)+B2#");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_intersection() {
|
||||
assert_eq!(to_file(&Formula::new("SUM(@A1:A3*B1:B3)")), "SUM(_xlfn.SINGLE(A1:A3)*B1:B3)");
|
||||
assert_eq!(to_file(&Formula::new("@A1:A3*2")), "_xlfn.SINGLE(A1:A3)*2");
|
||||
assert_eq!(text_to_file("=@Sheet1!$A$1:$A$3"), "_xlfn.SINGLE(Sheet1!$A$1:$A$3)");
|
||||
assert_eq!(from_file("_xlfn.SINGLE(A1:A3)*2"), "@A1:A3*2");
|
||||
assert_eq!(from_file("SUM(_xlfn.SINGLE(A1:A3)*B1:B3)"), "SUM(@A1:A3*B1:B3)");
|
||||
}
|
||||
|
||||
#[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)");
|
||||
|
||||
@@ -236,8 +236,10 @@ fn implicit_intersection_metadata_respects_function_argument_context() {
|
||||
("SUM(A1:A3)", false),
|
||||
("SUMPRODUCT(A1:A3*B1:B3)", false),
|
||||
("SUMPRODUCT(SQRT(A1:A3))", false),
|
||||
("SQRT(@A1:A3)", false),
|
||||
("@A1:A3*2", false),
|
||||
// An explicit `@` is written as `_xlfn.SINGLE`, a dynamic-array formula. Dropping it
|
||||
// instead would need the reader to put it back on legacy formulas.
|
||||
("SQRT(@A1:A3)", true),
|
||||
("@A1:A3*2", true),
|
||||
("@(A1:A3*2)", true),
|
||||
("SQRT(INDIRECT(\"A1\"))", false),
|
||||
("SUM(INDIRECT(\"A1:A3\"))", false),
|
||||
|
||||
@@ -648,3 +648,26 @@ fn icon_sets_always_write_one_cfvo_per_icon() {
|
||||
.collect();
|
||||
assert_eq!(rules, [3, 4, 5]);
|
||||
}
|
||||
|
||||
/// Files have no `@` operator: Excel writes `@x` as `_xlfn.SINGLE(x)` in a dynamic-array
|
||||
/// formula, and reads it back as `@x`.
|
||||
#[test]
|
||||
fn implicit_intersection_is_written_as_single() {
|
||||
let mut wb = Workbook::new();
|
||||
let s = wb.sheet_mut(0).unwrap();
|
||||
for (r, n) in [(0, 1.0), (1, 3.0), (2, 5.0)] {
|
||||
s.set_value(CellRef::new(r, 0), Value::Number(n));
|
||||
}
|
||||
s.cells.set(at("D3"), Cell { value: Value::Number(10.0), ..Cell::formula(Formula::new("@A1:A3*2")) });
|
||||
s.cells.set(at("D4"), Cell { value: Value::Number(2.0), ..Cell::formula(Formula::new("A1*2")) });
|
||||
let bytes = write_xlsx(&wb).unwrap();
|
||||
let (xml, metadata) = sheet1_xml(&bytes);
|
||||
assert!(xml.contains("<c r=\"D3\" cm=\"1\"><f t=\"array\" ref=\"D3\">_xlfn.SINGLE(A1:A3)*2</f><v>10</v>"), "{xml}");
|
||||
assert!(xml.contains("<c r=\"D4\"><f>A1*2</f>"), "{xml}");
|
||||
assert!(!xml.contains('@'), "{xml}");
|
||||
assert!(metadata);
|
||||
let (back, _) = read_xlsx(&bytes).unwrap();
|
||||
let sh = back.sheet(0).unwrap();
|
||||
assert_eq!(sh.cell(at("D3")).and_then(|c| c.formula.as_ref()).map(|f| f.text.as_str()), Some("@A1:A3*2"));
|
||||
assert_eq!(sh.cell(at("D3")).and_then(|c| c.formula.as_ref()).and_then(|f| f.array), None);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user