Skip to content

fix(xlsx): write the cells of a table's total row - #615

Open
ronistrom wants to merge 1 commit into
productdevbook:mainfrom
ronistrom:fix/table-total-row-cells
Open

ronistrom wants to merge 1 commit into
productdevbook:mainfrom
ronistrom:fix/table-total-row-cells

Conversation

@ronistrom

@ronistrom ronistrom commented Oct 20, 2026 •

Copy link
Copy Markdown

Closes #604

What was wrong

A table with showTotalRow: true got its total row only in the table part (totalsRowLabel, totalsRowFunction, totalsRowFormula in xl/tables/table1.xml). The sheet's total row had no cells at all.

await writeXlsx({
  sheets: [{
    name: "Sheet1",
    rows: [["Name", "Qty"], ["a", 1], ["b", 2], ["c", 3]],
    tables: [{
      name: "Table1", range: "A1:B5", showTotalRow: true,
      columns: [
        { name: "Name", totalLabel: "Total" },
        { name: "Qty", totalFunction: "custom", totalFormula: "SUM(Table1[Qty])*2" },
      ],
    }],
  }],
})
<!-- before: sheet1.xml has no row 5 -->
<!-- after -->
<row r="5"><c r="A5" t="s"><v>5</v></c><c r="B5"><f>SUM(Table1[Qty])*2</f></c></row>

In Excel for Mac 16:

  • totalFunction: "custom": Excel asks to repair the file, whether or not the formula uses newer functions. The repair deletes the total row, and the table becomes A1:B4.
  • totalFunction: "sum": the file opens, but row 5 is empty. Neither the label nor the sum shows.

Excel writes a total row twice. The table part describes it, and the sheet holds it as ordinary cells, which is what Excel reads. Here is what Excel for Mac wrote for the same table:

<c r="A5" t="s"><v>5</v></c>                                    <!-- "Total" -->
<c r="B5"><f>SUBTOTAL(109,Table1[Qty])</f><v>6</v></c>           <!-- totalsRowFunction="sum" -->
<c r="B5"><f>SUM(Table1[Qty])*2</f><v>12</v></c>                 <!-- custom -->
<c r="B5"><f>_xlfn.XLOOKUP("c",Table1[Name],Table1[Qty])</f>…    <!-- custom, newer function -->

With a newer function, the _xlfn. prefix is in both <totalsRowFormula> and the cell. #597 couldn't confirm in Excel that its total-row prefix worked, because any custom total row already needed repair.

Why the tests did not catch it

They checked the table part's XML and round-tripped it through hucre's own reader. That reader reads totalsRowFunction and totalsRowLabel back from the table part, so the round trip looked right. Nothing looked at the sheet's total row, and nothing opened the file in Excel. The one existing total-row test in tables.test.ts hid the problem: its rows already include a hand-written ["Total", 30] row.

