mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
68e58a2eae
commit
eb30ab8f3f
@@ -40,6 +40,7 @@ const MAX_ITEMS: usize = 1_048_576;
|
||||
const MAX_FIELDS: usize = 16_384;
|
||||
/// The `x14:dataField` extension (Excel 2010 show-values-as additions such as rank).
|
||||
const EXT_X14_DATA_FIELD: &str = "{E15A36E0-9728-4e99-A89B-3F7291B0FE68}";
|
||||
const EXT_X14_PIVOT_TABLE: &str = "{962EF5D1-5CA2-4c93-8EF4-DBF5C05439D2}";
|
||||
const NS_X14: &str = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main";
|
||||
|
||||
const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
@@ -850,7 +851,14 @@ fn table_xml(pt: &PivotTable, idx: usize, cache_id: u32, c: &Cache, fmt_id: &dyn
|
||||
} else {
|
||||
row_levels.max(1)
|
||||
};
|
||||
let _ = write!(s, "<location ref=\"{}\" firstHeaderRow=\"1\" firstDataRow=\"{first_data_row}\" firstDataCol=\"{first_data_col}\"", loc.a1());
|
||||
// Two or more value fields in Columns alone: Excel 365 would add a `Values` caption row.
|
||||
let hide_values_row = col_values.is_some() && col_idx.is_empty();
|
||||
let first_header_row = usize::from(!hide_values_row);
|
||||
let _ = write!(
|
||||
s,
|
||||
"<location ref=\"{}\" firstHeaderRow=\"{first_header_row}\" firstDataRow=\"{first_data_row}\" firstDataCol=\"{first_data_col}\"",
|
||||
loc.a1()
|
||||
);
|
||||
if !pages.is_empty() {
|
||||
let _ = write!(s, " rowPageCount=\"{}\" colPageCount=\"1\"", pages.len());
|
||||
}
|
||||
@@ -1007,6 +1015,12 @@ fn table_xml(pt: &PivotTable, idx: usize, cache_id: u32, c: &Cache, fmt_id: &dyn
|
||||
let _ = write!(s, " name=\"{}\"", esc_attr(&pt.style));
|
||||
}
|
||||
s.push_str(" showRowHeaders=\"1\" showColHeaders=\"1\" showRowStripes=\"0\" showColStripes=\"0\" showLastColumn=\"1\"/>");
|
||||
if hide_values_row {
|
||||
let _ = write!(
|
||||
s,
|
||||
"<extLst><ext uri=\"{EXT_X14_PIVOT_TABLE}\" xmlns:x14=\"{NS_X14}\"><x14:pivotTableDefinition hideValuesRow=\"1\"/></ext></extLst>"
|
||||
);
|
||||
}
|
||||
s.push_str("</pivotTableDefinition>");
|
||||
s
|
||||
}
|
||||
|
||||
@@ -674,3 +674,53 @@ fn data_on_rows_with_one_value_field_reads_without_warning() {
|
||||
let (back, _) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap();
|
||||
assert!(back.sheet(0).unwrap().pivots[0].values_on_rows);
|
||||
}
|
||||
|
||||
/// A one-sheet-of-data workbook with a pivot on "Report": `columns` are the source columns
|
||||
/// (header, values), `pt` the pivot built on `Data!A1:<last>`.
|
||||
fn small_pivot(columns: &[(&str, Vec<Value>)], pt: impl FnOnce(&mut PivotTable)) -> HashMap<String, String> {
|
||||
let mut wb = Workbook::new();
|
||||
let data = wb.sheet_mut(0).unwrap();
|
||||
data.name = "Data".into();
|
||||
let mut rows = 0;
|
||||
for (c, (h, vals)) in columns.iter().enumerate() {
|
||||
data.cells.set(CellRef::new(0, c as u32), Cell::value(Value::text(*h)));
|
||||
for (r, v) in vals.iter().enumerate() {
|
||||
if *v != Value::Empty {
|
||||
data.cells.set(CellRef::new(r as u32 + 1, c as u32), Cell::value(v.clone()));
|
||||
}
|
||||
}
|
||||
rows = rows.max(vals.len());
|
||||
}
|
||||
let last = format!("{}{}", gridcraft_core::col_to_letters(columns.len() as u32 - 1), rows + 1);
|
||||
let mut table = PivotTable { id: 1, name: "PT".into(), source: format!("Data!$A$1:{last}"), anchor: at("A3"), ..PivotTable::default() };
|
||||
pt(&mut table);
|
||||
let mut report = Sheet::new("Report");
|
||||
report.pivots = vec![table];
|
||||
wb.sheets.push(Arc::new(report));
|
||||
unzip(&write_xlsx(&wb).unwrap()).into_iter().map(|(k, v)| (k, String::from_utf8_lossy(&v).into_owned())).collect()
|
||||
}
|
||||
|
||||
/// #1447: two value fields and no column field: Excel 365 adds a `Values` row unless told not to.
|
||||
#[test]
|
||||
fn two_value_fields_without_columns_hide_the_values_row() {
|
||||
let cols = [
|
||||
("Region", vec![Value::text("East"), Value::text("West"), Value::text("East")]),
|
||||
("Sales", vec![Value::number(10.0), Value::number(20.0), Value::number(5.0)]),
|
||||
("Qty", vec![Value::number(1.0), Value::number(2.0), Value::number(3.0)]),
|
||||
];
|
||||
let value = |c: &str| PivotValue { source_col: c.into(), name: format!("Sum of {c}"), ..PivotValue::default() };
|
||||
let two = small_pivot(&cols, |p| {
|
||||
p.rows = vec![field("Region")];
|
||||
p.values = vec![value("Sales"), value("Qty")];
|
||||
});
|
||||
let t = &two["xl/pivotTables/pivotTable1.xml"];
|
||||
assert!(t.contains("firstHeaderRow=\"0\" firstDataRow=\"1\""), "{t}");
|
||||
assert!(t.contains("<x14:pivotTableDefinition hideValuesRow=\"1\"/>"), "{t}");
|
||||
let one = small_pivot(&cols, |p| {
|
||||
p.rows = vec![field("Region")];
|
||||
p.values = vec![value("Sales")];
|
||||
});
|
||||
let t = &one["xl/pivotTables/pivotTable1.xml"];
|
||||
assert!(t.contains("firstHeaderRow=\"1\""), "{t}");
|
||||
assert!(!t.contains("hideValuesRow"), "{t}");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-19 · **Change:** minor (.xls dialog sheets are skipped instead of read as worksheets; `.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas; XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; 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; 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; HTML export keeps right-to-left) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-19 · **Change:** minor (XLSX PivotTables with two value fields keep their layout in Excel (#1447); .xls dialog sheets are skipped instead of read as worksheets; `.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas; XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; 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; 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; HTML export keeps right-to-left) · **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**
|
||||
@@ -91,6 +91,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here.
|
||||
| 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-19 | minor | HTML (Save as Web Page and clipboard) marks right-to-left sheets and right-to-left cell text with `dir` |
|
||||
| 2026-10-16 | minor | XLSX PivotTables: two value fields without a column field are written with `firstHeaderRow="0"` and `hideValuesRow`, so Excel 365 doesn't add a `Values` row that can turn the report into `#SPILL!` (#1447, item 6) |
|
||||
| 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