Skip to content

feat: read merged cell ranges via Sheet.getMergedCells() - #634

Open
lowecg wants to merge 1 commit into
dhatim:masterfrom
lowecg:reader-merged-cells
Open

lowecg wants to merge 1 commit into
dhatim:masterfrom
lowecg:reader-merged-cells

Conversation

@lowecg

@lowecg lowecg commented Oct 3, 2026

Copy link
Copy Markdown

Fixes #633

This change adds Sheet.getMergedCells(). This method returns the merged cell ranges of a sheet as List<CellRangeAddress>.

How it works

In the sheet XML, <mergeCells> is immediately after <sheetData>. When RowSpliterator gets to the end of sheetData, it continues with the same reader. It reads each <mergeCell ref="..."> range and gives the list to the Sheet. The results are:

  • The reader parses the sheet one time only.
  • The reader does not compare cells with merged ranges. Rows are read as before.
  • getMergedCells() returns the ranges after all rows are read. Use Sheet.read() or read all of a stream from openStream().
  • If the rows are not all read, getMergedCells() throws IllegalStateException.

This method prevents the performance problems that #323 has.

Performance

I ran ReaderBenchmark in e2e (mvn test -Pbench) on master and on this branch. Each run has 15 single-shot samples.

Benchmark master this branch
fastExcelReader 0.444 ± 0.002 s/op 0.456 ± 0.002 s/op
streamingApachePoiWithStyles (this change has no effect on it) 1.178 ± 0.005 s/op 1.252 ± 0.004 s/op

The fastexcel difference is 2.6%. The POI difference between the two runs is 6.3%, but this change has no effect on POI. Thus, the fastexcel difference is less than the variation between runs. calendar_stress_test.xlsx has no merged cells. Thus, this benchmark measures the usual case.

I also tested a sheet with 20,000 rows, 20 columns and 2,000 merged ranges. I did not use JMH for this test. To read all rows took 147.3 ms before the change. It took 151.0 ms after the change, and this time includes the merged ranges.

Possibly, you do not want extra work at the end of each sheet. If so, I can make this feature optional with ReadingOptions.

Tests

  • FastExcelReaderTest.testFile compares getMergedCells() with getMergedRegions() from POI. It does this for all test files, which include merge_cells.xlsx.
  • MergedCellsTest tests these conditions:
    • a sheet with merged cells
    • a sheet with no merged cells
    • a call to getMergedCells() before the rows are read
    • a call to getMergedCells() after only some rows are read

Merged cells only hold a value in the first cell of the range, and the
reader had no way to discover the ranges. They are stored in <mergeCells>,
which follows <sheetData> in the sheet XML.

RowSpliterator now reads the ranges in the same pass once it reaches the
end of sheetData, so there is no second parse of the sheet and no per-cell
lookup. Sheet.getMergedCells() returns them once the rows have been read
to the end, and throws IllegalStateException before then.

FastExcelReaderTest compares the ranges with POI's getMergedRegions() for
every test file, and MergedCellsTest covers a sheet without merged cells
and reading the ranges before or partway through the rows.

Fixes dhatim#633

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.

Reader: expose merged cell ranges

1 participant