# tablepress/3.4/libraries/vendor/PhpSpreadsheet/ReferenceHelper.php

TablePress – Tables in WordPress made easy, version 3.4. 1,412 lines.

- Page: https://pluginprobe.com/plugins/tablepress/3.4/code/libraries/vendor/PhpSpreadsheet/ReferenceHelper.php
- Raw: https://pluginprobe.com/plugins/tablepress/3.4/raw/libraries/vendor/PhpSpreadsheet/ReferenceHelper.php
- Modified: 2026-04-07T08:40:50+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/tablepress/3.4/code/libraries/vendor/PhpSpreadsheet/ReferenceHelper.php#L10-L20`.

```php
<?php

namespace TablePress\PhpOffice\PhpSpreadsheet;

use TablePress\PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use TablePress\PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use TablePress\PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use TablePress\PhpOffice\PhpSpreadsheet\Cell\DataType;
use TablePress\PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use TablePress\PhpOffice\PhpSpreadsheet\Style\Conditional;
use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Table;
use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;

class ReferenceHelper
{
	/**    Constants                */
	/**    Regular Expressions      */
	private const SHEETNAME_PART = '((\w*|\'[^!]*\')!)';
	private const SHEETNAME_PART_WITH_SLASHES = '/' . self::SHEETNAME_PART . '/';
	const REFHELPER_REGEXP_CELLREF = self::SHEETNAME_PART . '?(?<![:a-z1-9_\.\$])(\$?[a-z]{1,3}\$?\d+)(?=[^:!\d\'])';
	const REFHELPER_REGEXP_CELLRANGE = self::SHEETNAME_PART . '?(\$?[a-z]{1,3}\$?\d+):(\$?[a-z]{1,3}\$?\d+)';
	const REFHELPER_REGEXP_ROWRANGE = self::SHEETNAME_PART . '?(\$?\d+):(\$?\d+)';
	const REFHELPER_REGEXP_COLRANGE = self::SHEETNAME_PART . '?(\$?[a-z]{1,3}):(\$?[a-z]{1,3})';

	/**
	 * Instance of this class.
	 */
	private static ?ReferenceHelper $instance = null;

	private ?CellReferenceHelper $cellReferenceHelper = null;

	/**
	 * Get an instance of this class.
	 */
	public static function getInstance(): self
	{
		if (self::$instance === null) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Create a new ReferenceHelper.
	 */
	protected function __construct()
	{
	}

	/**
	 * Compare two column addresses
	 * Intended for use as a Callback function for sorting column addresses by column.
	 *
	 * @param string $a First column to test (e.g. 'AA')
	 * @param string $b Second column to test (e.g. 'Z')
	 */
	public static function columnSort(string $a, string $b): int
	{
		return strcasecmp(strlen($a) . $a, strlen($b) . $b);
	}

	/**
	 * Compare two column addresses
	 * Intended for use as a Callback function for reverse sorting column addresses by column.
	 *
	 * @param string $a First column to test (e.g. 'AA')
	 * @param string $b Second column to test (e.g. 'Z')
	 */
	public static function columnReverseSort(string $a, string $b): int
	{
		return -strcasecmp(strlen($a) . $a, strlen($b) . $b);
	}

	/**
	 * Compare two cell addresses
	 * Intended for use as a Callback function for sorting cell addresses by column and row.
	 *
	 * @param string $a First cell to test (e.g. 'AA1')
	 * @param string $b Second cell to test (e.g. 'Z1')
	 */
	public static function cellSort(string $a, string $b): int
	{
		sscanf($a, '%[A-Z]%d', $ac, $ar);
		/** @var int $ar */
		/** @var string $ac */
		sscanf($b, '%[A-Z]%d', $bc, $br);
		/** @var int $br */
		/** @var string $bc */
		if ($ar === $br) {
			return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
		}

		return ($ar < $br) ? -1 : 1;
	}

	/**
	 * Compare two cell addresses
	 * Intended for use as a Callback function for sorting cell addresses by column and row.
	 *
	 * @param string $a First cell to test (e.g. 'AA1')
	 * @param string $b Second cell to test (e.g. 'Z1')
	 */
	public static function cellReverseSort(string $a, string $b): int
	{
		sscanf($a, '%[A-Z]%d', $ac, $ar);
		/** @var int $ar */
		/** @var string $ac */
		sscanf($b, '%[A-Z]%d', $bc, $br);
		/** @var int $br */
		/** @var string $bc */
		if ($ar === $br) {
			return -strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
		}

		return ($ar < $br) ? 1 : -1;
	}

	/**
	 * Update page breaks when inserting/deleting rows/columns.
	 *
	 * @param Worksheet $worksheet The worksheet that we're editing
	 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
	 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
	 */
	protected function adjustPageBreaks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
	{
		$aBreaks = $worksheet->getBreaks();
		($numberOfColumns > 0 || $numberOfRows > 0)
			? uksort($aBreaks, [self::class, 'cellReverseSort'])
			: uksort($aBreaks, [self::class, 'cellSort']);

		foreach ($aBreaks as $cellAddress => $value) {
			/** @var CellReferenceHelper */
			$cellReferenceHelper = $this->cellReferenceHelper;
			if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) {
				//    If we're deleting, then clear any defined breaks that are within the range
				//        of rows/columns that we're deleting
				$worksheet->setBreak($cellAddress, Worksheet::BREAK_NONE);
			} else {
				//    Otherwise update any affected breaks by inserting a new break at the appropriate point
				//        and removing the old affected break
				$newReference = $this->updateCellReference($cellAddress);
				if ($cellAddress !== $newReference) {
					$worksheet->setBreak($newReference, $value)
						->setBreak($cellAddress, Worksheet::BREAK_NONE);
				}
			}
		}
	}

	/**
	 * Update cell comments when inserting/deleting rows/columns.
	 *
	 * @param Worksheet $worksheet The worksheet that we're editing
	 */
	protected function adjustComments(Worksheet $worksheet): void
	{
		$aComments = $worksheet->getComments();
		$aNewComments = []; // the new array of all comments

		foreach ($aComments as $cellAddress => &$value) {
			// Any comments inside a deleted range will be ignored
			/** @var CellReferenceHelper */
			$cellReferenceHelper = $this->cellReferenceHelper;
			if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === false) {
				// Otherwise build a new array of comments indexed by the adjusted cell reference
				$newReference = $this->updateCellReference($cellAddress);
				$aNewComments[$newReference] = $value;
			}
		}
		//    Replace the comments array with the new set of comments
		$worksheet->setComments($aNewComments);
	}

	/**
	 * Update hyperlinks when inserting/deleting rows/columns.
	 *
	 * @param Worksheet $worksheet The worksheet that we're editing
	 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
	 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
	 */
	protected function adjustHyperlinks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
	{
		$aHyperlinkCollection = $worksheet->getHyperlinkCollection();
		($numberOfColumns > 0 || $numberOfRows > 0)
			? uksort($aHyperlinkCollection, [self::class, 'cellReverseSort'])
			: uksort($aHyperlinkCollection, [self::class, 'cellSort']);

		foreach ($aHyperlinkCollection as $cellAddress => $value) {
			$newReference = $this->updateCellReference($cellAddress);
			/** @var CellReferenceHelper */
			$cellReferenceHelper = $this->cellReferenceHelper;
			if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) {
				$worksheet->setHyperlink($cellAddress, null);
			} elseif ($cellAddress !== $newReference) {
				$worksheet->setHyperlink($cellAddress, null);
				if ($newReference) {
					$worksheet->setHyperlink($newReference, $value);
				}
			}
		}
	}

	/**
	 * Update conditional formatting styles when inserting/deleting rows/columns.
	 *
	 * @param Worksheet $worksheet The worksheet that we're editing
	 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
	 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
	 */
	protected function adjustConditionalFormatting(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
	{
		$aStyles = $worksheet->getConditionalStylesCollection();
		($numberOfColumns > 0 || $numberOfRows > 0)
			? uksort($aStyles, [self::class, 'cellReverseSort'])
			: uksort($aStyles, [self::class, 'cellSort']);

		foreach ($aStyles as $cellAddress => $cfRules) {
			$worksheet->removeConditionalStyles($cellAddress);
			$newReference = $this->updateCellReference($cellAddress);

			foreach ($cfRules as &$cfRule) {
				/** @var Conditional $cfRule */
				$conditions = $cfRule->getConditions();
				foreach ($conditions as &$condition) {
					if (is_string($condition)) {
						/** @var CellReferenceHelper */
						$cellReferenceHelper = $this->cellReferenceHelper;
						$condition = $this->updateFormulaReferences(
							$condition,
							$cellReferenceHelper->beforeCellAddress(),
							$numberOfColumns,
							$numberOfRows,
							$worksheet->getTitle(),
							true
						);
					}
				}
				$cfRule->setConditions($conditions);
			}
			$worksheet->setConditionalStyles($newReference, $cfRules);
		}
	}

	/**
	 * Update data validations when inserting/deleting rows/columns.
	 *
	 * @param Worksheet $worksheet The worksheet that we're editing
	 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
	 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
	 */
	protected function adjustDataValidations(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows, string $beforeCellAddress): void
	{
		$aDataValidationCollection = $worksheet->getDataValidationCollection();
		($numberOfColumns > 0 || $numberOfRows > 0)
			? uksort($aDataValidationCollection, [self::class, 'cellReverseSort'])
			: uksort($aDataValidationCollection, [self::class, 'cellSort']);

		foreach ($aDataValidationCollection as $cellAddress => $dataValidation) {
			$formula = $dataValidation->getFormula1();
			if ($formula !== '') {
				$dataValidation->setFormula1(
					$this->updateFormulaReferences(
						$formula,
						$beforeCellAddress,
						$numberOfColumns,
						$numberOfRows,
						$worksheet->getTitle(),
						true
					)
				);
			}
			$formula = $dataValidation->getFormula2();
			if ($formula !== '') {
				$dataValidation->setFormula2(
					$this->updateFormulaReferences(
						$formula,
						$beforeCellAddress,
						$numberOfColumns,
						$numberOfRows,
						$worksheet->getTitle(),
						true
					)
				);
			}
			$addressParts = explode(' ', $cellAddress);
			$newReference = '';
			$separator = '';
			foreach ($addressParts as $addressPart) {
				$newReference .= $separator . $this->updateCellReference($addressPart);
				$separator = ' ';
			}
			if ($cellAddress !== $newReference) {
				$worksheet->setDataValidation($newReference, $dataValidation);
				$worksheet->setDataValidation($cellAddress, null);
				if ($newReference) {
					$worksheet->setDataValidation($newReference, $dataValidation);
				}
			}
		}
	}

	/**
	 * Update merged cells when inserting/deleting rows/columns.
	 *
	 * @param Worksheet $worksheet The worksheet that we're editing
	 */
	protected function adjustMergeCells(Worksheet $worksheet): void
	{
		$aMergeCells = $worksheet->getMergeCells();
		$aNewMergeCells = []; // the new array of all merge cells
		foreach ($aMergeCells as $cellAddress => &$value) {
			$newReference = $this->updateCellReference($cellAddress);
			if ($newReference) {
				$aNewMergeCells[$newReference] = $newReference;
			}
		}
		$worksheet->setMergeCells($aNewMergeCells); // replace the merge cells array
	}

	/**
	 * Update protected cells when inserting/deleting rows/columns.
	 *
	 * @param Worksheet $worksheet The worksheet that we're editing
	 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
	 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
	 */
	protected function adjustProtectedCells(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
	{
		$aProtectedCells = $worksheet->getProtectedCellRanges();
		/** @var CellReferenceHelper */
		$cellReferenceHelper = $this->cellReferenceHelper;
		if ($numberOfRows >= 0 && $numberOfColumns >= 0) {
			foreach ($aProtectedCells as $key2 => $value) {
				$ranges = $value->allRanges();
				$newKey = $separator = '';
				foreach ($ranges as $key => $range) {
					$oldKey = $range[0] . (array_key_exists(1, $range) ? (':' . $range[1]) : '');
					$newKey .= $separator . $this->updateCellReference($oldKey);
					$separator = ' ';
				}
				if ($key2 !== $newKey) {
					$worksheet->unprotectCells($key2);
					$worksheet->protectCells($newKey, $value->getPassword(), true, $value->getName(), $value->getSecurityDescriptor());
				}
			}
		} else {
			foreach ($aProtectedCells as $key2 => $value) {
				$range = str_replace([' ', ',', "\0"], ["\0", ' ', ','], $key2);
				$extracted = Coordinate::extractAllCellReferencesInRange($range);
				$outArray = [];
				foreach ($extracted as $cellAddress) {
					if (!$cellReferenceHelper->cellAddressInDeleteRange($cellAddress)) {
						$outArray[$this->updateCellReference($cellAddress)] = 'x';
					}
				}
				$outArray2 = Coordinate::mergeRangesInCollection($outArray);
				$newKey = implode(' ', array_keys($outArray2));
				if ($key2 !== $newKey) {
					$worksheet->unprotectCells($key2);
					$worksheet->protectCells($newKey, $value->getPassword(), true, $value->getName(), $value->getSecurityDescriptor());
				}
			}
		}
	}

	/**
	 * Update column dimensions when inserting/deleting rows/columns.
	 *
	 * @param Worksheet $worksheet The worksheet that we're editing
	 */
	protected function adjustColumnDimensions(Worksheet $worksheet): void
	{
		$aColumnDimensions = array_reverse($worksheet->getColumnDimensions(), true);
		if (!empty($aColumnDimensions)) {
			foreach ($aColumnDimensions as $objColumnDimension) {
				$newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1');
				[$newReference] = Coordinate::coordinateFromString($newReference);
				if ($objColumnDimension->getColumnIndex() !== $newReference) {
					$objColumnDimension->setColumnIndex($newReference);
				}
			}

			$worksheet->refreshColumnDimensions();
		}
	}

	/**
	 * Update row dimensions when inserting/deleting rows/columns.
	 *
	 * @param Worksheet $worksheet The worksheet that we're editing
	 * @param int $beforeRow Number of the row we're inserting/deleting before
	 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
	 */
	protected function adjustRowDimensions(Worksheet $worksheet, int $beforeRow, int $numberOfRows): void
	{
		$aRowDimensions = array_reverse($worksheet->getRowDimensions(), true);
		if (!empty($aRowDimensions)) {
			foreach ($aRowDimensions as $objRowDimension) {
				$newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex());
				[, $newReference] = Coordinate::coordinateFromString($newReference);
				$newRoweference = (int) $newReference;
				if ($objRowDimension->getRowIndex() !== $newRoweference) {
					$objRowDimension->setRowIndex($newRoweference);
				}
			}

			$worksheet->refreshRowDimensions();

			$copyDimension = $worksheet->getRowDimension($beforeRow - 1);
			for ($i = $beforeRow; $i <= $beforeRow - 1 + $numberOfRows; ++$i) {
				$newDimension = $worksheet->getRowDimension($i);
				$newDimension->setRowHeight($copyDimension->getRowHeight());
				$newDimension->setVisible($copyDimension->getVisible());
				$newDimension->setOutlineLevel($copyDimension->getOutlineLevel());
				$newDimension->setCollapsed($copyDimension->getCollapsed());
			}
		}
	}

	/**
	 * Insert a new column or row, updating all possible related data.
	 *
	 * @param string $beforeCellAddress Insert before this cell address (e.g. 'A1')
	 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
	 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
	 * @param Worksheet $worksheet The worksheet that we're editing
	 */
	public function insertNewBefore(
		string $beforeCellAddress,
		int $numberOfColumns,
		int $numberOfRows,
		Worksheet $worksheet
	): void {
		$remove = ($numberOfColumns < 0 || $numberOfRows < 0);

		if (
			$this->cellReferenceHelper === null
			|| $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows)
		) {
			$this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows);
		}

		// Get coordinate of $beforeCellAddress
		[$beforeColumn, $beforeRow, $beforeColumnString] = Coordinate::indexesFromString($beforeCellAddress);

		// Clear cells if we are removing columns or rows
		$highestColumn = $worksheet->getHighestColumn();
		$highestDataColumn = $worksheet->getHighestDataColumn();
		$highestRow = $worksheet->getHighestRow();
		$highestDataRow = $worksheet->getHighestDataRow();

		// 1. Clear column strips if we are removing columns
		if ($numberOfColumns < 0 && $beforeColumn - 2 + $numberOfColumns > 0) {
			$this->clearColumnStrips($highestRow, $beforeColumn, $numberOfColumns, $worksheet);
		}

		// 2. Clear row strips if we are removing rows
		if ($numberOfRows < 0 && $beforeRow - 1 + $numberOfRows > 0) {
			$this->clearRowStrips($highestColumn, $beforeColumn, $beforeRow, $numberOfRows, $worksheet);
		}

		// Find missing coordinates. This is important when inserting or deleting column before the last column
		$startRow = $startCol = 1;
		$startColString = 'A';
		if ($numberOfRows === 0) {
			$startCol = $beforeColumn;
			$startColString = $beforeColumnString;
		} elseif ($numberOfColumns === 0) {
			$startRow = $beforeRow;
		}
		$highColumn = Coordinate::columnIndexFromString($highestDataColumn);
		for ($row = $startRow; $row <= $highestDataRow; ++$row) {
			for ($col = $startCol, $colString = $startColString; $col <= $highColumn; ++$col, StringHelper::stringIncrement($colString)) {
				$worksheet->getCell("$colString$row"); // create cell if it doesn't exist
			}
		}

		$allCoordinates = $worksheet->getCoordinates();
		if ($remove) {
			// It's faster to reverse and pop than to use unshift, especially with large cell collections
			$allCoordinates = array_reverse($allCoordinates);
		}

		// Loop through cells, bottom-up, and change cell coordinate
		while ($coordinate = array_pop($allCoordinates)) {
			$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;
			}

			// 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());
					}
				}

				// 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                    */
				if ($cell->getDataType() === DataType::TYPE_FORMULA) {
					// Formula should be adjusted
					$cell->setValue(
						$this->updateFormulaReferences(
							$cell->getValueString(),
							$beforeCellAddress,
							$numberOfColumns,
							$numberOfRows,
							$worksheet->getTitle(),
							true
						)
					);
				}
			}
		}

		// Duplicate styles for the newly inserted cells
		$highestColumn = $worksheet->getHighestColumn();
		$highestRow = $worksheet->getHighestRow();

		if ($numberOfColumns > 0 && $beforeColumn > 1) {
			$this->duplicateStylesByColumn($worksheet, $beforeColumn, $beforeRow, $highestRow, $numberOfColumns);
		}

		if ($numberOfRows > 0 && $beforeRow - 1 > 0) {
			$this->duplicateStylesByRow($worksheet, $beforeColumn, $beforeRow, $highestColumn, $numberOfRows);
		}

		// Update worksheet: column dimensions
		$this->adjustColumnDimensions($worksheet);

		// Update worksheet: row dimensions
		$this->adjustRowDimensions($worksheet, $beforeRow, $numberOfRows);

		//    Update worksheet: page breaks
		$this->adjustPageBreaks($worksheet, $numberOfColumns, $numberOfRows);

		//    Update worksheet: comments
		$this->adjustComments($worksheet);

		// Update worksheet: hyperlinks
		$this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows);

		// Update worksheet: conditional formatting styles
		$this->adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows);

		// Update worksheet: data validations
		$this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows, $beforeCellAddress);

		// Update worksheet: merge cells
		$this->adjustMergeCells($worksheet);

		// Update worksheet: protected cells
		$this->adjustProtectedCells($worksheet, $numberOfColumns, $numberOfRows);

		// Update worksheet: autofilter
		$this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns);

		// Update worksheet: table
		$this->adjustTable($worksheet, $beforeCellAddress, $numberOfColumns);

		// Update worksheet: freeze pane
		if ($worksheet->getFreezePane()) {
			$splitCell = $worksheet->getFreezePane();
			$topLeftCell = $worksheet->getTopLeftCell() ?? '';

			$splitCell = $this->updateCellReference($splitCell);
			$topLeftCell = $this->updateCellReference($topLeftCell);

			$worksheet->freezePane($splitCell, $topLeftCell);
		}

		$this->updatePrintAreas($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);

		// Update worksheet: drawings
		$aDrawings = $worksheet->getDrawingCollection();
		foreach ($aDrawings as $objDrawing) {
			$newReference = $this->updateCellReference($objDrawing->getCoordinates());
			if ($objDrawing->getCoordinates() != $newReference) {
				$objDrawing->setCoordinates($newReference);
			}
			if ($objDrawing->getCoordinates2() !== '') {
				$newReference = $this->updateCellReference($objDrawing->getCoordinates2());
				if ($objDrawing->getCoordinates2() != $newReference) {
					$objDrawing->setCoordinates2($newReference);
				}
			}
		}

		// Update workbook: define names
		if (count($worksheet->getParentOrThrow()->getDefinedNames()) > 0) {
			$this->updateDefinedNames($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
		}

		// Garbage collect
		$worksheet->garbageCollect();
	}

	private function updatePrintAreas(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
	{
		$pageSetup = $worksheet->getPageSetup();
		if (!$pageSetup->isPrintAreaSet()) {
			return;
		}
		$printAreas = explode(',', $pageSetup->getPrintArea());
		$newPrintAreas = [];
		foreach ($printAreas as $printArea) {
			$result = $this->updatePrintArea($printArea, $beforeCellAddress, $numberOfColumns, $numberOfRows);
			if ($result !== '') {
				$newPrintAreas[] = $result;
			}
		}
		$result = implode(',', $newPrintAreas);
		if ($result === '') {
			$pageSetup->clearPrintArea();
		} else {
			$pageSetup->setPrintArea($result);
		}
	}

	private function updatePrintArea(string $printArea, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): string
	{
		$coordinates = Coordinate::indexesFromString($beforeCellAddress);
		if (preg_match('/^([A-Z]{1,3})(\d{1,7}):([A-Z]{1,3})(\d{1,7})$/i', $printArea, $matches) === 1) {
			$firstRow = (int) $matches[2];
			$lastRow = (int) $matches[4];
			$firstColumnString = $matches[1];
			$lastColumnString = $matches[3];
			if ($numberOfRows < 0) {
				$affectedRow = $coordinates[1] + $numberOfRows - 1;
				$lastAffectedRow = $coordinates[1] - 1;
				if ($affectedRow >= $firstRow && $affectedRow <= $lastRow) {
					$newLastRow = max($affectedRow, $lastRow + $numberOfRows);
					if ($newLastRow >= $firstRow) {
						return $matches[1] . $matches[2] . ':' . $matches[3] . $newLastRow;
					}

					return '';
				}
				if ($lastAffectedRow >= $firstRow && $affectedRow <= $lastRow) {
					$newFirstRow = $affectedRow + 1;
					$newLastRow = $lastRow + $numberOfRows;
					if ($newFirstRow >= 1 && $newLastRow >= $newFirstRow) {
						return $matches[1] . $newFirstRow . ':' . $matches[3] . $newLastRow;
					}

					return '';
				}
			}
			if ($numberOfColumns < 0) {
				$firstColumnInt = Coordinate::columnIndexFromString($firstColumnString);
				$lastColumnInt = Coordinate::columnIndexFromString($lastColumnString);
				$affectedColumn = $coordinates[0] + $numberOfColumns - 1;
				$lastAffectedColumn = $coordinates[0] - 1;
				if ($affectedColumn >= $firstColumnInt && $affectedColumn <= $lastColumnInt) {
					$newLastColumnInt = max($affectedColumn, $lastColumnInt + $numberOfColumns);
					if ($newLastColumnInt >= $firstColumnInt) {
						$newLastColumnString = Coordinate::stringFromColumnIndex($newLastColumnInt);

						return $matches[1] . $matches[2] . ':' . $newLastColumnString . $matches[4];
					}

					return '';
				}
				if ($affectedColumn < $firstColumnInt && $lastAffectedColumn > $lastColumnInt) {
					return '';
				}
				if ($lastAffectedColumn >= $firstColumnInt && $lastAffectedColumn <= $lastColumnInt) {
					$newFirstColumn = $affectedColumn + 1;
					$newLastColumn = $lastColumnInt + $numberOfColumns;
					if ($newFirstColumn >= 1 && $newLastColumn >= $newFirstColumn) {
						$firstString = Coordinate::stringFromColumnIndex($newFirstColumn);
						$lastString = Coordinate::stringFromColumnIndex($newLastColumn);

						return $firstString . $matches[2] . ':' . $lastString . $matches[4];
					}

					return '';
				}
			}
		}

		return $this->updateCellReference($printArea);
	}

	private static function matchSheetName(?string $match, string $worksheetName): bool
	{
		return $match === null || $match === '' || $match === "'\u{fffc}'" || $match === "'\u{fffb}'" || strcasecmp(trim($match, "'"), $worksheetName) === 0;
	}

	private static function sheetnameBeforeCells(string $match, string $worksheetName, string $cells): string
	{
		$toString = ($match > '') ? "$match!" : '';

		return str_replace(["\u{fffc}", "'\u{fffb}'"], $worksheetName, $toString) . $cells;
	}

	/**
	 * Update references within formulas.
	 *
	 * @param string $formula Formula to update
	 * @param string $beforeCellAddress Insert before this one
	 * @param int $numberOfColumns Number of columns to insert
	 * @param int $numberOfRows Number of rows to insert
	 * @param string $worksheetName Worksheet name/title
	 *
	 * @return string Updated formula
	 */
	public function updateFormulaReferences(
		string $formula = '',
		string $beforeCellAddress = 'A1',
		int $numberOfColumns = 0,
		int $numberOfRows = 0,
		string $worksheetName = '',
		bool $includeAbsoluteReferences = false,
		bool $onlyAbsoluteReferences = false
	): string {
		$callback = fn (array $matches): string => (strcasecmp(trim($matches[2], "'"), $worksheetName) === 0) ? (($matches[2][0] === "'") ? "'\u{fffc}'!" : "'\u{fffb}'!") : "'\u{fffd}'!";
		if (
			$this->cellReferenceHelper === null
			|| $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows)
		) {
			$this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows);
		}

		//    Update cell references in the formula
		$formulaBlocks = explode('"', $formula);
		$i = false;
		foreach ($formulaBlocks as &$formulaBlock) {
			//    Ignore blocks that were enclosed in quotes (alternating entries in the $formulaBlocks array after the explode)
			$i = $i === false;
			if ($i) {
				$adjustCount = 0;
				$newCellTokens = $cellTokens = [];
				//    Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5)
				$formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
				$matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
				if ($matchCount > 0) {
					foreach ($matches as $match) {
						$fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
						$modified3 = (string) substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, true), 2);
						$modified4 = (string) substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences, false), 2);

						if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
							if (self::matchSheetName($match[2], $worksheetName)) {
								$toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
								//    Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
								$column = 100000;
								$row = 10000000 + (int) trim($match[3], '$');
								$cellIndex = "{$column}{$row}";

								$newCellTokens[$cellIndex] = preg_quote($toString, '/');
								$cellTokens[$cellIndex] = '/(?<!\d\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
								++$adjustCount;
							}
						}
					}
				}
				//    Search for column ranges (e.g. 'Sheet1'!C:E or C:E) with or without $ absolutes (e.g. $C:E)
				$formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
				$matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
				if ($matchCount > 0) {
					foreach ($matches as $match) {
						$fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
						$modified3 = (string) substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences, true), 0, -2);
						$modified4 = (string) substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences, false), 0, -2);

						if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
							if (self::matchSheetName($match[2], $worksheetName)) {
								$toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
								//    Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
								$column = Coordinate::columnIndexFromString(trim($match[3], '$')) + 100000;
								$row = 10000000;
								$cellIndex = "{$column}{$row}";

								$newCellTokens[$cellIndex] = preg_quote($toString, '/');
								$cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?![A-Z])/i';
								++$adjustCount;
							}
						}
					}
				}
				//    Search for cell ranges (e.g. 'Sheet1'!A3:C5 or A3:C5) with or without $ absolutes (e.g. $A1:C$5)
				$formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, "$formulaBlock") ?? "$formulaBlock") . ' ';
				$matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
				if ($matchCount > 0) {
					foreach ($matches as $match) {
						$fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
						$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, true);
						$modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences, false);

						if ($match[3] . $match[4] !== $modified3 . $modified4) {
							if (self::matchSheetName($match[2], $worksheetName)) {
								$toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
								[$column, $row] = Coordinate::coordinateFromString($match[3]);
								//    Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
								$column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000;
								$row = (int) trim($row, '$') + 10000000;
								$cellIndex = "{$column}{$row}";

								$newCellTokens[$cellIndex] = preg_quote($toString, '/');
								$cellTokens[$cellIndex] = '/(?<![A-Z]\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
								++$adjustCount;
							}
						}
					}
				}
				//    Search for cell references (e.g. 'Sheet1'!A3 or C5) with or without $ absolutes (e.g. $A1 or C$5)

				$formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
				$matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);

				if ($matchCount > 0) {
					foreach ($matches as $match) {
						$fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}");

						$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, null);
						if ($match[3] !== $modified3) {
							if (self::matchSheetName($match[2], $worksheetName)) {
								$toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3");
								[$column, $row] = Coordinate::coordinateFromString($match[3]);
								$columnAdditionalIndex = $column[0] === '$' ? 1 : 0;
								$rowAdditionalIndex = $row[0] === '$' ? 1 : 0;
								//    Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
								$column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000;
								$row = (int) trim($row, '$') + 10000000;
								$cellIndex = $row . $rowAdditionalIndex . $column . $columnAdditionalIndex;

								$newCellTokens[$cellIndex] = preg_quote($toString, '/');
								$cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?!\d)/i';
								++$adjustCount;
							}
						}
					}
				}
				if ($adjustCount > 0) {
					if ($numberOfColumns > 0 || $numberOfRows > 0) {
						krsort($cellTokens);
						krsort($newCellTokens);
					} else {
						ksort($cellTokens);
						ksort($newCellTokens);
					}   //  Update cell references in the formula
					$formulaBlock = str_replace('\\', '', (string) preg_replace($cellTokens, $newCellTokens, $formulaBlock));
				}
			}
		}
		unset($formulaBlock);

		//    Then rebuild the formula string
		return implode('"', $formulaBlocks);
	}

	/**
	 * Update all cell references within a formula, irrespective of worksheet.
	 */
	public function updateFormulaReferencesAnyWorksheet(string $formula = '', int $numberOfColumns = 0, int $numberOfRows = 0): string
	{
		$formula = $this->updateCellReferencesAllWorksheets($formula, $numberOfColumns, $numberOfRows);

		if ($numberOfColumns !== 0) {
			$formula = $this->updateColumnRangesAllWorksheets($formula, $numberOfColumns);
		}

		if ($numberOfRows !== 0) {
			$formula = $this->updateRowRangesAllWorksheets($formula, $numberOfRows);
		}

		return $formula;
	}

	private function updateCellReferencesAllWorksheets(string $formula, int $numberOfColumns, int $numberOfRows): string
	{
		$splitCount = preg_match_all(
			'/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui',
			$formula,
			$splitRanges,
			PREG_OFFSET_CAPTURE
		);

		$columnLengths = array_map('strlen', array_column($splitRanges[6], 0));
		$rowLengths = array_map('strlen', array_column($splitRanges[7], 0));
		$columnOffsets = array_column($splitRanges[6], 1);
		$rowOffsets = array_column($splitRanges[7], 1);

		$columns = $splitRanges[6];
		$rows = $splitRanges[7];

		while ($splitCount > 0) {
			--$splitCount;
			$columnLength = $columnLengths[$splitCount];
			$rowLength = $rowLengths[$splitCount];
			$columnOffset = $columnOffsets[$splitCount];
			$rowOffset = $rowOffsets[$splitCount];
			$column = $columns[$splitCount][0];
			$row = $rows[$splitCount][0];

			if ($column[0] !== '$') {
				$column = ((Coordinate::columnIndexFromString($column) + $numberOfColumns) % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
				$column = Coordinate::stringFromColumnIndex($column);
				$rowOffset -= ($columnLength - strlen($column));
				$formula = substr($formula, 0, $columnOffset) . $column . substr($formula, $columnOffset + $columnLength);
			}
			if (!empty($row) && $row[0] !== '$') {
				$row = (((int) $row + $numberOfRows) % AddressRange::MAX_ROW) ?: AddressRange::MAX_ROW;
				$formula = substr($formula, 0, $rowOffset) . $row . substr($formula, $rowOffset + $rowLength);
			}
		}

		return $formula;
	}

	private function updateColumnRangesAllWorksheets(string $formula, int $numberOfColumns): string
	{
		$splitCount = preg_match_all(
			'/' . Calculation::CALCULATION_REGEXP_COLUMNRANGE_RELATIVE . '/mui',
			$formula,
			$splitRanges,
			PREG_OFFSET_CAPTURE
		);

		$fromColumnLengths = array_map('strlen', array_column($splitRanges[1], 0));
		$fromColumnOffsets = array_column($splitRanges[1], 1);
		$toColumnLengths = array_map('strlen', array_column($splitRanges[2], 0));
		$toColumnOffsets = array_column($splitRanges[2], 1);

		$fromColumns = $splitRanges[1];
		$toColumns = $splitRanges[2];

		while ($splitCount > 0) {
			--$splitCount;
			$fromColumnLength = $fromColumnLengths[$splitCount];
			$toColumnLength = $toColumnLengths[$splitCount];
			$fromColumnOffset = $fromColumnOffsets[$splitCount];
			$toColumnOffset = $toColumnOffsets[$splitCount];
			$fromColumn = $fromColumns[$splitCount][0];
			$toColumn = $toColumns[$splitCount][0];

			if (!empty($fromColumn) && $fromColumn[0] !== '$') {
				$fromColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($fromColumn) + $numberOfColumns);
				$formula = substr($formula, 0, $fromColumnOffset) . $fromColumn . substr($formula, $fromColumnOffset + $fromColumnLength);
			}
			if (!empty($toColumn) && $toColumn[0] !== '$') {
				$toColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($toColumn) + $numberOfColumns);
				$formula = substr($formula, 0, $toColumnOffset) . $toColumn . substr($formula, $toColumnOffset + $toColumnLength);
			}
		}

		return $formula;
	}

	private function updateRowRangesAllWorksheets(string $formula, int $numberOfRows): string
	{
		$splitCount = preg_match_all(
			'/' . Calculation::CALCULATION_REGEXP_ROWRANGE_RELATIVE . '/mui',
			$formula,
			$splitRanges,
			PREG_OFFSET_CAPTURE
		);

		$fromRowLengths = array_map('strlen', array_column($splitRanges[1], 0));
		$fromRowOffsets = array_column($splitRanges[1], 1);
		$toRowLengths = array_map('strlen', array_column($splitRanges[2], 0));
		$toRowOffsets = array_column($splitRanges[2], 1);

		$fromRows = $splitRanges[1];
		$toRows = $splitRanges[2];

		while ($splitCount > 0) {
			--$splitCount;
			$fromRowLength = $fromRowLengths[$splitCount];
			$toRowLength = $toRowLengths[$splitCount];
			$fromRowOffset = $fromRowOffsets[$splitCount];
			$toRowOffset = $toRowOffsets[$splitCount];
			$fromRow = $fromRows[$splitCount][0];
			$toRow = $toRows[$splitCount][0];

			if (!empty($fromRow) && $fromRow[0] !== '$') {
				$fromRow = (int) $fromRow + $numberOfRows;
				$formula = substr($formula, 0, $fromRowOffset) . $fromRow . substr($formula, $fromRowOffset + $fromRowLength);
			}
			if (!empty($toRow) && $toRow[0] !== '$') {
				$toRow = (int) $toRow + $numberOfRows;
				$formula = substr($formula, 0, $toRowOffset) . $toRow . substr($formula, $toRowOffset + $toRowLength);
			}
		}

		return $formula;
	}

	/**
	 * Update cell reference.
	 *
	 * @param string $cellReference Cell address or range of addresses
	 *
	 * @return string Updated cell range
	 */
	private function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false, ?bool $topLeft = null)
	{
		// Is it in another worksheet? Will not have to update anything.
		if (str_contains($cellReference, '!')) {
			return $cellReference;
		}
		// Is it a range or a single cell?
		if (!Coordinate::coordinateIsRange($cellReference)) {
			// Single cell
			/** @var CellReferenceHelper */
			$cellReferenceHelper = $this->cellReferenceHelper;

			return $cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences, $topLeft);
		}

		// Range
		return $this->updateCellRange($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences);
	}

	/**
	 * Update named formulae (i.e. containing worksheet references / named ranges).
	 *
	 * @param Spreadsheet $spreadsheet Object to update
	 * @param string $oldName Old name (name to replace)
	 * @param string $newName New name
	 */
	public function updateNamedFormulae(Spreadsheet $spreadsheet, string $oldName = '', string $newName = ''): void
	{
		if ($oldName == '') {
			return;
		}

		foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
			foreach ($sheet->getCoordinates(false) as $coordinate) {
				$cell = $sheet->getCell($coordinate);
				if ($cell->getDataType() === DataType::TYPE_FORMULA) {
					$formula = $cell->getValueString();
					if (str_contains($formula, $oldName)) {
						$formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula);
						$formula = str_replace($oldName . '!', $newName . '!', $formula);
						$cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
					}
				}
			}
		}
	}

	private function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
	{
		foreach ($worksheet->getParentOrThrow()->getDefinedNames() as $definedName) {
			if ($definedName->isFormula() === false) {
				$this->updateNamedRange($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
			} else {
				$this->updateNamedFormula($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
			}
		}
	}

	private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
	{
		$cellAddress = $definedName->getValue();
		$asFormula = ($cellAddress[0] === '=');
		if ($definedName->getWorksheet() === $worksheet) {
			/**
			 * If we delete the entire range that is referenced by a Named Range, MS Excel sets the value to #REF!
			 * PhpSpreadsheet still only does a basic adjustment, so the Named Range will still reference Cells.
			 * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF!
			 * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace
			 *      them with a #REF!
			 */
			if ($asFormula === true) {
				$formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true, true);
				$definedName->setValue($formula);
			} else {
				$definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='), true));
			}
		}
	}

	private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
	{
		if ($definedName->getWorksheet() === $worksheet) {
			/**
			 * If we delete the entire range that is referenced by a Named Formula, MS Excel sets the value to #REF!
			 * PhpSpreadsheet still only does a basic adjustment, so the Named Formula will still reference Cells.
			 * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF!
			 * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace
			 *      them with a #REF!
			 */
			$formula = $definedName->getValue();
			$formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true);
			$definedName->setValue($formula);
		}
	}

	/**
	 * Update cell range.
	 *
	 * @param string $cellRange Cell range    (e.g. 'B2:D4', 'B:C' or '2:3')
	 *
	 * @return string Updated cell range
	 */
	private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false): string
	{
		if (!Coordinate::coordinateIsRange($cellRange)) {
			throw new Exception('Only cell ranges may be passed to this method.');
		}

		// Update range
		$range = Coordinate::splitRange($cellRange);
		$ic = count($range);
		for ($i = 0; $i < $ic; ++$i) {
			$jc = count($range[$i]);
			for ($j = 0; $j < $jc; ++$j) {
				/** @var CellReferenceHelper */
				$cellReferenceHelper = $this->cellReferenceHelper;
				if (ctype_alpha($range[$i][$j])) {
					$range[$i][$j] = Coordinate::coordinateFromString(
						$cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences, $onlyAbsoluteReferences, null)
					)[0];
				} elseif (ctype_digit($range[$i][$j])) {
					$range[$i][$j] = Coordinate::coordinateFromString(
						$cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences, null)
					)[1];
				} else {
					$range[$i][$j] = $cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences, null);
				}
			}
		}

		// Recreate range string
		return Coordinate::buildRange($range);
	}

	private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void
	{
		$startColumnId = Coordinate::stringFromColumnIndex($beforeColumn + $numberOfColumns);
		$endColumnId = Coordinate::stringFromColumnIndex($beforeColumn);

		for ($row = 1; $row <= $highestRow - 1; ++$row) {
			for ($column = $startColumnId; $column !== $endColumnId; StringHelper::stringIncrement($column)) {
				$coordinate = $column . $row;
				$this->clearStripCell($worksheet, $coordinate);
			}
		}
	}

	private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void
	{
		$startColumnId = Coordinate::stringFromColumnIndex($beforeColumn);
		StringHelper::stringIncrement($highestColumn);

		for ($column = $startColumnId; $column !== $highestColumn; StringHelper::stringIncrement($column)) {
			for ($row = $beforeRow + $numberOfRows; $row <= $beforeRow - 1; ++$row) {
				$coordinate = $column . $row;
				$this->clearStripCell($worksheet, $coordinate);
			}
		}
	}

	private function clearStripCell(Worksheet $worksheet, string $coordinate): void
	{
		$worksheet->removeConditionalStyles($coordinate);
		$worksheet->setHyperlink($coordinate, null, false);
		$worksheet->setDataValidation($coordinate);
		$worksheet->removeComment($coordinate);

		if ($worksheet->cellExists($coordinate)) {
			$worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL);
			$worksheet->getCell($coordinate)->setXfIndex(0);
		}
	}

	private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void
	{
		$autoFilter = $worksheet->getAutoFilter();
		$autoFilterRange = $autoFilter->getRange();
		if (!empty($autoFilterRange)) {
			if ($numberOfColumns !== 0) {
				$autoFilterColumns = $autoFilter->getColumns();
				if (count($autoFilterColumns) > 0) {
					$column = '';
					$row = 0;
					sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row);
					$columnIndex = Coordinate::columnIndexFromString((string) $column);
					[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange);
					if ($columnIndex <= $rangeEnd[0]) {
						if ($numberOfColumns < 0) {
							$this->adjustAutoFilterDeleteRules($columnIndex, $numberOfColumns, $autoFilterColumns, $autoFilter);
						}
						$startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0];

						//    Shuffle columns in autofilter range
						if ($numberOfColumns > 0) {
							$this->adjustAutoFilterInsert($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter);
						} else {
							$this->adjustAutoFilterDelete($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter);
						}
					}
				}
			}

			$worksheet->setAutoFilter(
				$this->updateCellReference($autoFilterRange)
			);
		}
	}

	/** @param mixed[] $autoFilterColumns */
	private function adjustAutoFilterDeleteRules(int $columnIndex, int $numberOfColumns, array $autoFilterColumns, AutoFilter $autoFilter): void
	{
		// If we're actually deleting any columns that fall within the autofilter range,
		//    then we delete any rules for those columns
		$deleteColumn = $columnIndex + $numberOfColumns - 1;
		$deleteCount = abs($numberOfColumns);

		for ($i = 1; $i <= $deleteCount; ++$i) {
			$columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1);
			if (isset($autoFilterColumns[$columnName])) {
				$autoFilter->clearColumn($columnName);
			}
			++$deleteColumn;
		}
	}

	private function adjustAutoFilterInsert(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void
	{
		$startColRef = $startCol;
		$endColRef = $rangeEnd;
		$toColRef = $rangeEnd + $numberOfColumns;

		do {
			$autoFilter->shiftColumn(
				Coordinate::stringFromColumnIndex($endColRef),
				Coordinate::stringFromColumnIndex($toColRef)
			);
			--$endColRef;
			--$toColRef;
		} while ($startColRef <= $endColRef);
	}

	private function adjustAutoFilterDelete(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void
	{
		// For delete, we shuffle from beginning to end to avoid overwriting
		$startColID = Coordinate::stringFromColumnIndex($startCol);
		$toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns);
		$endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1);

		do {
			$autoFilter->shiftColumn($startColID, $toColID);
			StringHelper::stringIncrement($toColID);
			StringHelper::stringIncrement($startColID);
		} while ($startColID !== $endColID);
	}

	private function adjustTable(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void
	{
		$tableCollection = $worksheet->getTableCollection();

		foreach ($tableCollection as $table) {
			$tableRange = $table->getRange();
			if (!empty($tableRange)) {
				if ($numberOfColumns !== 0) {
					$tableColumns = $table->getColumns();
					if (count($tableColumns) > 0) {
						$column = '';
						$row = 0;
						sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row);
						$columnIndex = Coordinate::columnIndexFromString((string) $column);
						[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($tableRange);
						if ($columnIndex <= $rangeEnd[0]) {
							if ($numberOfColumns < 0) {
								$this->adjustTableDeleteRules($columnIndex, $numberOfColumns, $tableColumns, $table);
							}
							$startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0];

							//    Shuffle columns in table range
							if ($numberOfColumns > 0) {
								$this->adjustTableInsert($startCol, $numberOfColumns, $rangeEnd[0], $table);
							} else {
								$this->adjustTableDelete($startCol, $numberOfColumns, $rangeEnd[0], $table);
							}
						}
					}
				}

				$table->setRange($this->updateCellReference($tableRange));
			}
		}
	}

	/** @param mixed[] $tableColumns */
	private function adjustTableDeleteRules(int $columnIndex, int $numberOfColumns, array $tableColumns, Table $table): void
	{
		// If we're actually deleting any columns that fall within the table range,
		//    then we delete any rules for those columns
		$deleteColumn = $columnIndex + $numberOfColumns - 1;
		$deleteCount = abs($numberOfColumns);

		for ($i = 1; $i <= $deleteCount; ++$i) {
			$columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1);
			if (isset($tableColumns[$columnName])) {
				$table->clearColumn($columnName);
			}
			++$deleteColumn;
		}
	}

	private function adjustTableInsert(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void
	{
		$startColRef = $startCol;
		$endColRef = $rangeEnd;
		$toColRef = $rangeEnd + $numberOfColumns;

		do {
			$table->shiftColumn(
				Coordinate::stringFromColumnIndex($endColRef),
				Coordinate::stringFromColumnIndex($toColRef)
			);
			--$endColRef;
			--$toColRef;
		} while ($startColRef <= $endColRef);
	}

	private function adjustTableDelete(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void
	{
		// For delete, we shuffle from beginning to end to avoid overwriting
		$startColID = Coordinate::stringFromColumnIndex($startCol);
		$toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns);
		$endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1);

		do {
			$table->shiftColumn($startColID, $toColID);
			StringHelper::stringIncrement($toColID);
			StringHelper::stringIncrement($startColID);
		} while ($startColID !== $endColID);
	}

	private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn, int $beforeRow, int $highestRow, int $numberOfColumns): void
	{
		$beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1);
		for ($i = $beforeRow; $i <= $highestRow; ++$i) {
			// Style
			$coordinate = $beforeColumnName . $i;
			if ($worksheet->cellExists($coordinate)) {
				$xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
				for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) {
					if (!empty($xfIndex) || $worksheet->cellExists([$j, $i])) {
						$worksheet->getCell([$j, $i])->setXfIndex($xfIndex);
					}
				}
			}
		}
	}

	private function duplicateStylesByRow(Worksheet $worksheet, int $beforeColumn, int $beforeRow, string $highestColumn, int $numberOfRows): void
	{
		$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
		for ($i = $beforeColumn; $i <= $highestColumnIndex; ++$i) {
			// Style
			$coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1);
			if ($worksheet->cellExists($coordinate)) {
				$xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
				for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) {
					if (!empty($xfIndex) || $worksheet->cellExists([$i, $j])) {
						$worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex);
					}
				}
			}
		}
	}

	/**
	 * __clone implementation. Cloning should not be allowed in a Singleton!
	 */
	final public function __clone()
	{
		throw new Exception('Cloning a Singleton is not allowed!');
	}
}

```
