Passed
Push — master ( b20f5c...b636c5 )
by Adrien
29:30 queued 11:53
created

ReferenceHelper::updateFormulaReferences()   D

Complexity

Conditions 34
Paths 50

Size

Total Lines 128
Code Lines 85

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 55
CRAP Score 84.8057

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 34
eloc 85
c 1
b 0
f 0
nc 50
nop 5
dl 0
loc 128
ccs 55
cts 85
cp 0.6471
crap 84.8057
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet;
4
5
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6
use PhpOffice\PhpSpreadsheet\Cell\DataType;
7
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
8
9
class ReferenceHelper
10
{
11
    /**    Constants                */
12
    /**    Regular Expressions      */
13
    const REFHELPER_REGEXP_CELLREF = '((\w*|\'[^!]*\')!)?(?<![:a-z\$])(\$?[a-z]{1,3}\$?\d+)(?=[^:!\d\'])';
14
    const REFHELPER_REGEXP_CELLRANGE = '((\w*|\'[^!]*\')!)?(\$?[a-z]{1,3}\$?\d+):(\$?[a-z]{1,3}\$?\d+)';
15
    const REFHELPER_REGEXP_ROWRANGE = '((\w*|\'[^!]*\')!)?(\$?\d+):(\$?\d+)';
16
    const REFHELPER_REGEXP_COLRANGE = '((\w*|\'[^!]*\')!)?(\$?[a-z]{1,3}):(\$?[a-z]{1,3})';
17
18
    /**
19
     * Instance of this class.
20
     *
21
     * @var ReferenceHelper
22
     */
23
    private static $instance;
24
25
    /**
26
     * Get an instance of this class.
27
     *
28
     * @return ReferenceHelper
29
     */
30 109
    public static function getInstance()
31
    {
32 109
        if (!isset(self::$instance) || (self::$instance === null)) {
33 41
            self::$instance = new self();
34
        }
35
36 109
        return self::$instance;
37
    }
38
39
    /**
40
     * Create a new ReferenceHelper.
41
     */
42 41
    protected function __construct()
43
    {
44 41
    }
45
46
    /**
47
     * Compare two column addresses
48
     * Intended for use as a Callback function for sorting column addresses by column.
49
     *
50
     * @param string $a First column to test (e.g. 'AA')
51
     * @param string $b Second column to test (e.g. 'Z')
52
     *
53
     * @return int
54
     */
55 1
    public static function columnSort($a, $b)
56
    {
57 1
        return strcasecmp(strlen($a) . $a, strlen($b) . $b);
58
    }
59
60
    /**
61
     * Compare two column addresses
62
     * Intended for use as a Callback function for reverse sorting column addresses by column.
63
     *
64
     * @param string $a First column to test (e.g. 'AA')
65
     * @param string $b Second column to test (e.g. 'Z')
66
     *
67
     * @return int
68
     */
69 1
    public static function columnReverseSort($a, $b)
70
    {
71 1
        return 1 - strcasecmp(strlen($a) . $a, strlen($b) . $b);
72
    }
73
74
    /**
75
     * Compare two cell addresses
76
     * Intended for use as a Callback function for sorting cell addresses by column and row.
77
     *
78
     * @param string $a First cell to test (e.g. 'AA1')
79
     * @param string $b Second cell to test (e.g. 'Z1')
80
     *
81
     * @return int
82
     */
83 13
    public static function cellSort($a, $b)
84
    {
85 13
        [$ac, $ar] = sscanf($a, '%[A-Z]%d');
86 13
        [$bc, $br] = sscanf($b, '%[A-Z]%d');
87
88 13
        if ($ar === $br) {
89 1
            return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
90
        }
91
92 13
        return ($ar < $br) ? -1 : 1;
93
    }
94
95
    /**
96
     * Compare two cell addresses
97
     * Intended for use as a Callback function for sorting cell addresses by column and row.
98
     *
99
     * @param string $a First cell to test (e.g. 'AA1')
100
     * @param string $b Second cell to test (e.g. 'Z1')
101
     *
102
     * @return int
103
     */
104 13
    public static function cellReverseSort($a, $b)
105
    {
106 13
        [$ac, $ar] = sscanf($a, '%[A-Z]%d');
107 13
        [$bc, $br] = sscanf($b, '%[A-Z]%d');
108
109 13
        if ($ar === $br) {
110 1
            return 1 - strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
111
        }
112
113 13
        return ($ar < $br) ? 1 : -1;
114
    }
115
116
    /**
117
     * Test whether a cell address falls within a defined range of cells.
118
     *
119
     * @param string $cellAddress Address of the cell we're testing
120
     * @param int $beforeRow Number of the row we're inserting/deleting before
121
     * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion)
122
     * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before
123
     * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion)
124
     *
125
     * @return bool
126
     */
127 12
    private static function cellAddressInDeleteRange($cellAddress, $beforeRow, $pNumRows, $beforeColumnIndex, $pNumCols)
128
    {
129 12
        [$cellColumn, $cellRow] = Coordinate::coordinateFromString($cellAddress);
130 12
        $cellColumnIndex = Coordinate::columnIndexFromString($cellColumn);
131
        //    Is cell within the range of rows/columns if we're deleting
132 12
        if ($pNumRows < 0 &&
133 12
            ($cellRow >= ($beforeRow + $pNumRows)) &&
134 12
            ($cellRow < $beforeRow)) {
135
            return true;
136 12
        } elseif ($pNumCols < 0 &&
137 12
            ($cellColumnIndex >= ($beforeColumnIndex + $pNumCols)) &&
138 12
            ($cellColumnIndex < $beforeColumnIndex)) {
139
            return true;
140
        }
141
142 12
        return false;
143
    }
144
145
    /**
146
     * Update page breaks when inserting/deleting rows/columns.
147
     *
148
     * @param Worksheet $pSheet The worksheet that we're editing
149
     * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1')
150
     * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before
151
     * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion)
152
     * @param int $beforeRow Number of the row we're inserting/deleting before
153
     * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion)
154
     */
155 19
    protected function adjustPageBreaks(Worksheet $pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)
156
    {
157 19
        $aBreaks = $pSheet->getBreaks();
158 19
        ($pNumCols > 0 || $pNumRows > 0) ?
159 19
            uksort($aBreaks, ['self', 'cellReverseSort']) : uksort($aBreaks, ['self', 'cellSort']);
160
161 19
        foreach ($aBreaks as $key => $value) {
162
            if (self::cellAddressInDeleteRange($key, $beforeRow, $pNumRows, $beforeColumnIndex, $pNumCols)) {
163
                //    If we're deleting, then clear any defined breaks that are within the range
164
                //        of rows/columns that we're deleting
165
                $pSheet->setBreak($key, Worksheet::BREAK_NONE);
166
            } else {
167
                //    Otherwise update any affected breaks by inserting a new break at the appropriate point
168
                //        and removing the old affected break
169
                $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows);
170
                if ($key != $newReference) {
171
                    $pSheet->setBreak($newReference, $value)
172
                        ->setBreak($key, Worksheet::BREAK_NONE);
173
                }
174
            }
175
        }
176 19
    }
