From ad31626088f42e280d3b8647edaa55092884cd82 Mon Sep 17 00:00:00 2001 From: driesva Date: Tue, 17 Feb 2026 20:56:54 +0100 Subject: [PATCH] perf: improve OdfTableRow cell count performance * Issue was mentioned in #25 * Do not iterate over all cells which is slow for Excel generated spreadsheets which have 16384 columns by default. * Rely on `number-columns-repeated` attribute to count the _real_ cells. * ATTN: this gives a different result for merged cells over multiple rows compared to the previous implementation. However, this count seems more in line with the actual content XML. --- .../odfdom/doc/table/OdfTableRow.java | 20 ++-- .../odfdom/doc/table/TableCellCountTest.java | 92 ++++++++++++++++++ .../TestExcelSpreadsheetTableCellCount.ods | Bin 0 -> 3083 bytes ...stLibreOfficeSpreadsheetTableCellCount.ods | Bin 0 -> 8463 bytes 4 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 odfdom/src/test/java/org/odftoolkit/odfdom/doc/table/TableCellCountTest.java create mode 100644 odfdom/src/test/resources/test-input/TestExcelSpreadsheetTableCellCount.ods create mode 100644 odfdom/src/test/resources/test-input/TestLibreOfficeSpreadsheetTableCellCount.ods diff --git a/odfdom/src/main/java/org/odftoolkit/odfdom/doc/table/OdfTableRow.java b/odfdom/src/main/java/org/odftoolkit/odfdom/doc/table/OdfTableRow.java index c2635583e..c186bf0a1 100644 --- a/odfdom/src/main/java/org/odftoolkit/odfdom/doc/table/OdfTableRow.java +++ b/odfdom/src/main/java/org/odftoolkit/odfdom/doc/table/OdfTableRow.java @@ -287,16 +287,18 @@ public OdfTableCell getCellByIndex(int index) { * @return the cell count */ public int getCellCount() { - OdfTable table = getTable(); - Set realCells = new HashSet<>(); - List coverList = - table.getCellCoverInfos(0, 0, table.getColumnCount() - 1, table.getRowCount() - 1); - int rowIndex = getRowIndex(); - for (int i = 0; i < table.getColumnCount(); i++) { - OdfTableCell cell = table.getOwnerCellByPosition(coverList, i, rowIndex); - realCells.add(cell); + // count cells by skipping covered-table-cell and taking into account number-columns-repeated attribute + int cellCount = 0; + for (Node node : new DomNodeList(maRowElement.getChildNodes())) { + if (node instanceof TableTableCellElement tableCell) { + if (tableCell.getTableNumberColumnsRepeatedAttribute() == null) { + cellCount++; + } else { + cellCount += tableCell.getTableNumberColumnsRepeatedAttribute(); + } + } } - return realCells.size(); + return cellCount; } /** diff --git a/odfdom/src/test/java/org/odftoolkit/odfdom/doc/table/TableCellCountTest.java b/odfdom/src/test/java/org/odftoolkit/odfdom/doc/table/TableCellCountTest.java new file mode 100644 index 000000000..4ab6b9ad5 --- /dev/null +++ b/odfdom/src/test/java/org/odftoolkit/odfdom/doc/table/TableCellCountTest.java @@ -0,0 +1,92 @@ +package org.odftoolkit.odfdom.doc.table; + +import static org.junit.Assert.assertEquals; +import static org.odftoolkit.odfdom.utils.ResourceUtilities.getAbsoluteInputPath; + +import junit.framework.AssertionFailedError; +import org.junit.Test; +import org.odftoolkit.odfdom.doc.OdfSpreadsheetDocument; + +public class TableCellCountTest { + + // The number of columns that Excel always uses. + // For example Excel puts if no cell values or covered cells + private static final int EXCEL_COLUMN_COUNT = 16384; + + @Test + public void verifyCellCountForLibreOfficeGeneratedSpreadsheet() { + // Spreadsheet created by LibreOffice on Mac version 25.8.4.2 (AARCH64) + try (OdfSpreadsheetDocument spreadsheet = loadSpreadsheetDocument("TestLibreOfficeSpreadsheetTableCellCount.ods")) { + OdfTable sheet = spreadsheet.getSpreadsheetTables().get(0); + assertEquals(3, sheet.getColumnCount()); + + // 3 cells merged so 2 covered cells + assertEquals(3 - 2 /* covered cells */, sheet.getRowByIndex(0).getCellCount()); + + // 2 cells merged so 1 covered cell + assertEquals(3 - 1 /* covered cell */, sheet.getRowByIndex(1).getCellCount()); + + // no merged cells + assertEquals(3, sheet.getRowByIndex(2).getCellCount()); + + // 2 cells merged over 2 rows (simplified XML): + // + // + // 1 + // + // + // + // 2 + // + // + // + // + // + // 2 + // + // + // so 1 covered cell in first row + assertEquals(3 - 1 /* covered cell */, sheet.getRowByIndex(3).getCellCount()); + // ... and 2 covered cells in second row + assertEquals(3 - 2 /* covered cells */, sheet.getRowByIndex(4).getCellCount()); + } + } + + @Test + public void verifyCellCountForExcelGeneratedSpreadsheet() { + // Spreadsheet created by Microsoft® Excel for Mac Version 16.106 (26020821) + try (OdfSpreadsheetDocument spreadsheet = loadSpreadsheetDocument("TestExcelSpreadsheetTableCellCount.ods")) { + OdfTable sheet = spreadsheet.getSpreadsheetTables().get(0); + assertEquals(EXCEL_COLUMN_COUNT, sheet.getColumnCount()); + + // 3 cells merged so 2 covered cells + assertEquals(EXCEL_COLUMN_COUNT - 2 /* covered cells */, sheet.getRowByIndex(0).getCellCount()); + + // 2 cells merged so 1 covered cell + assertEquals(EXCEL_COLUMN_COUNT - 1 /* covered cell */, sheet.getRowByIndex(1).getCellCount()); + + // no merged cells + assertEquals(EXCEL_COLUMN_COUNT, sheet.getRowByIndex(2).getCellCount()); + + // 2 cells merged over 2 rows so 1 covered cell in first row + assertEquals(EXCEL_COLUMN_COUNT - 1 /* covered cell */, sheet.getRowByIndex(3).getCellCount()); + // ... and 2 covered cells in second row + assertEquals(EXCEL_COLUMN_COUNT - 2 /* covered cells */, sheet.getRowByIndex(4).getCellCount()); + + // Excel always adds + // + // + // + assertEquals(EXCEL_COLUMN_COUNT, sheet.getRowByIndex(5).getCellCount()); + } + } + + private OdfSpreadsheetDocument loadSpreadsheetDocument(String filename) { + try { + return OdfSpreadsheetDocument.loadDocument( + getAbsoluteInputPath(filename)); + } catch (Exception ex) { + throw new AssertionFailedError(ex.getMessage()); + } + } +} diff --git a/odfdom/src/test/resources/test-input/TestExcelSpreadsheetTableCellCount.ods b/odfdom/src/test/resources/test-input/TestExcelSpreadsheetTableCellCount.ods new file mode 100644 index 0000000000000000000000000000000000000000..8c4737fff2fb707959bf07b7131c54afbb324e8c GIT binary patch literal 3083 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 I&p&{F02_KrX8-^I literal 0 HcmV?d00001 diff --git a/odfdom/src/test/resources/test-input/TestLibreOfficeSpreadsheetTableCellCount.ods b/odfdom/src/test/resources/test-input/TestLibreOfficeSpreadsheetTableCellCount.ods new file mode 100644 index 0000000000000000000000000000000000000000..b194c237557d8fa646bda0b8bbcbb2bd375fb961 GIT binary patch literal 8463 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