mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-25 11:16:49 -06:00
A formula read with `cm` (Excel's XLDAPR dynamic-array record in xl/metadata.xml) now carries `Formula::dynamic`, and the writer keeps the `cm` and the metadata part for it even when the formula returns one value and the export shape analysis would not need array evaluation. Before, such formulas were saved as legacy formulas, so Excel recalculated some of them with implicit intersection (SUMPRODUCT(IFERROR(range/range,0)) changed value), and workbooks whose every dynamic formula was scalar lost xl/metadata.xml altogether. The mark survives moves, row and column insertion or deletion, sheet and name renames and LAMBDA call-case fixes; retyping a formula drops it and the writer decides from the formula's shape, as before. It is serialized only when set, so saved documents without it are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
3.3 KiB
Rust
88 lines
3.3 KiB
Rust
//! A formula opened with dynamic-array metadata keeps it through edits that only move or adjust
|
|
//! it, and loses it when it is retyped (the writer then decides from the formula's shape).
|
|
|
|
use std::sync::Arc;
|
|
|
|
use gridcraft_engine::core::{CellRef, Value};
|
|
use gridcraft_engine::model::{Cell, Formula, Workbook};
|
|
use gridcraft_engine::{DocState, Session, io};
|
|
use serde_json::json;
|
|
|
|
fn at(s: &str) -> CellRef {
|
|
CellRef::parse(s).unwrap()
|
|
}
|
|
|
|
/// A workbook saved by GridCraft whose D1 formula came from a file with `cm`.
|
|
fn opened() -> Session {
|
|
let mut wb = Workbook::new();
|
|
let sh = wb.sheet_mut(0).unwrap();
|
|
for r in 0..3 {
|
|
sh.set_value(CellRef::new(r, 1), Value::Number(f64::from(r + 1)));
|
|
sh.set_value(CellRef::new(r, 2), Value::Number(f64::from(r + 2)));
|
|
}
|
|
let mut f = Formula::new("SUMPRODUCT(B1:B3*C1:C3)");
|
|
f.dynamic = true;
|
|
sh.cells.set(at("D1"), Cell::formula(f));
|
|
let bytes = io::save_bytes(&wb, "dynamic.xlsx").unwrap();
|
|
let (back, _) = io::open_bytes("dynamic.xlsx", &bytes).unwrap();
|
|
let mut s = Session::new();
|
|
s.add_document(DocState::new(back, None, "dynamic".into()));
|
|
s
|
|
}
|
|
|
|
fn formula(s: &Session, cell: &str) -> Arc<Formula> {
|
|
s.doc().unwrap().wb.sheet(0).unwrap().cell(at(cell)).unwrap().formula.clone().unwrap()
|
|
}
|
|
|
|
/// Whether `cell` of the saved file carries dynamic-array metadata.
|
|
fn saved_dynamic(s: &Session, cell: &str) -> bool {
|
|
let bytes = io::save_bytes(&s.doc().unwrap().wb, "dynamic.xlsx").unwrap();
|
|
let (back, _) = io::open_bytes("dynamic.xlsx", &bytes).unwrap();
|
|
back.sheet(0).unwrap().cell(at(cell)).unwrap().formula.as_ref().unwrap().dynamic
|
|
}
|
|
|
|
#[test]
|
|
fn opened_dynamic_formula_keeps_its_metadata_on_save() {
|
|
let s = opened();
|
|
assert!(formula(&s, "D1").dynamic);
|
|
assert_eq!(s.doc().unwrap().wb.sheet(0).unwrap().value(at("D1")), Value::Number(20.0));
|
|
assert!(saved_dynamic(&s, "D1"));
|
|
}
|
|
|
|
#[test]
|
|
fn inserting_rows_and_moving_keep_the_mark() {
|
|
let mut s = opened();
|
|
s.execute("home.insertRows", json!({"rows": "1:1"})).unwrap();
|
|
let f = formula(&s, "D2");
|
|
assert!(f.dynamic);
|
|
assert_eq!(f.text, "SUMPRODUCT(B2:B4*C2:C4)");
|
|
assert!(saved_dynamic(&s, "D2"));
|
|
s.execute("selection.set", json!({"range": "D2"})).unwrap();
|
|
s.execute("edit.cut", json!({})).unwrap();
|
|
s.execute("selection.set", json!({"range": "F2"})).unwrap();
|
|
s.execute("edit.paste", json!({})).unwrap();
|
|
assert!(formula(&s, "F2").dynamic);
|
|
assert!(saved_dynamic(&s, "F2"));
|
|
}
|
|
|
|
#[test]
|
|
fn retyped_formula_drops_the_mark() {
|
|
let mut s = opened();
|
|
s.execute("cell.set", json!({"cell": "D1", "input": "=SUMPRODUCT(B1:B3*C1:C3)"})).unwrap();
|
|
assert!(!formula(&s, "D1").dynamic);
|
|
// SUMPRODUCT evaluates its arguments as arrays anyway: saved as a legacy formula.
|
|
assert!(!saved_dynamic(&s, "D1"));
|
|
}
|
|
|
|
#[test]
|
|
fn dynamic_mark_is_serialized_only_when_set() {
|
|
let s = opened();
|
|
let dynamic = serde_json::to_value(&*formula(&s, "D1")).unwrap();
|
|
assert_eq!(dynamic["dynamic"], json!(true));
|
|
let plain = serde_json::to_value(Formula::new("SUM(A1:A3)")).unwrap();
|
|
assert!(plain.get("dynamic").is_none(), "{plain}");
|
|
// Documents written before the field existed still load.
|
|
let old: Formula = serde_json::from_value(json!({"text": "SUM(A1:A3)", "array": null})).unwrap();
|
|
assert!(!old.dynamic);
|
|
}
|