177
178
    /**
179
     * Update cell comments when inserting/deleting rows/columns.
180
     *
181
     * @param Worksheet $pSheet The worksheet that we're editing
182
     * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1')
183
     * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before
184
     * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion)
185
     * @param int $beforeRow Number of the row we're inserting/deleting before
186
     * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion)
187
     */
188 19
    protected function adjustComments($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)
189
    {
190 19
        $aComments = $pSheet->getComments();
191 19
        $aNewComments = []; // the new array of all comments
192
193 19
        foreach ($aComments as $key => &$value) {
194
            // Any comments inside a deleted range will be ignored
195 12
            if (!self::cellAddressInDeleteRange($key, $beforeRow, $pNumRows, $beforeColumnIndex, $pNumCols)) {
196
                // Otherwise build a new array of comments indexed by the adjusted cell reference
197 12
                $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows);
198 12
                $aNewComments[$newReference] = $value;
199
            }
200
        }
201
        //    Replace the comments array with the new set of comments
202 19
        $pSheet->setComments($aNewComments);
203 19
    }
204
205
    /**
206
     * Update hyperlinks when inserting/deleting rows/columns.
207
     *
208
     * @param Worksheet $pSheet The worksheet that we're editing
209
     * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1')
210
     * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before
211
     * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion)
212
     * @param int $beforeRow Number of the row we're inserting/deleting before
213
     * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion)
214
     */
215 19
    protected function adjustHyperlinks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)
0 ignored issues
show
Unused Code introduced by
The parameter $beforeColumnIndex is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

215
    protected function adjustHyperlinks($pSheet, $pBefore, /** @scrutinizer ignore-unused */ $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $beforeRow is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

215
    protected function adjustHyperlinks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-unused */ $beforeRow, $pNumRows)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
216
    {
217 19
        $aHyperlinkCollection = $pSheet->getHyperlinkCollection();
218 19
        ($pNumCols > 0 || $pNumRows > 0) ?
219 19
            uksort($aHyperlinkCollection, ['self', 'cellReverseSort']) : uksort($aHyperlinkCollection, ['self', 'cellSort']);
220
221 19
        foreach ($aHyperlinkCollection as $key => $value) {
222 12
            $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows);
223 12
            if ($key != $newReference) {
224 12
                $pSheet->setHyperlink($newReference, $value);
225 12
                $pSheet->setHyperlink($key, null);
226
            }
227
        }
228 19
    }
229
230
    /**
231
     * Update data validations when inserting/deleting rows/columns.
232
     *
233
     * @param Worksheet $pSheet The worksheet that we're editing
234
     * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1')
235
     * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before
236
     * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion)
237
     * @param int $beforeRow Number of the row we're inserting/deleting before
238
     * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion)
239
     */
240 19
    protected function adjustDataValidations($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)
0 ignored issues
show
Unused Code introduced by
The parameter $beforeRow is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

240
    protected function adjustDataValidations($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-unused */ $beforeRow, $pNumRows)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $beforeColumnIndex is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

240
    protected function adjustDataValidations($pSheet, $pBefore, /** @scrutinizer ignore-unused */ $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
241
    {
242 19
        $aDataValidationCollection = $pSheet->getDataValidationCollection();
243 19
        ($pNumCols > 0 || $pNumRows > 0) ?
244 19
            uksort($aDataValidationCollection, ['self', 'cellReverseSort']) : uksort($aDataValidationCollection, ['self', 'cellSort']);
245
246 19
        foreach ($aDataValidationCollection as $key => $value) {
247
            $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows);
248
            if ($key != $newReference) {
249
                $pSheet->setDataValidation($newReference, $value);
250
                $pSheet->setDataValidation($key, null);
251
            }
252
        }
253 19
    }
254
255
    /**
256
     * Update merged cells when inserting/deleting rows/columns.
257
     *
258
     * @param Worksheet $pSheet The worksheet that we're editing
259
     * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1')
260
     * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before
261
     * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion)
262
     * @param int $beforeRow Number of the row we're inserting/deleting before
263
     * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion)
264
     */
