xlsx: subtract the totals row count, not the header row count, from a table's range — and expose both - #718
Open
sjvrensburg wants to merge 1 commit into
Open
sjvrensburg wants to merge 1 commit into
sjvrensburg wants to merge 1 commit into
Conversation
… table's range — and expose both `Xlsx::read_tables()` shifts a declared table's `ref` range to exclude its header and totals rows before it becomes `Table::data()`. The line excluding the totals row subtracted `header_row_count` instead of `totals_row_count`. For the overwhelmingly common shape — one header row, and either no totals row or one — both counts are small integers and no existing fixture disagreed, so this passed every test. It surfaces exactly when the counts differ: a table with no header row (`headerRowCount="0"`, "My table has headers" unchecked in Excel) and a totals row (`totalsRowCount="1"`) subtracts 0 instead of 1, leaving the fabricated totals row inside the data. Neither count reached a caller even when computed correctly. `Table<T>` carried `name`, `sheet_name`, `columns`, `data` and nothing else, so there was no way to ask a table whether it actually declares a header row — only whether `columns()` is non-empty, which is always true: Excel auto-names an unheaded table's columns (`Column1`, `Column2`, ...) rather than leaving them blank. A caller reading that as "has a header" would annex whatever real content sits above the table. `InnerTableMetadata`'s already-parsed counts are threaded through `Tables`, `TableMetadata`, `table_by_name` and `table_by_name_ref` onto two new fields on `Table<T>`, with public accessors `has_header_row()`/`has_totals_row()` alongside the existing `name()`/`sheet_name()`/`columns()`/`data()`. No existing fixture has `headerRowCount="0"` or a nonzero `totalsRowCount` — checked directly against every `.xlsx` fixture's `xl/tables/*.xml`. XLSX table XML is plain OOXML, so the regression test authors one on the fly with `rust_xlsxwriter` (new dev-dependency) rather than needing a checked-in fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related problems in
Xlsx::read_tables()(src/xlsx/mod.rs).The range bug. A declared table's
refrange is shifted to exclude itsheader and totals rows before becoming
Table::data():The
dims.endline subtractsheader_row_countinstead oftotals_row_count.For the overwhelmingly common shape — one header row, and either no totals row
or one — both counts are small integers that happen to agree, so this passes
every existing test. It surfaces exactly when the two differ: a table with
no header row (
headerRowCount="0", "My table has headers" unchecked inExcel) and a totals row (
totalsRowCount="1") subtracts 0 instead of 1,leaving the fabricated totals row inside
data(). The totals formula thenreads back as if it were one more row of data.
The missing accessors. Even computed correctly, neither count reached a
caller.
Table<T>carriedname,sheet_name,columnsanddata, so therewas no way to ask a table whether it actually declares a header row — only
whether
columns()is non-empty, which is always true, because Excelauto-names an unheaded table's columns (
Column1,Column2, …) rather thanleaving them empty. Downstream, a headerless table's auto-generated column
names get treated as if they were the row above the table.
The fix
InnerTableMetadata's already-parsedheader_row_count/totals_row_countarethreaded through
Tables,TableMetadata,table_by_nameandtable_by_name_refinto two new fields onTable<T>, with public accessorsTable::has_header_row()andTable::has_totals_row()(src/lib.rs)alongside the existing
name()/sheet_name()/columns()/data(). Thedims.endline now subtractstotals_row_count.Tests
Unlike my other branches, this one has a real regression test, because XLSX
table XML is plain OOXML and can be authored.
No existing fixture has
headerRowCount="0"or a nonzerototalsRowCount— Ichecked every
.xlsxfixture'sxl/tables/*.xmldirectly. Sotest_headerless_table_with_totals_row(tests/test.rs) writes one on the flywith
rust_xlsxwriter(added as a dev-dependency) rather than needing achecked-in binary: a headerless three-row table with a totals row, asserting
!has_header_row(),has_totals_row(), and thatdata()is exactly the threedata rows.
Reverting the
dims.endline toheader_row_countfails it —data.height()comes back
4, with the totals row counted as data.If you would rather not take a new dev-dependency, say so and I will author the
fixture bytes inline instead.
Independent of #712, #713 and my other branches; branched from
master.All 272 tests pass;
cargo fmt --checkandcargo clippy --all-targetsareclean.