Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
],
"config": {
"platform": {
"php" : "8.1.99"
"php": "8.1.99"
},
"process-timeout": 600,
"sort-packages": true,
Expand Down Expand Up @@ -120,7 +120,8 @@
"autoload-dev": {
"psr-4": {
"PhpOffice\\PhpSpreadsheetTests\\": "tests/PhpSpreadsheetTests",
"PhpOffice\\PhpSpreadsheetInfra\\": "infra"
"PhpOffice\\PhpSpreadsheetInfra\\": "infra",
"PhpOffice\\PhpSpreadsheetBenchmarks\\": "tests/Benchmark"
}
}
}
65 changes: 65 additions & 0 deletions src/PhpSpreadsheet/Collection/Cells.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,71 @@ public function getSortedCoordinates(): array
return $this->indexKeysCache;
}

/**
* Get coordinates of cells at or after a given row and column boundary.
*
* Returns only those cell coordinates where row >= minRow AND column >= minCol.
* Uses the internal index to filter without loading each cell from cache.
*
* @param int $minRow Minimum row number (1-based)
* @param int $minCol Minimum column index (1-based)
*
* @return string[] Cell coordinates matching the criteria, sorted by row then column
*/
public function getCoordinatesInRange(int $minRow, int $minCol): array
{
$result = [];
$minRowIndex = ($minRow - 1) * AddressRange::MAX_COLUMN_INT;

foreach ($this->index as $coordinate => $indexValue) {
if ($indexValue < $minRowIndex + $minCol) {
// Quick reject: index is below the minimum possible value for (minRow, minCol).
// But a cell in a later row with a smaller column could still be below this,
// so we must also check per-component.
continue;
}
$row = (int) floor(($indexValue - 1) / AddressRange::MAX_COLUMN_INT) + 1;
if ($row < $minRow) {
continue;
}
$col = ($indexValue % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
if ($col < $minCol) {
continue;
}
$result[$coordinate] = $indexValue;
}

asort($result);

return array_keys($result);
}

/**
* Get coordinates of cells outside a given row and column boundary.
*
* Returns only those cell coordinates where row < minRow OR column < minCol.
* This is the complement of getCoordinatesInRange().
*
* @param int $minRow Minimum row number (1-based)
* @param int $minCol Minimum column index (1-based)
*
* @return string[] Cell coordinates matching the criteria
*/
public function getCoordinatesOutsideRange(int $minRow, int $minCol): array
{
$result = [];

foreach ($this->index as $coordinate => $indexValue) {
$row = (int) floor(($indexValue - 1) / AddressRange::MAX_COLUMN_INT) + 1;
$col = ($indexValue % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
if ($row < $minRow || $col < $minCol) {
$result[] = $coordinate;
}
}

return $result;
}

/**
* Get a sorted list of all cell coordinates currently held in the collection by index (16384*row+column).
*
Expand Down
112 changes: 57 additions & 55 deletions src/PhpSpreadsheet/ReferenceHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -478,74 +478,76 @@ public function insertNewBefore(
}
}

$allCoordinates = $worksheet->getCoordinates();
$cellCollection = $worksheet->getCellCollection();

// Get only cells in the affected range (row >= beforeRow AND col >= beforeColumn)
// instead of iterating ALL cells in the worksheet
$affectedCoordinates = $cellCollection->getCoordinatesInRange($beforeRow, $beforeColumn);
if ($remove) {
// It's faster to reverse and pop than to use unshift, especially with large cell collections
$allCoordinates = array_reverse($allCoordinates);
$affectedCoordinates = array_reverse($affectedCoordinates);
}

// Loop through cells, bottom-up, and change cell coordinate
while ($coordinate = array_pop($allCoordinates)) {
// Loop through affected cells and move them to their new coordinates
while ($coordinate = array_pop($affectedCoordinates)) {
$cell = $worksheet->getCell($coordinate);
$cellIndex = Coordinate::columnIndexFromString($cell->getColumn());

// Don't update cells that are being removed
if ($numberOfColumns < 0 && $cellIndex >= $beforeColumn + $numberOfColumns && $cellIndex < $beforeColumn) {
continue;
}
// Note: The "cells being removed" check (numberOfColumns < 0 && cellIndex < beforeColumn)
// is unnecessary here because getCoordinatesInRange() already guarantees col >= beforeColumn.
// Cells in the removal zone are handled by clearColumnStrips/clearRowStrips above.

// Should the cell be updated? Move value and cellXf index from one cell to another.
if (($cellIndex >= $beforeColumn) && ($cell->getRow() >= $beforeRow)) {
// New coordinate
$newColumn = $cellIndex + $numberOfColumns;
$newRow = $cell->getRow() + $numberOfRows;
if ($newColumn > 0 && $newRow > 0 && $newColumn <= AddressRange::MAX_COLUMN_INT && $newRow <= AddressRange::MAX_ROW) {
$newCoordinate = Coordinate::stringFromColumnIndex($newColumn) . $newRow;
// Update cell styles
$worksheet->getCell($newCoordinate)
->setXfIndex($cell->getXfIndex());

// Insert this cell at its new location
if ($cell->getDataType() === DataType::TYPE_FORMULA) {
// Formula should be adjusted
$worksheet->getCell($newCoordinate)
->setValue(
$this->updateFormulaReferences(
$cell->getValueString(),
$beforeCellAddress,
$numberOfColumns,
$numberOfRows,
$worksheet->getTitle(),
true
)
);
} else {
// Cell value should not be adjusted
$worksheet->getCell($newCoordinate)
->setValueExplicit($cell->getValue(), $cell->getDataType());
}
}
// New coordinate
$newColumn = $cellIndex + $numberOfColumns;
$newRow = $cell->getRow() + $numberOfRows;
if ($newColumn > 0 && $newRow > 0 && $newColumn <= AddressRange::MAX_COLUMN_INT && $newRow <= AddressRange::MAX_ROW) {
$newCoordinate = Coordinate::stringFromColumnIndex($newColumn) . $newRow;
// Update cell styles
$worksheet->getCell($newCoordinate)
->setXfIndex($cell->getXfIndex());

// Clear the original cell
$worksheet->getCellCollection()
->delete($coordinate);
} else {
/* We don't need to update styles for rows/columns before our insertion position,
but we do still need to adjust any formulae in those cells */
// Insert this cell at its new location
if ($cell->getDataType() === DataType::TYPE_FORMULA) {
// Formula should be adjusted
$cell->setValue(
$this->updateFormulaReferences(
$cell->getValueString(),
$beforeCellAddress,
$numberOfColumns,
$numberOfRows,
$worksheet->getTitle(),
true
)
);
$worksheet->getCell($newCoordinate)
->setValue(
$this->updateFormulaReferences(
$cell->getValueString(),
$beforeCellAddress,
$numberOfColumns,
$numberOfRows,
$worksheet->getTitle(),
true
)
);
} else {
// Cell value should not be adjusted
$worksheet->getCell($newCoordinate)
->setValueExplicit($cell->getValue(), $cell->getDataType());
}
}

// Clear the original cell
$cellCollection->delete($coordinate);
}

// For cells outside the affected range, only update formula references
// (they don't need to move, but their formulas may reference shifted cells)
$outsideCoordinates = $cellCollection->getCoordinatesOutsideRange($beforeRow, $beforeColumn);
foreach ($outsideCoordinates as $coordinate) {
$cell = $worksheet->getCell($coordinate);
if ($cell->getDataType() === DataType::TYPE_FORMULA) {
$cell->setValue(
$this->updateFormulaReferences(
$cell->getValueString(),
$beforeCellAddress,
$numberOfColumns,
$numberOfRows,
$worksheet->getTitle(),
true
)
);
}
}

// Duplicate styles for the newly inserted cells
Expand Down
Loading
Loading