265 19
    protected function adjustMergeCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)
0 ignored issues
show
Unused Code introduced by
The parameter $beforeColumnIndex is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

265
    protected function adjustMergeCells($pSheet, $pBefore, /** @scrutinizer ignore-unused */ $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $beforeRow is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

265
    protected function adjustMergeCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-unused */ $beforeRow, $pNumRows)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
266
    {
267 19
        $aMergeCells = $pSheet->getMergeCells();
268 19
        $aNewMergeCells = []; // the new array of all merge cells
269 19
        foreach ($aMergeCells as $key => &$value) {
270 13
            $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows);
271 13
            $aNewMergeCells[$newReference] = $newReference;
272
        }
273 19
        $pSheet->setMergeCells($aNewMergeCells); // replace the merge cells array
274 19
    }
275
276
    /**
277
     * Update protected cells when inserting/deleting rows/columns.
278
     *
279
     * @param Worksheet $pSheet The worksheet that we're editing
280
     * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1')
281
     * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before
282
     * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion)
283
     * @param int $beforeRow Number of the row we're inserting/deleting before
284
     * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion)
285
     */
286 19
    protected function adjustProtectedCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)
0 ignored issues
show
Unused Code introduced by
The parameter $beforeRow is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

286
    protected function adjustProtectedCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-unused */ $beforeRow, $pNumRows)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $beforeColumnIndex is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

286
    protected function adjustProtectedCells($pSheet, $pBefore, /** @scrutinizer ignore-unused */ $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
287
    {
288 19
        $aProtectedCells = $pSheet->getProtectedCells();
289 19
        ($pNumCols > 0 || $pNumRows > 0) ?
290 19
            uksort($aProtectedCells, ['self', 'cellReverseSort']) : uksort($aProtectedCells, ['self', 'cellSort']);
291 19
        foreach ($aProtectedCells as $key => $value) {
292 11
            $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows);
293 11
            if ($key != $newReference) {
294 11
                $pSheet->protectCells($newReference, $value, true);
295 11
                $pSheet->unprotectCells($key);
296
            }
297
        }
298 19
    }
299
300
    /**
301
     * Update column dimensions when inserting/deleting rows/columns.
302
     *
303
     * @param Worksheet $pSheet The worksheet that we're editing
304
     * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1')
305
     * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before
306
     * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion)
307
     * @param int $beforeRow Number of the row we're inserting/deleting before
308
     * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion)
309
     */
310 19
    protected function adjustColumnDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)
0 ignored issues
show
Unused Code introduced by
The parameter $beforeColumnIndex is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

310
    protected function adjustColumnDimensions($pSheet, $pBefore, /** @scrutinizer ignore-unused */ $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $beforeRow is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

310
    protected function adjustColumnDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-unused */ $beforeRow, $pNumRows)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
311
    {
312 19
        $aColumnDimensions = array_reverse($pSheet->getColumnDimensions(), true);
313 19
        if (!empty($aColumnDimensions)) {
314 14
            foreach ($aColumnDimensions as $objColumnDimension) {
315 14
                $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1', $pBefore, $pNumCols, $pNumRows);
316 14
                [$newReference] = Coordinate::coordinateFromString($newReference);
317 14
                if ($objColumnDimension->getColumnIndex() != $newReference) {
318 11
                    $objColumnDimension->setColumnIndex($newReference);
319
                }
320
            }
321 14
            $pSheet->refreshColumnDimensions();
322
        }
323 19
    }
324
325
    /**
326
     * Update row dimensions when inserting/deleting rows/columns.
327
     *
328
     * @param Worksheet $pSheet The worksheet that we're editing
329
     * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1')
330
     * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before
331
     * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion)
332
     * @param int $beforeRow Number of the row we're inserting/deleting before
333
     * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion)
334
     */
335 19
    protected function adjustRowDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)
0 ignored issues
show
Unused Code introduced by
The parameter $beforeColumnIndex is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

335
    protected function adjustRowDimensions($pSheet, $pBefore, /** @scrutinizer ignore-unused */ $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
336
    {
337 19
        $aRowDimensions = array_reverse($pSheet->getRowDimensions(), true);
338 19
        if (!empty($aRowDimensions)) {
339 2
            foreach ($aRowDimensions as $objRowDimension) {
340 2
                $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex(), $pBefore, $pNumCols, $pNumRows);
341 2
                [, $newReference] = Coordinate::coordinateFromString($newReference);
342 2
                if ($objRowDimension->getRowIndex() != $newReference) {
343 2
                    $objRowDimension->setRowIndex($newReference);
344
                }
345
            }
346 2
            $pSheet->refreshRowDimensions();
347
348 2
            $copyDimension = $pSheet->getRowDimension($beforeRow - 1);
349 2
            for ($i = $beforeRow; $i <= $beforeRow - 1 + $pNumRows; ++$i) {
350 1
                $newDimension = $pSheet->getRowDimension($i);
351 1
                $newDimension->setRowHeight($copyDimension->getRowHeight());
352 1
                $newDimension->setVisible($copyDimension->getVisible());
353 1
                $newDimension->setOutlineLevel($copyDimension->getOutlineLevel());
354 1
                $newDimension->setCollapsed($copyDimension->getCollapsed());
355
            }
356
        }
357 19
    }
358
359
    /**
360
     * Insert a new column or row, updating all possible related data.
361
     *
362
     * @param string $pBefore Insert before this cell address (e.g. 'A1')
363
     * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion)
364
     * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion)
365
     * @param Worksheet $pSheet The worksheet that we're editing
366
     *
367
     * @throws Exception
368
     */
