diff --git a/crates/xlsx/src/pivot.rs b/crates/xlsx/src/pivot.rs index 609ae530e..920d12ae7 100644 --- a/crates/xlsx/src/pivot.rs +++ b/crates/xlsx/src/pivot.rs @@ -208,8 +208,13 @@ impl Stats { Value::Array(_) => self.add(&v.scalar()), } } + /// Excel counts booleans and errors as strings in the cache field flags. + fn strings(&self) -> bool { + self.text || self.boolean || self.error + } + /// Kinds for `containsMixedTypes`: strings (text, booleans, errors) and numbers. fn kinds(&self) -> usize { - [self.text, self.number, self.boolean, self.error].iter().filter(|b| **b).count() + usize::from(self.strings()) + usize::from(self.number) } } @@ -709,11 +714,10 @@ fn cache_xml(c: &Cache, records_rid: Option<&str>, sys: DateSystem) -> String { } let _ = g; } else { - let semi = st.text || st.blank || st.boolean || st.error; - if !semi { + if !st.strings() && !st.blank { s.push_str(" containsSemiMixedTypes=\"0\""); } - if !st.text { + if !st.strings() { s.push_str(" containsString=\"0\""); } if st.blank { diff --git a/crates/xlsx/src/tests/pivot_tests.rs b/crates/xlsx/src/tests/pivot_tests.rs index 99a15d6e7..bb0adacc5 100644 --- a/crates/xlsx/src/tests/pivot_tests.rs +++ b/crates/xlsx/src/tests/pivot_tests.rs @@ -402,6 +402,67 @@ fn large_cache_is_written_without_records() { assert_eq!(back.sheet(0).unwrap().pivots.len(), 1); } +/// #1447 item 1: Excel counts booleans and errors as strings in ``; writing +/// `containsString="0"` for them makes a file Excel can't open. +#[test] +fn boolean_and_error_cache_flags_match_excel() { + let mut wb = Workbook::new(); + let s = wb.sheet_mut(0).unwrap(); + let na = || Value::Error(gridcraft_core::CellError::NA); + let cols: [(&str, [Value; 3]); 7] = [ + ("K", [Value::text("a"), Value::text("b"), Value::text("a")]), + ("BoolOnly", [Value::Bool(true), Value::Bool(false), Value::Bool(true)]), + ("NumBool", [Value::number(1.0), Value::Bool(true), Value::number(2.5)]), + ("ErrOnly", [na(), na(), na()]), + ("NumErr", [Value::number(1.0), na(), Value::number(3.0)]), + ("TextBool", [Value::text("x"), Value::Bool(true), Value::text("y")]), + ("BoolBlank", [Value::Bool(true), Value::Empty, Value::Bool(false)]), + ]; + for (c, (h, vals)) in cols.iter().enumerate() { + s.cells.set(CellRef::new(0, c as u32), Cell::value(Value::text(*h))); + for (r, v) in vals.iter().enumerate() { + if !matches!(v, Value::Empty) { + s.cells.set(CellRef::new(r as u32 + 1, c as u32), Cell::value(v.clone())); + } + } + } + s.pivots.push(PivotTable { + id: 1, + source: "Sheet1!$A$1:$G$4".into(), + anchor: at("J1"), + rows: vec![field("K")], + values: vec![PivotValue { source_col: "NumBool".into(), name: "Sum of NumBool".into(), ..PivotValue::default() }], + last_range: Some(rr("J1:K4")), + ..PivotTable::default() + }); + let bytes = write_xlsx(&wb).unwrap(); + let parts = unzip(&bytes); + let c = String::from_utf8(parts.get("xl/pivotCache/pivotCacheDefinition1.xml").unwrap().clone()).unwrap(); + let start = |name: &str| -> String { + let i = c.find(&format!("').unwrap(); + c[j..=k].trim_end_matches('>').trim_end_matches('/').to_string() + }; + let flags = |name: &str| -> String { + let t = start(name); + // Drop the trailing count attribute, which depends on itemization. + match t.find(" count=") { + Some(i) => t[..i].to_string(), + None => t, + } + }; + assert_eq!(flags("BoolOnly"), ""#; const PT_CACHE_RELS: &str = r#""#;