Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]
1 <?php 2 3 namespace PhpOffice\PhpSpreadsheet; 4 5 use PhpOffice\PhpSpreadsheet\Calculation\Calculation; 6 use PhpOffice\PhpSpreadsheet\Cell\Coordinate; 7 use PhpOffice\PhpSpreadsheet\Cell\DataType; 8 use PhpOffice\PhpSpreadsheet\Style\Conditional; 9 use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter; 10 use PhpOffice\PhpSpreadsheet\Worksheet\Table; 11 use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; 12 13 class ReferenceHelper 14 { 15 /** Constants */ 16 /** Regular Expressions */ 17 const REFHELPER_REGEXP_CELLREF = '((\w*|\'[^!]*\')!)?(?<![:a-z\$])(\$?[a-z]{1,3}\$?\d+)(?=[^:!\d\'])'; 18 const REFHELPER_REGEXP_CELLRANGE = '((\w*|\'[^!]*\')!)?(\$?[a-z]{1,3}\$?\d+):(\$?[a-z]{1,3}\$?\d+)'; 19 const REFHELPER_REGEXP_ROWRANGE = '((\w*|\'[^!]*\')!)?(\$?\d+):(\$?\d+)'; 20 const REFHELPER_REGEXP_COLRANGE = '((\w*|\'[^!]*\')!)?(\$?[a-z]{1,3}):(\$?[a-z]{1,3})'; 21 22 /** 23 * Instance of this class. 24 * 25 * @var ?ReferenceHelper 26 */ 27 private static $instance; 28 29 /** 30 * @var CellReferenceHelper 31 */ 32 private $cellReferenceHelper; 33 34 /** 35 * Get an instance of this class. 36 * 37 * @return ReferenceHelper 38 */ 39 public static function getInstance() 40 { 41 if (self::$instance === null) { 42 self::$instance = new self(); 43 } 44 45 return self::$instance; 46 } 47 48 /** 49 * Create a new ReferenceHelper. 50 */ 51 protected function __construct() 52 { 53 } 54 55 /** 56 * Compare two column addresses 57 * Intended for use as a Callback function for sorting column addresses by column. 58 * 59 * @param string $a First column to test (e.g. 'AA') 60 * @param string $b Second column to test (e.g. 'Z') 61 * 62 * @return int 63 */ 64 public static function columnSort($a, $b) 65 { 66 return strcasecmp(strlen($a) . $a, strlen($b) . $b); 67 } 68 69 /** 70 * Compare two column addresses 71 * Intended for use as a Callback function for reverse sorting column addresses by column. 72 * 73 * @param string $a First column to test (e.g. 'AA') 74 * @param string $b Second column to test (e.g. 'Z') 75 * 76 * @return int 77 */ 78 public static function columnReverseSort(string $a, string $b) 79 { 80 return -strcasecmp(strlen($a) . $a, strlen($b) . $b); 81 } 82 83 /** 84 * Compare two cell addresses 85 * Intended for use as a Callback function for sorting cell addresses by column and row. 86 * 87 * @param string $a First cell to test (e.g. 'AA1') 88 * @param string $b Second cell to test (e.g. 'Z1') 89 * 90 * @return int 91 */ 92 public static function cellSort(string $a, string $b) 93 { 94 /** @scrutinizer be-damned */ 95 sscanf($a, '%[A-Z]%d', $ac, $ar); 96 /** @var int $ar */ 97 /** @var string $ac */ 98 /** @scrutinizer be-damned */ 99 sscanf($b, '%[A-Z]%d', $bc, $br); 100 /** @var int $br */ 101 /** @var string $bc */ 102 if ($ar === $br) { 103 return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc); 104 } 105 106 return ($ar < $br) ? -1 : 1; 107 } 108 109 /** 110 * Compare two cell addresses 111 * Intended for use as a Callback function for sorting cell addresses by column and row. 112 * 113 * @param string $a First cell to test (e.g. 'AA1') 114 * @param string $b Second cell to test (e.g. 'Z1') 115 * 116 * @return int 117 */ 118 public static function cellReverseSort(string $a, string $b) 119 { 120 /** @scrutinizer be-damned */ 121 sscanf($a, '%[A-Z]%d', $ac, $ar); 122 /** @var int $ar */ 123 /** @var string $ac */ 124 /** @scrutinizer be-damned */ 125 sscanf($b, '%[A-Z]%d', $bc, $br); 126 /** @var int $br */ 127 /** @var string $bc */ 128 if ($ar === $br) { 129 return -strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc); 130 } 131 132 return ($ar < $br) ? 1 : -1; 133 } 134 135 /** 136 * Update page breaks when inserting/deleting rows/columns. 137 * 138 * @param Worksheet $worksheet The worksheet that we're editing 139 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) 140 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) 141 */ 142 protected function adjustPageBreaks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void 143 { 144 $aBreaks = $worksheet->getBreaks(); 145 ($numberOfColumns > 0 || $numberOfRows > 0) 146 ? uksort($aBreaks, [self::class, 'cellReverseSort']) 147 : uksort($aBreaks, [self::class, 'cellSort']); 148 149 foreach ($aBreaks as $cellAddress => $value) { 150 if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) { 151 // If we're deleting, then clear any defined breaks that are within the range 152 // of rows/columns that we're deleting 153 $worksheet->setBreak($cellAddress, Worksheet::BREAK_NONE); 154 } else { 155 // Otherwise update any affected breaks by inserting a new break at the appropriate point 156 // and removing the old affected break 157 $newReference = $this->updateCellReference($cellAddress); 158 if ($cellAddress !== $newReference) { 159 $worksheet->setBreak($newReference, $value) 160 ->setBreak($cellAddress, Worksheet::BREAK_NONE); 161 } 162 } 163 } 164 } 165 166 /** 167 * Update cell comments when inserting/deleting rows/columns. 168 * 169 * @param Worksheet $worksheet The worksheet that we're editing 170 */ 171 protected function adjustComments(Worksheet $worksheet): void 172 { 173 $aComments = $worksheet->getComments(); 174 $aNewComments = []; // the new array of all comments 175 176 foreach ($aComments as $cellAddress => &$value) { 177 // Any comments inside a deleted range will be ignored 178 if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === false) { 179 // Otherwise build a new array of comments indexed by the adjusted cell reference 180 $newReference = $this->updateCellReference($cellAddress); 181 $aNewComments[$newReference] = $value; 182 } 183 } 184 // Replace the comments array with the new set of comments 185 $worksheet->setComments($aNewComments); 186 } 187 188 /** 189 * Update hyperlinks when inserting/deleting rows/columns. 190 * 191 * @param Worksheet $worksheet The worksheet that we're editing 192 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) 193 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) 194 */ 195 protected function adjustHyperlinks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void 196 { 197 $aHyperlinkCollection = $worksheet->getHyperlinkCollection(); 198 ($numberOfColumns > 0 || $numberOfRows > 0) 199 ? uksort($aHyperlinkCollection, [self::class, 'cellReverseSort']) 200 : uksort($aHyperlinkCollection, [self::class, 'cellSort']); 201 202 foreach ($aHyperlinkCollection as $cellAddress => $value) { 203 $newReference = $this->updateCellReference($cellAddress); 204 if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) { 205 $worksheet->setHyperlink($cellAddress, null); 206 } elseif ($cellAddress !== $newReference) { 207 $worksheet->setHyperlink($newReference, $value); 208 $worksheet->setHyperlink($cellAddress, null); 209 } 210 } 211 } 212 213 /** 214 * Update conditional formatting styles when inserting/deleting rows/columns. 215 * 216 * @param Worksheet $worksheet The worksheet that we're editing 217 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) 218 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) 219 */ 220 protected function adjustConditionalFormatting(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void 221 { 222 $aStyles = $worksheet->getConditionalStylesCollection(); 223 ($numberOfColumns > 0 || $numberOfRows > 0) 224 ? uksort($aStyles, [self::class, 'cellReverseSort']) 225 : uksort($aStyles, [self::class, 'cellSort']); 226 227 foreach ($aStyles as $cellAddress => $cfRules) { 228 $worksheet->removeConditionalStyles($cellAddress); 229 $newReference = $this->updateCellReference($cellAddress); 230 231 foreach ($cfRules as &$cfRule) { 232 /** @var Conditional $cfRule */ 233 $conditions = $cfRule->getConditions(); 234 foreach ($conditions as &$condition) { 235 if (is_string($condition)) { 236 $condition = $this->updateFormulaReferences( 237 $condition, 238 $this->cellReferenceHelper->beforeCellAddress(), 239 $numberOfColumns, 240 $numberOfRows, 241 $worksheet->getTitle(), 242 true 243 ); 244 } 245 } 246 $cfRule->setConditions($conditions); 247 } 248 $worksheet->setConditionalStyles($newReference, $cfRules); 249 } 250 } 251 252 /** 253 * Update data validations when inserting/deleting rows/columns. 254 * 255 * @param Worksheet $worksheet The worksheet that we're editing 256 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) 257 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) 258 */ 259 protected function adjustDataValidations(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void 260 { 261 $aDataValidationCollection = $worksheet->getDataValidationCollection(); 262 ($numberOfColumns > 0 || $numberOfRows > 0) 263 ? uksort($aDataValidationCollection, [self::class, 'cellReverseSort']) 264 : uksort($aDataValidationCollection, [self::class, 'cellSort']); 265 266 foreach ($aDataValidationCollection as $cellAddress => $dataValidation) { 267 $newReference = $this->updateCellReference($cellAddress); 268 if ($cellAddress !== $newReference) { 269 $dataValidation->setSqref($newReference); 270 $worksheet->setDataValidation($newReference, $dataValidation); 271 $worksheet->setDataValidation($cellAddress, null); 272 } 273 } 274 } 275 276 /** 277 * Update merged cells when inserting/deleting rows/columns. 278 * 279 * @param Worksheet $worksheet The worksheet that we're editing 280 */ 281 protected function adjustMergeCells(Worksheet $worksheet): void 282 { 283 $aMergeCells = $worksheet->getMergeCells(); 284 $aNewMergeCells = []; // the new array of all merge cells 285 foreach ($aMergeCells as $cellAddress => &$value) { 286 $newReference = $this->updateCellReference($cellAddress); 287 $aNewMergeCells[$newReference] = $newReference; 288 } 289 $worksheet->setMergeCells($aNewMergeCells); // replace the merge cells array 290 } 291 292 /** 293 * Update protected cells when inserting/deleting rows/columns. 294 * 295 * @param Worksheet $worksheet The worksheet that we're editing 296 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) 297 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) 298 */ 299 protected function adjustProtectedCells(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void 300 { 301 $aProtectedCells = $worksheet->getProtectedCells(); 302 ($numberOfColumns > 0 || $numberOfRows > 0) 303 ? uksort($aProtectedCells, [self::class, 'cellReverseSort']) 304 : uksort($aProtectedCells, [self::class, 'cellSort']); 305 foreach ($aProtectedCells as $cellAddress => $value) { 306 $newReference = $this->updateCellReference($cellAddress); 307 if ($cellAddress !== $newReference) { 308 $worksheet->protectCells($newReference, $value, true); 309 $worksheet->unprotectCells($cellAddress); 310 } 311 } 312 } 313 314 /** 315 * Update column dimensions when inserting/deleting rows/columns. 316 * 317 * @param Worksheet $worksheet The worksheet that we're editing 318 */ 319 protected function adjustColumnDimensions(Worksheet $worksheet): void 320 { 321 $aColumnDimensions = array_reverse($worksheet->getColumnDimensions(), true); 322 if (!empty($aColumnDimensions)) { 323 foreach ($aColumnDimensions as $objColumnDimension) { 324 $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1'); 325 [$newReference] = Coordinate::coordinateFromString($newReference); 326 if ($objColumnDimension->getColumnIndex() !== $newReference) { 327 $objColumnDimension->setColumnIndex($newReference); 328 } 329 } 330 331 $worksheet->refreshColumnDimensions(); 332 } 333 } 334 335 /** 336 * Update row dimensions when inserting/deleting rows/columns. 337 * 338 * @param Worksheet $worksheet The worksheet that we're editing 339 * @param int $beforeRow Number of the row we're inserting/deleting before 340 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) 341 */ 342 protected function adjustRowDimensions(Worksheet $worksheet, $beforeRow, $numberOfRows): void 343 { 344 $aRowDimensions = array_reverse($worksheet->getRowDimensions(), true); 345 if (!empty($aRowDimensions)) { 346 foreach ($aRowDimensions as $objRowDimension) { 347 $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex()); 348 [, $newReference] = Coordinate::coordinateFromString($newReference); 349 $newRoweference = (int) $newReference; 350 if ($objRowDimension->getRowIndex() !== $newRoweference) { 351 $objRowDimension->setRowIndex($newRoweference); 352 } 353 } 354 355 $worksheet->refreshRowDimensions(); 356 357 $copyDimension = $worksheet->getRowDimension($beforeRow - 1); 358 for ($i = $beforeRow; $i <= $beforeRow - 1 + $numberOfRows; ++$i) { 359 $newDimension = $worksheet->getRowDimension($i); 360 $newDimension->setRowHeight($copyDimension->getRowHeight()); 361 $newDimension->setVisible($copyDimension->getVisible()); 362 $newDimension->setOutlineLevel($copyDimension->getOutlineLevel()); 363 $newDimension->setCollapsed($copyDimension->getCollapsed()); 364 } 365 } 366 } 367 368 /** 369 * Insert a new column or row, updating all possible related data. 370 * 371 * @param string $beforeCellAddress Insert before this cell address (e.g. 'A1') 372 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) 373 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) 374 * @param Worksheet $worksheet The worksheet that we're editing 375 */ 376 public function insertNewBefore( 377 string $beforeCellAddress, 378 int $numberOfColumns, 379 int $numberOfRows, 380 Worksheet $worksheet 381 ): void { 382 $remove = ($numberOfColumns < 0 || $numberOfRows < 0); 383 384 if ( 385 $this->cellReferenceHelper === null || 386 $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows) 387 ) { 388 $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows); 389 } 390 391 // Get coordinate of $beforeCellAddress 392 [$beforeColumn, $beforeRow] = Coordinate::indexesFromString($beforeCellAddress); 393 394 // Clear cells if we are removing columns or rows 395 $highestColumn = $worksheet->getHighestColumn(); 396 $highestRow = $worksheet->getHighestRow(); 397 398 // 1. Clear column strips if we are removing columns 399 if ($numberOfColumns < 0 && $beforeColumn - 2 + $numberOfColumns > 0) { 400 $this->clearColumnStrips($highestRow, $beforeColumn, $numberOfColumns, $worksheet); 401 } 402 403 // 2. Clear row strips if we are removing rows 404 if ($numberOfRows < 0 && $beforeRow - 1 + $numberOfRows > 0) { 405 $this->clearRowStrips($highestColumn, $beforeColumn, $beforeRow, $numberOfRows, $worksheet); 406 } 407 408 // Find missing coordinates. This is important when inserting column before the last column 409 $cellCollection = $worksheet->getCellCollection(); 410 $missingCoordinates = array_filter( 411 array_map(function ($row) use ($highestColumn) { 412 return "{$highestColumn}{$row}"; 413 }, range(1, $highestRow)), 414 function ($coordinate) use ($cellCollection) { 415 return $cellCollection->has($coordinate) === false; 416 } 417 ); 418 419 // Create missing cells with null values 420 if (!empty($missingCoordinates)) { 421 foreach ($missingCoordinates as $coordinate) { 422 $worksheet->createNewCell($coordinate); 423 } 424 } 425 426 $allCoordinates = $worksheet->getCoordinates(); 427 if ($remove) { 428 // It's faster to reverse and pop than to use unshift, especially with large cell collections 429 $allCoordinates = array_reverse($allCoordinates); 430 } 431 432 // Loop through cells, bottom-up, and change cell coordinate 433 while ($coordinate = array_pop($allCoordinates)) { 434 $cell = $worksheet->getCell($coordinate); 435 $cellIndex = Coordinate::columnIndexFromString($cell->getColumn()); 436 437 if ($cellIndex - 1 + $numberOfColumns < 0) { 438 continue; 439 } 440 441 // New coordinate 442 $newCoordinate = Coordinate::stringFromColumnIndex($cellIndex + $numberOfColumns) . ($cell->getRow() + $numberOfRows); 443 444 // Should the cell be updated? Move value and cellXf index from one cell to another. 445 if (($cellIndex >= $beforeColumn) && ($cell->getRow() >= $beforeRow)) { 446 // Update cell styles 447 $worksheet->getCell($newCoordinate)->setXfIndex($cell->getXfIndex()); 448 449 // Insert this cell at its new location 450 if ($cell->getDataType() === DataType::TYPE_FORMULA) { 451 // Formula should be adjusted 452 $worksheet->getCell($newCoordinate) 453 ->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true)); 454 } else { 455 // Cell value should not be adjusted 456 $worksheet->getCell($newCoordinate)->setValueExplicit($cell->getValue(), $cell->getDataType()); 457 } 458 459 // Clear the original cell 460 $worksheet->getCellCollection()->delete($coordinate); 461 } else { 462 /* We don't need to update styles for rows/columns before our insertion position, 463 but we do still need to adjust any formulae in those cells */ 464 if ($cell->getDataType() === DataType::TYPE_FORMULA) { 465 // Formula should be adjusted 466 $cell->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true)); 467 } 468 } 469 } 470 471 // Duplicate styles for the newly inserted cells 472 $highestColumn = $worksheet->getHighestColumn(); 473 $highestRow = $worksheet->getHighestRow(); 474 475 if ($numberOfColumns > 0 && $beforeColumn - 2 > 0) { 476 $this->duplicateStylesByColumn($worksheet, $beforeColumn, $beforeRow, $highestRow, $numberOfColumns); 477 } 478 479 if ($numberOfRows > 0 && $beforeRow - 1 > 0) { 480 $this->duplicateStylesByRow($worksheet, $beforeColumn, $beforeRow, $highestColumn, $numberOfRows); 481 } 482 483 // Update worksheet: column dimensions 484 $this->adjustColumnDimensions($worksheet); 485 486 // Update worksheet: row dimensions 487 $this->adjustRowDimensions($worksheet, $beforeRow, $numberOfRows); 488 489 // Update worksheet: page breaks 490 $this->adjustPageBreaks($worksheet, $numberOfColumns, $numberOfRows); 491 492 // Update worksheet: comments 493 $this->adjustComments($worksheet); 494 495 // Update worksheet: hyperlinks 496 $this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows); 497 498 // Update worksheet: conditional formatting styles 499 $this->adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows); 500 501 // Update worksheet: data validations 502 $this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows); 503 504 // Update worksheet: merge cells 505 $this->adjustMergeCells($worksheet); 506 507 // Update worksheet: protected cells 508 $this->adjustProtectedCells($worksheet, $numberOfColumns, $numberOfRows); 509 510 // Update worksheet: autofilter 511 $this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns); 512 513 // Update worksheet: table 514 $this->adjustTable($worksheet, $beforeCellAddress, $numberOfColumns); 515 516 // Update worksheet: freeze pane 517 if ($worksheet->getFreezePane()) { 518 $splitCell = $worksheet->getFreezePane(); 519 $topLeftCell = $worksheet->getTopLeftCell() ?? ''; 520 521 $splitCell = $this->updateCellReference($splitCell); 522 $topLeftCell = $this->updateCellReference($topLeftCell); 523 524 $worksheet->freezePane($splitCell, $topLeftCell); 525 } 526 527 // Page setup 528 if ($worksheet->getPageSetup()->isPrintAreaSet()) { 529 $worksheet->getPageSetup()->setPrintArea( 530 $this->updateCellReference($worksheet->getPageSetup()->getPrintArea()) 531 ); 532 } 533 534 // Update worksheet: drawings 535 $aDrawings = $worksheet->getDrawingCollection(); 536 foreach ($aDrawings as $objDrawing) { 537 $newReference = $this->updateCellReference($objDrawing->getCoordinates()); 538 if ($objDrawing->getCoordinates() != $newReference) { 539 $objDrawing->setCoordinates($newReference); 540 } 541 if ($objDrawing->getCoordinates2() !== '') { 542 $newReference = $this->updateCellReference($objDrawing->getCoordinates2()); 543 if ($objDrawing->getCoordinates2() != $newReference) { 544 $objDrawing->setCoordinates2($newReference); 545 } 546 } 547 } 548 549 // Update workbook: define names 550 if (count($worksheet->getParentOrThrow()->getDefinedNames()) > 0) { 551 $this->updateDefinedNames($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); 552 } 553 554 // Garbage collect 555 $worksheet->garbageCollect(); 556 } 557 558 /** 559 * Update references within formulas. 560 * 561 * @param string $formula Formula to update 562 * @param string $beforeCellAddress Insert before this one 563 * @param int $numberOfColumns Number of columns to insert 564 * @param int $numberOfRows Number of rows to insert 565 * @param string $worksheetName Worksheet name/title 566 * 567 * @return string Updated formula 568 */ 569 public function updateFormulaReferences( 570 $formula = '', 571 $beforeCellAddress = 'A1', 572 $numberOfColumns = 0, 573 $numberOfRows = 0, 574 $worksheetName = '', 575 bool $includeAbsoluteReferences = false 576 ) { 577 if ( 578 $this->cellReferenceHelper === null || 579 $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows) 580 ) { 581 $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows); 582 } 583 584 // Update cell references in the formula 585 $formulaBlocks = explode('"', $formula); 586 $i = false; 587 foreach ($formulaBlocks as &$formulaBlock) { 588 // Ignore blocks that were enclosed in quotes (alternating entries in the $formulaBlocks array after the explode) 589 $i = $i === false; 590 if ($i) { 591 $adjustCount = 0; 592 $newCellTokens = $cellTokens = []; 593 // Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5) 594 $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); 595 if ($matchCount > 0) { 596 foreach ($matches as $match) { 597 $fromString = ($match[2] > '') ? $match[2] . '!' : ''; 598 $fromString .= $match[3] . ':' . $match[4]; 599 $modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences), 2); 600 $modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences), 2); 601 602 if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { 603 if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { 604 $toString = ($match[2] > '') ? $match[2] . '!' : ''; 605 $toString .= $modified3 . ':' . $modified4; 606 // 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 607 $column = 100000; 608 $row = 10000000 + (int) trim($match[3], '$'); 609 $cellIndex = "{$column}{$row}"; 610 611 $newCellTokens[$cellIndex] = preg_quote($toString, '/'); 612 $cellTokens[$cellIndex] = '/(?<!\d\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i'; 613 ++$adjustCount; 614 } 615 } 616 } 617 } 618 // Search for column ranges (e.g. 'Sheet1'!C:E or C:E) with or without $ absolutes (e.g. $C:E) 619 $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); 620 if ($matchCount > 0) { 621 foreach ($matches as $match) { 622 $fromString = ($match[2] > '') ? $match[2] . '!' : ''; 623 $fromString .= $match[3] . ':' . $match[4]; 624 $modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences), 0, -2); 625 $modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences), 0, -2); 626 627 if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { 628 if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { 629 $toString = ($match[2] > '') ? $match[2] . '!' : ''; 630 $toString .= $modified3 . ':' . $modified4; 631 // 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 632 $column = Coordinate::columnIndexFromString(trim($match[3], '$')) + 100000; 633 $row = 10000000; 634 $cellIndex = "{$column}{$row}"; 635 636 $newCellTokens[$cellIndex] = preg_quote($toString, '/'); 637 $cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?![A-Z])/i'; 638 ++$adjustCount; 639 } 640 } 641 } 642 } 643 // Search for cell ranges (e.g. 'Sheet1'!A3:C5 or A3:C5) with or without $ absolutes (e.g. $A1:C$5) 644 $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); 645 if ($matchCount > 0) { 646 foreach ($matches as $match) { 647 $fromString = ($match[2] > '') ? $match[2] . '!' : ''; 648 $fromString .= $match[3] . ':' . $match[4]; 649 $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences); 650 $modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences); 651 652 if ($match[3] . $match[4] !== $modified3 . $modified4) { 653 if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { 654 $toString = ($match[2] > '') ? $match[2] . '!' : ''; 655 $toString .= $modified3 . ':' . $modified4; 656 [$column, $row] = Coordinate::coordinateFromString($match[3]); 657 // 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 658 $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000; 659 $row = (int) trim($row, '$') + 10000000; 660 $cellIndex = "{$column}{$row}"; 661 662 $newCellTokens[$cellIndex] = preg_quote($toString, '/'); 663 $cellTokens[$cellIndex] = '/(?<![A-Z]\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i'; 664 ++$adjustCount; 665 } 666 } 667 } 668 } 669 // Search for cell references (e.g. 'Sheet1'!A3 or C5) with or without $ absolutes (e.g. $A1 or C$5) 670 $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); 671 672 if ($matchCount > 0) { 673 foreach ($matches as $match) { 674 $fromString = ($match[2] > '') ? $match[2] . '!' : ''; 675 $fromString .= $match[3]; 676 677 $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences); 678 if ($match[3] !== $modified3) { 679 if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { 680 $toString = ($match[2] > '') ? $match[2] . '!' : ''; 681 $toString .= $modified3; 682 [$column, $row] = Coordinate::coordinateFromString($match[3]); 683 $columnAdditionalIndex = $column[0] === '$' ? 1 : 0; 684 $rowAdditionalIndex = $row[0] === '$' ? 1 : 0; 685 // 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 686 $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000; 687 $row = (int) trim($row, '$') + 10000000; 688 $cellIndex = $row . $rowAdditionalIndex . $column . $columnAdditionalIndex; 689 690 $newCellTokens[$cellIndex] = preg_quote($toString, '/'); 691 $cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?!\d)/i'; 692 ++$adjustCount; 693 } 694 } 695 } 696 } 697 if ($adjustCount > 0) { 698 if ($numberOfColumns > 0 || $numberOfRows > 0) { 699 krsort($cellTokens); 700 krsort($newCellTokens); 701 } else { 702 ksort($cellTokens); 703 ksort($newCellTokens); 704 } // Update cell references in the formula 705 $formulaBlock = str_replace('\\', '', (string) preg_replace($cellTokens, $newCellTokens, $formulaBlock)); 706 } 707 } 708 } 709 unset($formulaBlock); 710 711 // Then rebuild the formula string 712 return implode('"', $formulaBlocks); 713 } 714 715 /** 716 * Update all cell references within a formula, irrespective of worksheet. 717 */ 718 public function updateFormulaReferencesAnyWorksheet(string $formula = '', int $numberOfColumns = 0, int $numberOfRows = 0): string 719 { 720 $formula = $this->updateCellReferencesAllWorksheets($formula, $numberOfColumns, $numberOfRows); 721 722 if ($numberOfColumns !== 0) { 723 $formula = $this->updateColumnRangesAllWorksheets($formula, $numberOfColumns); 724 } 725 726 if ($numberOfRows !== 0) { 727 $formula = $this->updateRowRangesAllWorksheets($formula, $numberOfRows); 728 } 729 730 return $formula; 731 } 732 733 private function updateCellReferencesAllWorksheets(string $formula, int $numberOfColumns, int $numberOfRows): string 734 { 735 $splitCount = preg_match_all( 736 '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui', 737 $formula, 738 $splitRanges, 739 PREG_OFFSET_CAPTURE 740 ); 741 742 $columnLengths = array_map('strlen', array_column($splitRanges[6], 0)); 743 $rowLengths = array_map('strlen', array_column($splitRanges[7], 0)); 744 $columnOffsets = array_column($splitRanges[6], 1); 745 $rowOffsets = array_column($splitRanges[7], 1); 746 747 $columns = $splitRanges[6]; 748 $rows = $splitRanges[7]; 749 750 while ($splitCount > 0) { 751 --$splitCount; 752 $columnLength = $columnLengths[$splitCount]; 753 $rowLength = $rowLengths[$splitCount]; 754 $columnOffset = $columnOffsets[$splitCount]; 755 $rowOffset = $rowOffsets[$splitCount]; 756 $column = $columns[$splitCount][0]; 757 $row = $rows[$splitCount][0]; 758 759 if (!empty($column) && $column[0] !== '$') { 760 $column = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($column) + $numberOfColumns); 761 $formula = substr($formula, 0, $columnOffset) . $column . substr($formula, $columnOffset + $columnLength); 762 } 763 if (!empty($row) && $row[0] !== '$') { 764 $row = (int) $row + $numberOfRows; 765 $formula = substr($formula, 0, $rowOffset) . $row . substr($formula, $rowOffset + $rowLength); 766 } 767 } 768 769 return $formula; 770 } 771 772 private function updateColumnRangesAllWorksheets(string $formula, int $numberOfColumns): string 773 { 774 $splitCount = preg_match_all( 775 '/' . Calculation::CALCULATION_REGEXP_COLUMNRANGE_RELATIVE . '/mui', 776 $formula, 777 $splitRanges, 778 PREG_OFFSET_CAPTURE 779 ); 780 781 $fromColumnLengths = array_map('strlen', array_column($splitRanges[1], 0)); 782 $fromColumnOffsets = array_column($splitRanges[1], 1); 783 $toColumnLengths = array_map('strlen', array_column($splitRanges[2], 0)); 784 $toColumnOffsets = array_column($splitRanges[2], 1); 785 786 $fromColumns = $splitRanges[1]; 787 $toColumns = $splitRanges[2]; 788 789 while ($splitCount > 0) { 790 --$splitCount; 791 $fromColumnLength = $fromColumnLengths[$splitCount]; 792 $toColumnLength = $toColumnLengths[$splitCount]; 793 $fromColumnOffset = $fromColumnOffsets[$splitCount]; 794 $toColumnOffset = $toColumnOffsets[$splitCount]; 795 $fromColumn = $fromColumns[$splitCount][0]; 796 $toColumn = $toColumns[$splitCount][0]; 797 798 if (!empty($fromColumn) && $fromColumn[0] !== '$') { 799 $fromColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($fromColumn) + $numberOfColumns); 800 $formula = substr($formula, 0, $fromColumnOffset) . $fromColumn . substr($formula, $fromColumnOffset + $fromColumnLength); 801 } 802 if (!empty($toColumn) && $toColumn[0] !== '$') { 803 $toColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($toColumn) + $numberOfColumns); 804 $formula = substr($formula, 0, $toColumnOffset) . $toColumn . substr($formula, $toColumnOffset + $toColumnLength); 805 } 806 } 807 808 return $formula; 809 } 810 811 private function updateRowRangesAllWorksheets(string $formula, int $numberOfRows): string 812 { 813 $splitCount = preg_match_all( 814 '/' . Calculation::CALCULATION_REGEXP_ROWRANGE_RELATIVE . '/mui', 815 $formula, 816 $splitRanges, 817 PREG_OFFSET_CAPTURE 818 ); 819 820 $fromRowLengths = array_map('strlen', array_column($splitRanges[1], 0)); 821 $fromRowOffsets = array_column($splitRanges[1], 1); 822 $toRowLengths = array_map('strlen', array_column($splitRanges[2], 0)); 823 $toRowOffsets = array_column($splitRanges[2], 1); 824 825 $fromRows = $splitRanges[1]; 826 $toRows = $splitRanges[2]; 827 828 while ($splitCount > 0) { 829 --$splitCount; 830 $fromRowLength = $fromRowLengths[$splitCount]; 831 $toRowLength = $toRowLengths[$splitCount]; 832 $fromRowOffset = $fromRowOffsets[$splitCount]; 833 $toRowOffset = $toRowOffsets[$splitCount]; 834 $fromRow = $fromRows[$splitCount][0]; 835 $toRow = $toRows[$splitCount][0]; 836 837 if (!empty($fromRow) && $fromRow[0] !== '$') { 838 $fromRow = (int) $fromRow + $numberOfRows; 839 $formula = substr($formula, 0, $fromRowOffset) . $fromRow . substr($formula, $fromRowOffset + $fromRowLength); 840 } 841 if (!empty($toRow) && $toRow[0] !== '$') { 842 $toRow = (int) $toRow + $numberOfRows; 843 $formula = substr($formula, 0, $toRowOffset) . $toRow . substr($formula, $toRowOffset + $toRowLength); 844 } 845 } 846 847 return $formula; 848 } 849 850 /** 851 * Update cell reference. 852 * 853 * @param string $cellReference Cell address or range of addresses 854 * 855 * @return string Updated cell range 856 */ 857 private function updateCellReference($cellReference = 'A1', bool $includeAbsoluteReferences = false) 858 { 859 // Is it in another worksheet? Will not have to update anything. 860 if (strpos($cellReference, '!') !== false) { 861 return $cellReference; 862 } 863 // Is it a range or a single cell? 864 if (!Coordinate::coordinateIsRange($cellReference)) { 865 // Single cell 866 return $this->cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences); 867 } 868 869 // Range 870 return $this->updateCellRange($cellReference, $includeAbsoluteReferences); 871 } 872 873 /** 874 * Update named formulae (i.e. containing worksheet references / named ranges). 875 * 876 * @param Spreadsheet $spreadsheet Object to update 877 * @param string $oldName Old name (name to replace) 878 * @param string $newName New name 879 */ 880 public function updateNamedFormulae(Spreadsheet $spreadsheet, $oldName = '', $newName = ''): void 881 { 882 if ($oldName == '') { 883 return; 884 } 885 886 foreach ($spreadsheet->getWorksheetIterator() as $sheet) { 887 foreach ($sheet->getCoordinates(false) as $coordinate) { 888 $cell = $sheet->getCell($coordinate); 889 if ($cell->getDataType() === DataType::TYPE_FORMULA) { 890 $formula = $cell->getValue(); 891 if (strpos($formula, $oldName) !== false) { 892 $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula); 893 $formula = str_replace($oldName . '!', $newName . '!', $formula); 894 $cell->setValueExplicit($formula, DataType::TYPE_FORMULA); 895 } 896 } 897 } 898 } 899 } 900 901 private function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void 902 { 903 foreach ($worksheet->getParentOrThrow()->getDefinedNames() as $definedName) { 904 if ($definedName->isFormula() === false) { 905 $this->updateNamedRange($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); 906 } else { 907 $this->updateNamedFormula($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); 908 } 909 } 910 } 911 912 private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void 913 { 914 $cellAddress = $definedName->getValue(); 915 $asFormula = ($cellAddress[0] === '='); 916 if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { 917 /** 918 * If we delete the entire range that is referenced by a Named Range, MS Excel sets the value to #REF! 919 * PhpSpreadsheet still only does a basic adjustment, so the Named Range will still reference Cells. 920 * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF! 921 * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace 922 * them with a #REF! 923 */ 924 if ($asFormula === true) { 925 $formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true); 926 $definedName->setValue($formula); 927 } else { 928 $definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='), true)); 929 } 930 } 931 } 932 933 private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void 934 { 935 if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { 936 /** 937 * If we delete the entire range that is referenced by a Named Formula, MS Excel sets the value to #REF! 938 * PhpSpreadsheet still only does a basic adjustment, so the Named Formula will still reference Cells. 939 * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF! 940 * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace 941 * them with a #REF! 942 */ 943 $formula = $definedName->getValue(); 944 $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true); 945 $definedName->setValue($formula); 946 } 947 } 948 949 /** 950 * Update cell range. 951 * 952 * @param string $cellRange Cell range (e.g. 'B2:D4', 'B:C' or '2:3') 953 * 954 * @return string Updated cell range 955 */ 956 private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsoluteReferences = false): string 957 { 958 if (!Coordinate::coordinateIsRange($cellRange)) { 959 throw new Exception('Only cell ranges may be passed to this method.'); 960 } 961 962 // Update range 963 $range = Coordinate::splitRange($cellRange); 964 $ic = count($range); 965 for ($i = 0; $i < $ic; ++$i) { 966 $jc = count($range[$i]); 967 for ($j = 0; $j < $jc; ++$j) { 968 if (ctype_alpha($range[$i][$j])) { 969 $range[$i][$j] = Coordinate::coordinateFromString( 970 $this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences) 971 )[0]; 972 } elseif (ctype_digit($range[$i][$j])) { 973 $range[$i][$j] = Coordinate::coordinateFromString( 974 $this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences) 975 )[1]; 976 } else { 977 $range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences); 978 } 979 } 980 } 981 982 // Recreate range string 983 return Coordinate::buildRange($range); 984 } 985 986 private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void 987 { 988 $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn + $numberOfColumns); 989 $endColumnId = Coordinate::stringFromColumnIndex($beforeColumn); 990 991 for ($row = 1; $row <= $highestRow - 1; ++$row) { 992 for ($column = $startColumnId; $column !== $endColumnId; ++$column) { 993 $coordinate = $column . $row; 994 $this->clearStripCell($worksheet, $coordinate); 995 } 996 } 997 } 998 999 private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void 1000 { 1001 $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn); 1002 ++$highestColumn; 1003 1004 for ($column = $startColumnId; $column !== $highestColumn; ++$column) { 1005 for ($row = $beforeRow + $numberOfRows; $row <= $beforeRow - 1; ++$row) { 1006 $coordinate = $column . $row; 1007 $this->clearStripCell($worksheet, $coordinate); 1008 } 1009 } 1010 } 1011 1012 private function clearStripCell(Worksheet $worksheet, string $coordinate): void 1013 { 1014 $worksheet->removeConditionalStyles($coordinate); 1015 $worksheet->setHyperlink($coordinate); 1016 $worksheet->setDataValidation($coordinate); 1017 $worksheet->removeComment($coordinate); 1018 1019 if ($worksheet->cellExists($coordinate)) { 1020 $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); 1021 $worksheet->getCell($coordinate)->setXfIndex(0); 1022 } 1023 } 1024 1025 private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void 1026 { 1027 $autoFilter = $worksheet->getAutoFilter(); 1028 $autoFilterRange = $autoFilter->getRange(); 1029 if (!empty($autoFilterRange)) { 1030 if ($numberOfColumns !== 0) { 1031 $autoFilterColumns = $autoFilter->getColumns(); 1032 if (count($autoFilterColumns) > 0) { 1033 $column = ''; 1034 $row = 0; 1035 sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); 1036 $columnIndex = Coordinate::columnIndexFromString((string) $column); 1037 [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange); 1038 if ($columnIndex <= $rangeEnd[0]) { 1039 if ($numberOfColumns < 0) { 1040 $this->adjustAutoFilterDeleteRules($columnIndex, $numberOfColumns, $autoFilterColumns, $autoFilter); 1041 } 1042 $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; 1043 1044 // Shuffle columns in autofilter range 1045 if ($numberOfColumns > 0) { 1046 $this->adjustAutoFilterInsert($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter); 1047 } else { 1048 $this->adjustAutoFilterDelete($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter); 1049 } 1050 } 1051 } 1052 } 1053 1054 $worksheet->setAutoFilter( 1055 $this->updateCellReference($autoFilterRange) 1056 ); 1057 } 1058 } 1059 1060 private function adjustAutoFilterDeleteRules(int $columnIndex, int $numberOfColumns, array $autoFilterColumns, AutoFilter $autoFilter): void 1061 { 1062 // If we're actually deleting any columns that fall within the autofilter range, 1063 // then we delete any rules for those columns 1064 $deleteColumn = $columnIndex + $numberOfColumns - 1; 1065 $deleteCount = abs($numberOfColumns); 1066 1067 for ($i = 1; $i <= $deleteCount; ++$i) { 1068 $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1); 1069 if (isset($autoFilterColumns[$columnName])) { 1070 $autoFilter->clearColumn($columnName); 1071 } 1072 ++$deleteColumn; 1073 } 1074 } 1075 1076 private function adjustAutoFilterInsert(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void 1077 { 1078 $startColRef = $startCol; 1079 $endColRef = $rangeEnd; 1080 $toColRef = $rangeEnd + $numberOfColumns; 1081 1082 do { 1083 $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); 1084 --$endColRef; 1085 --$toColRef; 1086 } while ($startColRef <= $endColRef); 1087 } 1088 1089 private function adjustAutoFilterDelete(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void 1090 { 1091 // For delete, we shuffle from beginning to end to avoid overwriting 1092 $startColID = Coordinate::stringFromColumnIndex($startCol); 1093 $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); 1094 $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1); 1095 1096 do { 1097 $autoFilter->shiftColumn($startColID, $toColID); 1098 ++$startColID; 1099 ++$toColID; 1100 } while ($startColID !== $endColID); 1101 } 1102 1103 private function adjustTable(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void 1104 { 1105 $tableCollection = $worksheet->getTableCollection(); 1106 1107 foreach ($tableCollection as $table) { 1108 $tableRange = $table->getRange(); 1109 if (!empty($tableRange)) { 1110 if ($numberOfColumns !== 0) { 1111 $tableColumns = $table->getColumns(); 1112 if (count($tableColumns) > 0) { 1113 $column = ''; 1114 $row = 0; 1115 sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); 1116 $columnIndex = Coordinate::columnIndexFromString((string) $column); 1117 [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($tableRange); 1118 if ($columnIndex <= $rangeEnd[0]) { 1119 if ($numberOfColumns < 0) { 1120 $this->adjustTableDeleteRules($columnIndex, $numberOfColumns, $tableColumns, $table); 1121 } 1122 $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; 1123 1124 // Shuffle columns in table range 1125 if ($numberOfColumns > 0) { 1126 $this->adjustTableInsert($startCol, $numberOfColumns, $rangeEnd[0], $table); 1127 } else { 1128 $this->adjustTableDelete($startCol, $numberOfColumns, $rangeEnd[0], $table); 1129 } 1130 } 1131 } 1132 } 1133 1134 $table->setRange($this->updateCellReference($tableRange)); 1135 } 1136 } 1137 } 1138 1139 private function adjustTableDeleteRules(int $columnIndex, int $numberOfColumns, array $tableColumns, Table $table): void 1140 { 1141 // If we're actually deleting any columns that fall within the table range, 1142 // then we delete any rules for those columns 1143 $deleteColumn = $columnIndex + $numberOfColumns - 1; 1144 $deleteCount = abs($numberOfColumns); 1145 1146 for ($i = 1; $i <= $deleteCount; ++$i) { 1147 $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1); 1148 if (isset($tableColumns[$columnName])) { 1149 $table->clearColumn($columnName); 1150 } 1151 ++$deleteColumn; 1152 } 1153 } 1154 1155 private function adjustTableInsert(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void 1156 { 1157 $startColRef = $startCol; 1158 $endColRef = $rangeEnd; 1159 $toColRef = $rangeEnd + $numberOfColumns; 1160 1161 do { 1162 $table->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); 1163 --$endColRef; 1164 --$toColRef; 1165 } while ($startColRef <= $endColRef); 1166 } 1167 1168 private function adjustTableDelete(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void 1169 { 1170 // For delete, we shuffle from beginning to end to avoid overwriting 1171 $startColID = Coordinate::stringFromColumnIndex($startCol); 1172 $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); 1173 $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1); 1174 1175 do { 1176 $table->shiftColumn($startColID, $toColID); 1177 ++$startColID; 1178 ++$toColID; 1179 } while ($startColID !== $endColID); 1180 } 1181 1182 private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn, int $beforeRow, int $highestRow, int $numberOfColumns): void 1183 { 1184 $beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1); 1185 for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) { 1186 // Style 1187 $coordinate = $beforeColumnName . $i; 1188 if ($worksheet->cellExists($coordinate)) { 1189 $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); 1190 for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) { 1191 $worksheet->getCell([$j, $i])->setXfIndex($xfIndex); 1192 } 1193 } 1194 } 1195 } 1196 1197 private function duplicateStylesByRow(Worksheet $worksheet, int $beforeColumn, int $beforeRow, string $highestColumn, int $numberOfRows): void 1198 { 1199 $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); 1200 for ($i = $beforeColumn; $i <= $highestColumnIndex; ++$i) { 1201 // Style 1202 $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); 1203 if ($worksheet->cellExists($coordinate)) { 1204 $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); 1205 for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) { 1206 $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); 1207 } 1208 } 1209 } 1210 } 1211 1212 /** 1213 * __clone implementation. Cloning should not be allowed in a Singleton! 1214 */ 1215 final public function __clone() 1216 { 1217 throw new Exception('Cloning a Singleton is not allowed!'); 1218 } 1219 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body