369 19
    public function insertNewBefore($pBefore, $pNumCols, $pNumRows, Worksheet $pSheet)
370
    {
371 19
        $remove = ($pNumCols < 0 || $pNumRows < 0);
372 19
        $allCoordinates = $pSheet->getCoordinates();
373
374
        // Get coordinate of $pBefore
375 19
        [$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($pBefore);
376 19
        $beforeColumnIndex = Coordinate::columnIndexFromString($beforeColumn);
377
378
        // Clear cells if we are removing columns or rows
379 19
        $highestColumn = $pSheet->getHighestColumn();
380 19
        $highestRow = $pSheet->getHighestRow();
381
382
        // 1. Clear column strips if we are removing columns
383 19
        if ($pNumCols < 0 && $beforeColumnIndex - 2 + $pNumCols > 0) {
384 12
            for ($i = 1; $i <= $highestRow - 1; ++$i) {
385 12
                for ($j = $beforeColumnIndex - 1 + $pNumCols; $j <= $beforeColumnIndex - 2; ++$j) {
386 12
                    $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i;
387 12
                    $pSheet->removeConditionalStyles($coordinate);
388 12
                    if ($pSheet->cellExists($coordinate)) {
389 12
                        $pSheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL);
390 12
                        $pSheet->getCell($coordinate)->setXfIndex(0);
391
                    }
392
                }
393
            }
394
        }
395
396
        // 2. Clear row strips if we are removing rows
397 19
        if ($pNumRows < 0 && $beforeRow - 1 + $pNumRows > 0) {
398 13
            for ($i = $beforeColumnIndex - 1; $i <= Coordinate::columnIndexFromString($highestColumn) - 1; ++$i) {
399 13
                for ($j = $beforeRow + $pNumRows; $j <= $beforeRow - 1; ++$j) {
400 13
                    $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j;
401 13
                    $pSheet->removeConditionalStyles($coordinate);
402 13
                    if ($pSheet->cellExists($coordinate)) {
403
                        $pSheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL);
404
                        $pSheet->getCell($coordinate)->setXfIndex(0);
405
                    }
406
                }
407
            }
408
        }
409
410
        // Loop through cells, bottom-up, and change cell coordinate
411 19
        if ($remove) {
412
            // It's faster to reverse and pop than to use unshift, especially with large cell collections
413 19
            $allCoordinates = array_reverse($allCoordinates);
414
        }
415 19
        while ($coordinate = array_pop($allCoordinates)) {
416 19
            $cell = $pSheet->getCell($coordinate);
417 19
            $cellIndex = Coordinate::columnIndexFromString($cell->getColumn());
418
419 19
            if ($cellIndex - 1 + $pNumCols < 0) {
420 15
                continue;
421
            }
422
423
            // New coordinate
424 18
            $newCoordinate = Coordinate::stringFromColumnIndex($cellIndex + $pNumCols) . ($cell->getRow() + $pNumRows);
425
426
            // Should the cell be updated? Move value and cellXf index from one cell to another.
427 18
            if (($cellIndex >= $beforeColumnIndex) && ($cell->getRow() >= $beforeRow)) {
428
                // Update cell styles
429 17
                $pSheet->getCell($newCoordinate)->setXfIndex($cell->getXfIndex());
430
431
                // Insert this cell at its new location
432 17
                if ($cell->getDataType() == DataType::TYPE_FORMULA) {
433
                    // Formula should be adjusted
434 12
                    $pSheet->getCell($newCoordinate)
435 12
                        ->setValue($this->updateFormulaReferences($cell->getValue(), $pBefore, $pNumCols, $pNumRows, $pSheet->getTitle()));
436
                } else {
437
                    // Formula should not be adjusted
438 17
                    $pSheet->getCell($newCoordinate)->setValue($cell->getValue());
439
                }
440
441
                // Clear the original cell
442 17
                $pSheet->getCellCollection()->delete($coordinate);
443
            } else {
444
                /*    We don't need to update styles for rows/columns before our insertion position,
445
                        but we do still need to adjust any formulae    in those cells                    */
446 15
                if ($cell->getDataType() == DataType::TYPE_FORMULA) {
447
                    // Formula should be adjusted
448 12
                    $cell->setValue($this->updateFormulaReferences($cell->getValue(), $pBefore, $pNumCols, $pNumRows, $pSheet->getTitle()));
449
                }
450
            }
451
        }
452
453
        // Duplicate styles for the newly inserted cells
454 19
        $highestColumn = $pSheet->getHighestColumn();
455 19
        $highestRow = $pSheet->getHighestRow();
456
457 19
        if ($pNumCols > 0 && $beforeColumnIndex - 2 > 0) {
458 11
            for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) {
459
                // Style
460 11
                $coordinate = Coordinate::stringFromColumnIndex($beforeColumnIndex - 1) . $i;
461 11
                if ($pSheet->cellExists($coordinate)) {
462 11
                    $xfIndex = $pSheet->getCell($coordinate)->getXfIndex();
463 11
                    $conditionalStyles = $pSheet->conditionalStylesExists($coordinate) ?
464 11
                        $pSheet->getConditionalStyles($coordinate) : false;
465 11
                    for ($j = $beforeColumnIndex; $j <= $beforeColumnIndex - 1 + $pNumCols; ++$j) {
466 11
                        $pSheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex);
0 ignored issues
show
Bug introduced by
$i of type string is incompatible with the type integer expected by parameter $row of PhpOffice\PhpSpreadsheet...getCellByColumnAndRow(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

466
                        $pSheet->getCellByColumnAndRow($j, /** @scrutinizer ignore-type */ $i)->setXfIndex($xfIndex);
Loading history...
467 11
                        if ($conditionalStyles) {
468
                            $cloned = [];
469
                            foreach ($conditionalStyles as $conditionalStyle) {
470
                                $cloned[] = clone $conditionalStyle;
471
                            }
472
                            $pSheet->setConditionalStyles(Coordinate::stringFromColumnIndex($j) . $i, $cloned);
473
                        }
474
                    }
475
                }
476
            }