What landed

  • table-writer.ts has a new totalRowCells(table, range) that returns the cells Excel writes:

    • the label as a string;
    • a built-in function as SUBTOTAL(n,Name[Column]), with Excel's function numbers (average 101, countNums 102, count 103, max 104, min 105, stdDev 107, sum 109, var 110);
    • a custom total as its formula.

    The reference uses the table's displayName, as Excel does: a table named "Odd Tbl" becomes Odd_Tbl[...]. [ ] # ' in a column name are escaped with ', so a[b] becomes Odd_Tbl[a'[b']] and it's becomes [it''s]. Every one of these forms was checked against what Excel wrote.

  • worksheet-writer.ts: resolveRows merges these cells into the grid. It places them on the last row of the table's range, or of the computed range when there is none. Both writeXlsx and saveXlsx go through resolveRows.

  • A total-row cell the caller already filled is kept. A workbook read back from Excel already has these cells, with cached results. openXlsx → saveXlsx on Excel's own files still writes row 5 once, with its <v>.

  • No cached value is written. hucre always writes fullCalcOnLoad, so Excel computes the total when it opens the file.

  • docs/PARITY.md has a new section, "A table's total row comes back as cells". After a write and a read, the total row shows up in rows and cells even when the caller never wrote it.

Tests: six new ones in test/tables.test.ts. Four fail against main:

  • a built-in function writes the label and SUBTOTAL(109,Table1[Qty])
  • a custom total writes its formula into the cell
  • every function's SUBTOTAL number, the displayName and the escaped column names, for a table placed at C3
  • the total row lands below the data when the range is computed

The two that pass on main check what must not change: a total-row cell the caller filled is kept, and nothing is written for a column with no total or for a table without showTotalRow. bun run test passes, with 240 files and 10,704 tests.

Checked in Excel

All in Excel for Mac 16. "Opens cleanly" means no repair prompt.

file result
main, custom total repair prompt; the repair deletes the total row
main, sum total opens; row 5 empty
this branch, sum total opens cleanly; Total / 6. Excel reports the column's totals calculation as sum
this branch, custom total opens cleanly; Total / 12. totals calculation is custom
this branch, table at C3 with all eight functions, a label and the escaped names above opens cleanly; every total computed
this branch merged with #597's branch, custom XLOOKUP total opens cleanly; shows 3

The last row works because #597 adds the prefix when it writes a cell, so the total-row cell gets _xlfn. with no code here. The two branches merge without conflicts.

Not verified

  • Windows Excel, LibreOffice and Google Sheets. None was opened. I checked in Excel for Mac only.
  • writeXlsxStream. It does not write tables at all, so it is unchanged.
  • A table without a header row. TableDefinition has no option for one, so I didn't try it.
  • A total-row cell the caller filled with something other than the table's total. It is kept as the caller wrote it, and I did not check what Excel makes of a total row whose cell disagrees with totalsRowFunction.

Summary by CodeRabbit

  • New Features

    • Table total rows are now written directly into worksheet cells.
    • Built-in totals use appropriate SUBTOTAL formulas, while custom totals retain their specified formulas.
    • Total rows are generated correctly for calculated table ranges, including labels where applicable.
    • Existing caller-provided total-row values, formulas, and rich text remain unchanged.
  • Documentation

    • Documented how authored table total rows are represented in worksheets.

The total row of a table lived only in the table part: totalsRowLabel,
totalsRowFunction and totalsRowFormula, with nothing in the sheet. Excel
writes the row twice, in the table part and as ordinary cells, and reads
the cells. Excel for Mac showed an empty total row for a built-in
function, and asked to repair any table with a custom total, then
dropped the total row.

The worksheet writer now fills the total row the way Excel does: the
label as a string, a built-in function as SUBTOTAL(10x,Name[Column]) on
the displayName with [ ] # ' escaped, a custom total as its formula. A
cell the caller already filled is left alone, so a workbook read back
from Excel keeps its own cells and cached results. Those cells read back
as ordinary cells; docs/PARITY.md says so.

Four of the six new tests fail against main.

Closes productdevbook#604
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e1a771d4-458c-42be-9b4d-88ef060d190b
📥 Commits

Reviewing files that changed from the base of the PR and between c991ad0 and 3d7f5e2.

📒 Files selected for processing: 4
  • docs/PARITY.md
  • src/xlsx/table-writer.ts
  • src/xlsx/worksheet-writer.ts
  • test/tables.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 5 remain after this review.

📝 Walkthrough

Table total rows now produce worksheet cells for labels and formulas. The worksheet writer preserves existing populated cells and fills empty cells with generated values.

Changes: Table total-row serialization

Layer / File(s) Summary
Generate total-row cells
src/xlsx/table-writer.ts
Adds total-row cell generation for labels, built-in SUBTOTAL formulas, and custom formulas. Structured-reference column names escape special characters.
Materialize totals in worksheet rows
src/xlsx/worksheet-writer.ts, test/tables.test.ts, docs/PARITY.md
The worksheet writer adds generated cells to empty total-row cells and preserves populated cells. Tests cover formula generation, ranges, escaping, and preservation. Documentation describes serialization and readback.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: productdevbook

Merge Risk: ⚪ Minimal · up to 3d7f5

Tables with a total row now get their worksheet cells written, which addresses the repair-prompt and empty-total problem. No actionable merge-blocking risk remains from the supplied evidence.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 3 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: writing worksheet cells for a table's total row.
Linked Issues check Passed The changes meet the coding requirements in [#604]. totalRowCells generates label cells, built-in SUBTOTAL formulas with the Excel 10x function numbers, escaped structured-reference column names, …
Out of Scope Changes check Passed The changes stay within [#604]. The worksheet and table-writer changes implement total-row cell generation and preservation. The tests verify the implementation. The parity documentation describes the…
Full details: Linked Issues check

Explanation

The changes meet the coding requirements in [#604]. totalRowCells generates label cells, built-in SUBTOTAL formulas with the Excel 10x function numbers, escaped structured-reference column names, and custom formula cells. resolveRows merges these cells into the table's final row, including computed ranges. Existing caller cells remain unchanged. The added tests cover these cases. The PR also reports clean Excel for Mac results for built-in and custom totals.

Full details: Out of Scope Changes check

Explanation

The changes stay within [#604]. The worksheet and table-writer changes implement total-row cell generation and preservation. The tests verify the implementation. The parity documentation describes the new round-trip behavior. No unrelated product behavior is introduced.

Full details: Docstring Coverage

Explanation

Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 3 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ronistrom

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A table's total row is empty in Excel, and a custom total makes Excel repair the file

1 participant