diff --git a/crates/xlsx/src/sheet_write.rs b/crates/xlsx/src/sheet_write.rs
index 94d9b18..86999d1 100644
--- a/crates/xlsx/src/sheet_write.rs
+++ b/crates/xlsx/src/sheet_write.rs
@@ -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() {
diff --git a/crates/xlsx/src/tests/roundtrip.rs b/crates/xlsx/src/tests/roundtrip.rs
index bfc83b9..c70942f 100644
--- a/crates/xlsx/src/tests/roundtrip.rs
+++ b/crates/xlsx/src/tests/roundtrip.rs
@@ -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#"A1:A3*2#VALUE!"#), "{xml}");
+ assert!(xml.contains(r#"_xlfn._xlws.FILTER(A1:A3,A1:A3>5)#VALUE!"#), "{xml}");
+ assert!(xml.contains(r#"NA()#N/A"#), "{xml}");
+}