477
        }
478
479 19
        if ($pNumRows > 0 && $beforeRow - 1 > 0) {
480 12
            for ($i = $beforeColumnIndex; $i <= Coordinate::columnIndexFromString($highestColumn); ++$i) {
481
                // Style
482 12
                $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1);
483 12
                if ($pSheet->cellExists($coordinate)) {
484 12
                    $xfIndex = $pSheet->getCell($coordinate)->getXfIndex();
485 12
                    $conditionalStyles = $pSheet->conditionalStylesExists($coordinate) ?
486 12
                        $pSheet->getConditionalStyles($coordinate) : false;
487 12
                    for ($j = $beforeRow; $j <= $beforeRow - 1 + $pNumRows; ++$j) {
488 12
                        $pSheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex);
489 12
                        if ($conditionalStyles) {
490
                            $cloned = [];
491
                            foreach ($conditionalStyles as $conditionalStyle) {
492
                                $cloned[] = clone $conditionalStyle;
493
                            }
494
                            $pSheet->setConditionalStyles(Coordinate::stringFromColumnIndex($i) . $j, $cloned);
495
                        }
496
                    }
497
                }
498
            }
499
        }
500
501
        // Update worksheet: column dimensions
502 19
        $this->adjustColumnDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows);
0 ignored issues
show
Bug introduced by
$beforeRow of type string is incompatible with the type integer expected by parameter $beforeRow of PhpOffice\PhpSpreadsheet...djustColumnDimensions(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

502
        $this->adjustColumnDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-type */ $beforeRow, $pNumRows);
Loading history...
503
504
        // Update worksheet: row dimensions
505 19
        $this->adjustRowDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows);
0 ignored issues
show
Bug introduced by
$beforeRow of type string is incompatible with the type integer expected by parameter $beforeRow of PhpOffice\PhpSpreadsheet...::adjustRowDimensions(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

505
        $this->adjustRowDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-type */ $beforeRow, $pNumRows);
Loading history...
506
507
        //    Update worksheet: page breaks
508 19
        $this->adjustPageBreaks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows);
0 ignored issues
show
Bug introduced by
$beforeRow of type string is incompatible with the type integer expected by parameter $beforeRow of PhpOffice\PhpSpreadsheet...per::adjustPageBreaks(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

508
        $this->adjustPageBreaks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-type */ $beforeRow, $pNumRows);
Loading history...
509
510
        //    Update worksheet: comments
511 19
        $this->adjustComments($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows);
0 ignored issues
show
Bug introduced by
$beforeRow of type string is incompatible with the type integer expected by parameter $beforeRow of PhpOffice\PhpSpreadsheet...elper::adjustComments(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

511
        $this->adjustComments($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-type */ $beforeRow, $pNumRows);
Loading history...
512
513
        // Update worksheet: hyperlinks
514 19
        $this->adjustHyperlinks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows);
0 ignored issues
show
Bug introduced by
$beforeRow of type string is incompatible with the type integer expected by parameter $beforeRow of PhpOffice\PhpSpreadsheet...per::adjustHyperlinks(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

514
        $this->adjustHyperlinks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-type */ $beforeRow, $pNumRows);
Loading history...
515
516
        // Update worksheet: data validations
517 19
        $this->adjustDataValidations($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows);
0 ignored issues
show
Bug introduced by
$beforeRow of type string is incompatible with the type integer expected by parameter $beforeRow of PhpOffice\PhpSpreadsheet...adjustDataValidations(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

517
        $this->adjustDataValidations($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-type */ $beforeRow, $pNumRows);
Loading history...
518
519
        // Update worksheet: merge cells
520 19
        $this->adjustMergeCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows);
0 ignored issues
show
Bug introduced by
$beforeRow of type string is incompatible with the type integer expected by parameter $beforeRow of PhpOffice\PhpSpreadsheet...per::adjustMergeCells(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

520
        $this->adjustMergeCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-type */ $beforeRow, $pNumRows);
Loading history...
521
522
        // Update worksheet: protected cells
523 19
        $this->adjustProtectedCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows);
0 ignored issues
show
Bug introduced by
$beforeRow of type string is incompatible with the type integer expected by parameter $beforeRow of PhpOffice\PhpSpreadsheet...:adjustProtectedCells(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

523
        $this->adjustProtectedCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, /** @scrutinizer ignore-type */ $beforeRow, $pNumRows);
Loading history...
524
525
        // Update worksheet: autofilter
526 19
        $autoFilter = $pSheet->getAutoFilter();
527 19
        $autoFilterRange = $autoFilter->getRange();
