mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 02:36:48 -06:00
Merge pull request #1882 from neilcawse:fix/pivot-grand-totals
# Conflicts: # docs/file-format-parity.md
This commit is contained in:
@@ -820,10 +820,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\">");
|
||||
@@ -1266,8 +1268,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"\""));
|
||||
@@ -640,6 +644,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>"#;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-19 · **Change:** minor (`.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-19 · **Change:** trivial (XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; 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
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-18 · **Change:** trivial (CSV/TSV/TXT can open with every field kept as text via `file.open {csvAsText}`; 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**
|
||||
@@ -92,6 +93,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here.
|
||||
| 2026-10-19 | minor | `.xls` import reads WINDOW2: right-to-left sheets (common in older Arabic and Hebrew files) open right-to-left, and hidden gridlines, headings and zero values and Show Formulas carry over |
|
||||
| 2026-10-19 | trivial | XLSX text boxes and shapes keep right-to-left paragraph direction (`a:pPr rtl`, ECMA-376 Part 1 §21.1.2.2.7): read from the first paragraph and written on every paragraph; output without it is unchanged |
|
||||
| 2026-10-18 | trivial | `file.open {csvAsText: true}` keeps every CSV/TSV/TXT field as the text the file holds, so identifiers (GTINs, SKUs, ZIP codes) keep their digits and leading zeros (#1808; a preference and a CLI flag are separate work) |
|
||||
| 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