XLSX: cache #SPILL! and #CALC! as #VALUE!

They are not error codes of the file format, and Excel refuses to open a
file whose cached values contain them ("Parameter error"). Any workbook
with a blocked spill could not be opened in Excel. Excel caches these
errors as #VALUE! itself.
This commit is contained in:
nihilist-narwhal 2026-10-09 17:14:59 -05:00
parent 51b531be3e
commit cd083cfc50
2 changed files with 30 additions and 0 deletions

View File

@ -555,7 +555,13 @@ fn write_cell(
};
let value = match value {
Value::Array(a) => a.get(0, 0).cloned().unwrap_or(Value::Empty),
v => v,
};
let value = match value {
Value::Error(CellError::Circ) => Value::Number(0.0),
// #SPILL! and #CALC! are not error codes of the file format (Excel refuses a file that
// caches them); Excel itself caches them as #VALUE!.
Value::Error(CellError::Spill | CellError::Calc) => Value::Error(CellError::Value),
v => v,
};
if formula.is_some() && dynamic.is_some() {

View File

@ -563,3 +563,27 @@ fn writing_repairs_invalid_models() {
assert_eq!(s.tables[0].name, "Table1");
assert_eq!(s.cell(at("B2")).unwrap().style, StyleId::DEFAULT);
}
fn sheet1_xml(bytes: &[u8]) -> (String, bool) {
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
let mut s = String::new();
z.by_name("xl/worksheets/sheet1.xml").unwrap().read_to_string(&mut s).unwrap();
let metadata = z.by_name("xl/metadata.xml").is_ok();
(s, metadata)
}
/// Excel refuses to open a file whose cached values include `#SPILL!` or `#CALC!`.
#[test]
fn spill_and_calc_errors_are_cached_as_value_errors() {
let mut wb = Workbook::new();
let s = wb.sheet_mut(0).unwrap();
s.set_value(at("A1"), Value::Number(1.0));
s.cells.set(at("C1"), Cell { value: Value::Error(CellError::Spill), ..Cell::formula(Formula::new("A1:A3*2")) });
s.cells.set(at("C2"), Cell { value: Value::Error(CellError::Calc), ..Cell::formula(Formula::new("FILTER(A1:A3,A1:A3>5)")) });
s.cells.set(at("C3"), Cell { value: Value::Error(CellError::NA), ..Cell::formula(Formula::new("NA()")) });
let (xml, _) = sheet1_xml(&write_xlsx(&wb).unwrap());
assert!(!xml.contains("#SPILL!") && !xml.contains("#CALC!"), "{xml}");
assert!(xml.contains(r#"<f>A1:A3*2</f><v>#VALUE!</v></c>"#), "{xml}");
assert!(xml.contains(r#"<f>_xlfn._xlws.FILTER(A1:A3,A1:A3&gt;5)</f><v>#VALUE!</v></c>"#), "{xml}");
assert!(xml.contains(r#"<c r="C3" t="e"><f>NA()</f><v>#N/A</v></c>"#), "{xml}");
}