528 19
        if (!empty($autoFilterRange)) {
529
            if ($pNumCols != 0) {
530
                $autoFilterColumns = $autoFilter->getColumns();
531
                if (count($autoFilterColumns) > 0) {
532
                    $column = '';
533
                    $row = 0;
534
                    sscanf($pBefore, '%[A-Z]%d', $column, $row);
535
                    $columnIndex = Coordinate::columnIndexFromString($column);
536
                    [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange);
537
                    if ($columnIndex <= $rangeEnd[0]) {
538
                        if ($pNumCols < 0) {
539
                            //    If we're actually deleting any columns that fall within the autofilter range,
540
                            //        then we delete any rules for those columns
541
                            $deleteColumn = $columnIndex + $pNumCols - 1;
542
                            $deleteCount = abs($pNumCols);
543
                            for ($i = 1; $i <= $deleteCount; ++$i) {
544
                                if (isset($autoFilterColumns[Coordinate::stringFromColumnIndex($deleteColumn + 1)])) {
545
                                    $autoFilter->clearColumn(Coordinate::stringFromColumnIndex($deleteColumn + 1));
546
                                }
547
                                ++$deleteColumn;
548
                            }
549
                        }
550
                        $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0];
551
552
                        //    Shuffle columns in autofilter range
553
                        if ($pNumCols > 0) {
554
                            $startColRef = $startCol;
555
                            $endColRef = $rangeEnd[0];
556
                            $toColRef = $rangeEnd[0] + $pNumCols;
557
558
                            do {
559
                                $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef));
560
                                --$endColRef;
561
                                --$toColRef;
562
                            } while ($startColRef <= $endColRef);
563
                        } else {
564
                            //    For delete, we shuffle from beginning to end to avoid overwriting
565
                            $startColID = Coordinate::stringFromColumnIndex($startCol);
566
                            $toColID = Coordinate::stringFromColumnIndex($startCol + $pNumCols);
567
                            $endColID = Coordinate::stringFromColumnIndex($rangeEnd[0] + 1);
568
                            do {
569
                                $autoFilter->shiftColumn($startColID, $toColID);
570
                                ++$startColID;
571
                                ++$toColID;
572
                            } while ($startColID != $endColID);
573
                        }
574
                    }
575
                }
576
            }
577
            $pSheet->setAutoFilter($this->updateCellReference($autoFilterRange, $pBefore, $pNumCols, $pNumRows));
578
        }
579
580
        // Update worksheet: freeze pane
581 19
        if ($pSheet->getFreezePane()) {
582 1
            $splitCell = $pSheet->getFreezePane();
583 1
            $topLeftCell = $pSheet->getTopLeftCell();
584
585 1
            $splitCell = $this->updateCellReference($splitCell, $pBefore, $pNumCols, $pNumRows);
586 1
            $topLeftCell = $this->updateCellReference($topLeftCell, $pBefore, $pNumCols, $pNumRows);
587
588 1
            $pSheet->freezePane($splitCell, $topLeftCell);
589
        }
590
591
        // Page setup
592 19
        if ($pSheet->getPageSetup()->isPrintAreaSet()) {
593
            $pSheet->getPageSetup()->setPrintArea($this->updateCellReference($pSheet->getPageSetup()->getPrintArea(), $pBefore, $pNumCols, $pNumRows));
594
        }
595
596
        // Update worksheet: drawings
597 19
        $aDrawings = $pSheet->getDrawingCollection();
598 19
        foreach ($aDrawings as $objDrawing) {
599 12
            $newReference = $this->updateCellReference($objDrawing->getCoordinates(), $pBefore, $pNumCols, $pNumRows);
600 12
            if ($objDrawing->getCoordinates() != $newReference) {
601 12
                $objDrawing->setCoordinates($newReference);
602
            }
603
        }
604
605
        // Update workbook: named ranges
606 19
        if (count($pSheet->getParent()->getNamedRanges()) > 0) {
607 1
            foreach ($pSheet->getParent()->getNamedRanges() as $namedRange) {
608 1
                if ($namedRange->getWorksheet()->getHashCode() == $pSheet->getHashCode()) {
609 1
                    $namedRange->setRange($this->updateCellReference($namedRange->getRange(), $pBefore, $pNumCols, $pNumRows));
610
                }
611
            }
612
        }
613
614
        // Garbage collect
615 19
        $pSheet->garbageCollect();
616 19
    }
617
618
    /**
619
     * Update references within formulas.
620
     *
621
     * @param string $pFormula Formula to update
622
     * @param string $pBefore Insert before this one
623
     * @param int $pNumCols Number of columns to insert
624
     * @param int $pNumRows Number of rows to insert
625
     * @param string $sheetName Worksheet name/title
626
     *
627
     * @throws Exception
628
     *
629
     * @return string Updated formula
630
     */
631 16
    public function updateFormulaReferences($pFormula = '', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0, $sheetName = '')
632
    {
633
        //    Update cell references in the formula
634 16
        $formulaBlocks = explode('"', $pFormula);
635 16
        $i = false;
636 16
        foreach ($formulaBlocks as &$formulaBlock) {
637
            //    Ignore blocks that were enclosed in quotes (alternating entries in the $formulaBlocks array after the explode)
638 16
            if ($i = !$i) {
0 ignored issues
show
introduced by
The condition $i is always false.
Loading history...
639 16
                $adjustCount = 0;
640 16
                $newCellTokens = $cellTokens = [];
641
                //    Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5)
642 16
                $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER);
643 16
                if ($matchCount > 0) {
644
                    foreach ($matches as $match) {
645
                        $fromString = ($match[2] > '') ? $match[2] . '!' : '';
646
                        $fromString .= $match[3] . ':' . $match[4];
647
                        $modified3 = substr($this->updateCellReference('$A' . $match[3], $pBefore, $pNumCols, $pNumRows), 2);
648
                        $modified4 = substr($this->updateCellReference('$A' . $match[4], $pBefore, $pNumCols, $pNumRows), 2);
649
650
                        if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
651
                            if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) {
652
                                $toString = ($match[2] > '') ? $match[2] . '!' : '';
653
                                $toString .= $modified3 . ':' . $modified4;
654
                                //    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
655
                                $column = 100000;
656
                                $row = 10000000 + trim($match[3], '$');
657
                                $cellIndex = $column . $row;
658
659
                                $newCellTokens[$cellIndex] = preg_quote($toString, '/');
660
                                $cellTokens[$cellIndex] = '/(?<!\d\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
661
                                ++$adjustCount;
662
                            }
663
                        }
664
                    }
665
                }
