mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
fix(xlsx): read and write PivotTable grand totals in Excel's direction
rowGrandTotals is the Grand Total row under the report and colGrandTotals the Grand Total column at its right. Both were mapped the other way round; a GridCraft round trip hid it, but an Excel file with only one grand total opened with the other one.
This commit is contained in:
@@ -816,10 +816,12 @@ fn table_xml(pt: &PivotTable, idx: usize, cache_id: u32, c: &Cache, fmt_id: &dyn
|
||||
if !pt.show_headers {
|
||||
s.push_str(" showHeaders=\"0\"");
|
||||
}
|
||||
if !pt.grand_totals_rows {
|
||||
// `rowGrandTotals` is the grand total row below the report (Excel's "On for Columns"),
|
||||
// `colGrandTotals` the grand total column at its right ("On for Rows").
|
||||
if !pt.grand_totals_cols {
|
||||
s.push_str(" rowGrandTotals=\"0\"");
|
||||
}
|
||||
if !pt.grand_totals_cols {
|
||||
if !pt.grand_totals_rows {
|
||||
s.push_str(" colGrandTotals=\"0\"");
|
||||
}
|
||||
s.push_str(" multipleFieldFilters=\"0\">");
|
||||
@@ -1262,8 +1264,8 @@ fn read_table(cx: &mut Ctx<'_>, x: &El, cache: &CacheIn, id: u32) -> Option<Pivo
|
||||
pt.subtotals = f.flag("defaultSubtotal", true);
|
||||
pt.subtotals_top = f.flag("subtotalTop", true);
|
||||
}
|
||||
pt.grand_totals_rows = x.flag("rowGrandTotals", true);
|
||||
pt.grand_totals_cols = x.flag("colGrandTotals", true);
|
||||
pt.grand_totals_rows = x.flag("colGrandTotals", true);
|
||||
pt.grand_totals_cols = x.flag("rowGrandTotals", true);
|
||||
pt.show_headers = x.flag("showHeaders", true);
|
||||
pt.style = x.child("pivotTableStyleInfo").and_then(|s| s.attr("name")).unwrap_or("").to_string();
|
||||
if x.child("calculatedItems").is_some() || x.path(&["filters", "filter"]).is_some() {
|
||||
|
||||
@@ -355,6 +355,10 @@ fn pivot_xml_details() {
|
||||
let t2 = String::from_utf8(parts.get("xl/pivotTables/pivotTable2.xml").unwrap().clone()).unwrap();
|
||||
assert!(t2.contains("pivotShowAs=\"rankDescending\""));
|
||||
assert!(t2.contains("showDataAs=\"runTotal\""));
|
||||
// Pivot 1 has no Grand Total column (`colGrandTotals`), pivot 2 no Grand Total row
|
||||
// (`rowGrandTotals`).
|
||||
assert!(t1.contains(" colGrandTotals=\"0\"") && !t1.contains("rowGrandTotals"), "{t1}");
|
||||
assert!(t2.contains(" rowGrandTotals=\"0\"") && !t2.contains("colGrandTotals"), "{t2}");
|
||||
// The custom value format is registered in the styles part.
|
||||
let styles = String::from_utf8(parts.get("xl/styles.xml").unwrap().clone()).unwrap();
|
||||
assert!(styles.contains("formatCode=\"0.0" u"\""));
|
||||
@@ -579,6 +583,25 @@ fn values_ahead_of_column_fields_read_last() {
|
||||
assert_eq!(pt.values_position, None);
|
||||
}
|
||||
|
||||
// As Excel saves them: `rowGrandTotals="0"` drops the Grand Total row under the report (leaving
|
||||
// Excel's "On for Rows Only"), `colGrandTotals="0"` the Grand Total column at its right ("On for
|
||||
// Columns Only"). LibreOffice's Pivot2_Row_Subtotals.xlsx (`rowGrandTotals="0"`) shows no Grand
|
||||
// Total row; ClosedXML's TemplateWithTableSourcePivotTables.xlsx pivotTable5 (`colGrandTotals="0"`,
|
||||
// row and column fields) shows a Grand Total row and no Grand Total column.
|
||||
#[test]
|
||||
fn grand_totals_read_in_excel_direction() {
|
||||
let table = |attr: &str| {
|
||||
format!(
|
||||
r#"<pivotTableDefinition name="GT" cacheId="1" {attr}><location ref="C1:F4"/><pivotFields><pivotField axis="axisRow"><items><item x="0"/><item x="1"/><item t="default"/></items></pivotField><pivotField dataField="1"/></pivotFields><rowFields><field x="0"/></rowFields><dataFields><dataField name="Sum of V" fld="1"/></dataFields></pivotTableDefinition>"#
|
||||
)
|
||||
};
|
||||
for (attr, rows, cols) in [("rowGrandTotals=\"0\"", true, false), ("colGrandTotals=\"0\"", false, true), ("", true, true)] {
|
||||
let (wb, _) = read_xlsx(&with_pivot(&table(attr), Some(GOOD_CACHE))).unwrap();
|
||||
let pt = &wb.sheet(0).unwrap().pivots[0];
|
||||
assert_eq!((pt.grand_totals_rows, pt.grand_totals_cols), (rows, cols), "{attr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_on_rows_with_one_value_field_reads_without_warning() {
|
||||
let table = r#"<pivotTableDefinition name="OneValue" cacheId="1" dataOnRows="1"><location ref="C1:D4"/><pivotFields><pivotField axis="axisRow"><items><item x="0"/><item x="1"/><item t="default"/></items></pivotField><pivotField dataField="1"/></pivotFields><rowFields><field x="0"/></rowFields><dataFields><dataField name="Total" fld="1"/></dataFields></pivotTableDefinition>"#;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-14 · **Change:** trivial (formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; previously trivial: XLSX chart sheets are kept on save (#727)) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-18 · **Change:** trivial (XLSX PivotTables read and write `rowGrandTotals`/`colGrandTotals` the way Excel does, so a table saved with only one grand total opens with the right one; previously trivial: formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
Every format Excel opens or saves, with GridCraft's read and write support, fidelity and tests.
|
||||
Part of [target-app-parity.md](target-app-parity.md). **File formats: ~50% ready, 80–120 h**
|
||||
@@ -85,6 +85,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-18 | trivial | XLSX PivotTables: `rowGrandTotals` is the Grand Total row and `colGrandTotals` the Grand Total column. Both were read and written the other way round, which a GridCraft round trip hid, so an Excel file with only one grand total opened showing the other |
|
||||
| 2026-10-14 | trivial | Formula parser: text longer than Excel's 8,192 characters, and postfix chains (`1%%%…`, `f(1)(1)…`) past the 256-level nesting limit, are parse errors (the cell keeps its formula text and cached value) instead of trees deep enough to overflow the stack when a hostile file is opened, saved or dropped |
|
||||
| 2026-10-12 | trivial | XLSX chart sheets are kept on save (#727) |
|
||||
| 2026-10-12 | trivial | CSV and TSV export is capped at 512 MiB and refuses a larger used range with an error (#528) |
|
||||
|
||||
Reference in New Issue
Block a user