666
                //    Search for column ranges (e.g. 'Sheet1'!C:E or C:E) with or without $ absolutes (e.g. $C:E)
667 16
                $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER);
668 16
                if ($matchCount > 0) {
669
                    foreach ($matches as $match) {
670
                        $fromString = ($match[2] > '') ? $match[2] . '!' : '';
671
                        $fromString .= $match[3] . ':' . $match[4];
672
                        $modified3 = substr($this->updateCellReference($match[3] . '$1', $pBefore, $pNumCols, $pNumRows), 0, -2);
673
                        $modified4 = substr($this->updateCellReference($match[4] . '$1', $pBefore, $pNumCols, $pNumRows), 0, -2);
674
675
                        if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
676
                            if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) {
677
                                $toString = ($match[2] > '') ? $match[2] . '!' : '';
678
                                $toString .= $modified3 . ':' . $modified4;
679
                                //    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
680
                                $column = Coordinate::columnIndexFromString(trim($match[3], '$')) + 100000;
681
                                $row = 10000000;
682
                                $cellIndex = $column . $row;
683
684
                                $newCellTokens[$cellIndex] = preg_quote($toString, '/');
685
                                $cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?![A-Z])/i';
686
                                ++$adjustCount;
687
                            }
688
                        }
689
                    }
690
                }
691
                //    Search for cell ranges (e.g. 'Sheet1'!A3:C5 or A3:C5) with or without $ absolutes (e.g. $A1:C$5)
692 16
                $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER);
693 16
                if ($matchCount > 0) {
694 14
                    foreach ($matches as $match) {
695 14
                        $fromString = ($match[2] > '') ? $match[2] . '!' : '';
696 14
                        $fromString .= $match[3] . ':' . $match[4];
697 14
                        $modified3 = $this->updateCellReference($match[3], $pBefore, $pNumCols, $pNumRows);
698 14
                        $modified4 = $this->updateCellReference($match[4], $pBefore, $pNumCols, $pNumRows);
699
700 14
                        if ($match[3] . $match[4] !== $modified3 . $modified4) {
701 14
                            if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) {
702 14
                                $toString = ($match[2] > '') ? $match[2] . '!' : '';
703 14
                                $toString .= $modified3 . ':' . $modified4;
704 14
                                [$column, $row] = Coordinate::coordinateFromString($match[3]);
705
                                //    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
706 14
                                $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000;
707 14
                                $row = trim($row, '$') + 10000000;
708 14
                                $cellIndex = $column . $row;
709
710 14
                                $newCellTokens[$cellIndex] = preg_quote($toString, '/');
711 14
                                $cellTokens[$cellIndex] = '/(?<![A-Z]\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
712 14
                                ++$adjustCount;
713
                            }
714
                        }
715
                    }
716
                }
717
                //    Search for cell references (e.g. 'Sheet1'!A3 or C5) with or without $ absolutes (e.g. $A1 or C$5)
718 16
                $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER);
719
720 16
                if ($matchCount > 0) {
721 16
                    foreach ($matches as $match) {
722 16
                        $fromString = ($match[2] > '') ? $match[2] . '!' : '';
723 16
                        $fromString .= $match[3];
724
725 16
                        $modified3 = $this->updateCellReference($match[3], $pBefore, $pNumCols, $pNumRows);
726 16
                        if ($match[3] !== $modified3) {
727 16
                            if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) {
728 16
                                $toString = ($match[2] > '') ? $match[2] . '!' : '';
729 16
                                $toString .= $modified3;
730 16
                                [$column, $row] = Coordinate::coordinateFromString($match[3]);
731
                                //    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
732 16
                                $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000;
733 16
                                $row = trim($row, '$') + 10000000;
734 16
                                $cellIndex = $row . $column;
735
736 16
                                $newCellTokens[$cellIndex] = preg_quote($toString, '/');
737 16
                                $cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?!\d)/i';
738 16
                                ++$adjustCount;
739
                            }
740
                        }
741
                    }
742
                }
743 16
                if ($adjustCount > 0) {
744 16
                    if ($pNumCols > 0 || $pNumRows > 0) {
745 16
                        krsort($cellTokens);
746 16
                        krsort($newCellTokens);
747
                    } else {
748 12
                        ksort($cellTokens);
749 12
                        ksort($newCellTokens);
750
                    }   //  Update cell references in the formula
751 16
                    $formulaBlock = str_replace('\\', '', preg_replace($cellTokens, $newCellTokens, $formulaBlock));
752
                }
753
            }
754
        }
755 16
        unset($formulaBlock);
756
757
        //    Then rebuild the formula string
758 16
        return implode('"', $formulaBlocks);
759
    }
760
761
    /**
762
     * Update cell reference.
763
     *
764
     * @param string $pCellRange Cell range
765
     * @param string $pBefore Insert before this one
766
     * @param int $pNumCols Number of columns to increment
767
     * @param int $pNumRows Number of rows to increment
768
     *
769
     * @throws Exception
770
     *
771
     * @return string Updated cell range
772
     */
773 18
    public function updateCellReference($pCellRange = 'A1', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0)
774
    {
775
        // Is it in another worksheet? Will not have to update anything.
776 18
        if (strpos($pCellRange, '!') !== false) {
777
            return $pCellRange;
778
        // Is it a range or a single cell?
779 18
        } elseif (!Coordinate::coordinateIsRange($pCellRange)) {
780
            // Single cell
781 18
            return $this->updateSingleCellReference($pCellRange, $pBefore, $pNumCols, $pNumRows);
782 13
        } elseif (Coordinate::coordinateIsRange($pCellRange)) {
783
            // Range
784 13
            return $this->updateCellRange($pCellRange, $pBefore, $pNumCols, $pNumRows);
785
        }
786
787
        // Return original
788
        return $pCellRange;
789
    }
790
791
    /**
792
     * Update named formulas (i.e. containing worksheet references / named ranges).
793
     *
794
     * @param Spreadsheet $spreadsheet Object to update
795
     * @param string $oldName Old name (name to replace)
796
     * @param string $newName New name
797
     */
798 49
    public function updateNamedFormulas(Spreadsheet $spreadsheet, $oldName = '', $newName = '')
799
    {
800 49
        if ($oldName == '') {
801
            return;
802
        }
803
804 49
        foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
805 49
            foreach ($sheet->getCoordinates(false) as $coordinate) {
806 30
                $cell = $sheet->getCell($coordinate);
807 30
                if (($cell !== null) && ($cell->getDataType() == DataType::TYPE_FORMULA)) {
808 19
                    $formula = $cell->getValue();
809 19
                    if (strpos($formula, $oldName) !== false) {
810
                        $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula);
811
                        $formula = str_replace($oldName . '!', $newName . '!', $formula);
812
                        $cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
813
                    }
814
                }
815
            }
816
        }
817 49
    }
818
819
    /**
820
     * Update cell range.
821
     *
822
     * @param string $pCellRange Cell range    (e.g. 'B2:D4', 'B:C' or '2:3')
823
     * @param string $pBefore Insert before this one
824
     * @param int $pNumCols Number of columns to increment
825
     * @param int $pNumRows Number of rows to increment
826
     *
827
     * @throws Exception
828
     *
829
     * @return string Updated cell range
830
     */
831 13
    private function updateCellRange($pCellRange = 'A1:A1', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0)
832
    {
833 13
        if (!Coordinate::coordinateIsRange($pCellRange)) {
834
            throw new Exception('Only cell ranges may be passed to this method.');
835
        }
836
837
        // Update range
838 13
        $range = Coordinate::splitRange($pCellRange);
839 13
        $ic = count($range);
840 13
        for ($i = 0; $i < $ic; ++$i) {
841 13
            $jc = count($range[$i]);
842 13
            for ($j = 0; $j < $jc; ++$j) {
843 13
                if (ctype_alpha($range[$i][$j])) {
844
                    $r = Coordinate::coordinateFromString($this->updateSingleCellReference($range[$i][$j] . '1', $pBefore, $pNumCols, $pNumRows));
845
                    $range[$i][$j] = $r[0];
846 13
                } elseif (ctype_digit($range[$i][$j])) {
847
                    $r = Coordinate::coordinateFromString($this->updateSingleCellReference('A' . $range[$i][$j], $pBefore, $pNumCols, $pNumRows));
848
                    $range[$i][$j] = $r[1];
849
                } else {
850 13
                    $range[$i][$j] = $this->updateSingleCellReference($range[$i][$j], $pBefore, $pNumCols, $pNumRows);
851
                }
852
            }
853
        }
854
855
        // Recreate range string
856 13
        return Coordinate::buildRange($range);
857
    }
858
859
    /**
860
     * Update single cell reference.
861
     *
862
     * @param string $pCellReference Single cell reference
863
     * @param string $pBefore Insert before this one
864
     * @param int $pNumCols Number of columns to increment
865
     * @param int $pNumRows Number of rows to increment
866
     *
867
     * @throws Exception
868
     *
869
     * @return string Updated cell reference
870
     */
871 18
    private function updateSingleCellReference($pCellReference = 'A1', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0)
872
    {
873 18
        if (Coordinate::coordinateIsRange($pCellReference)) {
874
            throw new Exception('Only single cell references may be passed to this method.');
875
        }
876
877
        // Get coordinate of $pBefore
878 18
        [$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($pBefore);
879
880
        // Get coordinate of $pCellReference
881 18
        [$newColumn, $newRow] = Coordinate::coordinateFromString($pCellReference);
882
883
        // Verify which parts should be updated
884 18
        $updateColumn = (($newColumn[0] != '$') && ($beforeColumn[0] != '$') && (Coordinate::columnIndexFromString($newColumn) >= Coordinate::columnIndexFromString($beforeColumn)));
885 18
        $updateRow = (($newRow[0] != '$') && ($beforeRow[0] != '$') && $newRow >= $beforeRow);
886
887
        // Create new column reference
888 18
        if ($updateColumn) {
889 18
            $newColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($newColumn) + $pNumCols);
890
        }
891
892
        // Create new row reference
893 18
        if ($updateRow) {
894 17
            $newRow = $newRow + $pNumRows;
895
        }
896
897
        // Return new reference
898 18
        return $newColumn . $newRow;
899
    }
900
901
    /**
902
     * __clone implementation. Cloning should not be allowed in a Singleton!
903
     *
904
     * @throws Exception
905
     */
906
    final public function __clone()
907
    {
908
        throw new Exception('Cloning a Singleton is not allowed!');
909
    }
910
}
911