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
|
49 |
|
public static function getInstance() |
31
|
|
|
{ |
32
|
49 |
View Code Duplication |
if (!isset(self::$instance) || (self::$instance === null)) { |
|
|
|
|
33
|
40 |
|
self::$instance = new self(); |
34
|
|
|
} |
35
|
|
|
|
36
|
49 |
|
return self::$instance; |
37
|
|
|
} |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Create a new ReferenceHelper. |
41
|
|
|
*/ |
42
|
40 |
|
protected function __construct() |
43
|
|
|
{ |
44
|
40 |
|
} |
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 |
View Code Duplication |
public static function cellSort($a, $b) |
|
|
|
|
84
|
|
|
{ |
85
|
13 |
|
sscanf($a, '%[A-Z]%d', $ac, $ar); |
|
|
|
|
86
|
13 |
|
sscanf($b, '%[A-Z]%d', $bc, $br); |
|
|
|
|
87
|
|
|
|
88
|
13 |
|
if ($ar == $br) { |
89
|
|
|
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 |
View Code Duplication |
public static function cellReverseSort($a, $b) |
|
|
|
|
105
|
|
|
{ |
106
|
13 |
|
sscanf($a, '%[A-Z]%d', $ac, $ar); |
|
|
|
|
107
|
13 |
|
sscanf($b, '%[A-Z]%d', $bc, $br); |
|
|
|
|
108
|
|
|
|
109
|
13 |
|
if ($ar == $br) { |
110
|
|
|
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
|
13 |
|
private static function cellAddressInDeleteRange($cellAddress, $beforeRow, $pNumRows, $beforeColumnIndex, $pNumCols) |
128
|
|
|
{ |
129
|
13 |
|
list($cellColumn, $cellRow) = Coordinate::coordinateFromString($cellAddress); |
130
|
13 |
|
$cellColumnIndex = Coordinate::columnIndexFromString($cellColumn); |
131
|
|
|
// Is cell within the range of rows/columns if we're deleting |
132
|
13 |
|
if ($pNumRows < 0 && |
133
|
13 |
|
($cellRow >= ($beforeRow + $pNumRows)) && |
134
|
13 |
|
($cellRow < $beforeRow)) { |
135
|
|
|
return true; |
136
|
13 |
|
} elseif ($pNumCols < 0 && |
137
|
13 |
|
($cellColumnIndex >= ($beforeColumnIndex + $pNumCols)) && |
138
|
13 |
|
($cellColumnIndex < $beforeColumnIndex)) { |
139
|
|
|
return true; |
140
|
|
|
} |
141
|
|
|
|
142
|
13 |
|
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
|
15 |
|
protected function adjustPageBreaks(Worksheet $pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) |
156
|
|
|
{ |
157
|
15 |
|
$aBreaks = $pSheet->getBreaks(); |
158
|
15 |
|
($pNumCols > 0 || $pNumRows > 0) ? |
159
|
15 |
|
uksort($aBreaks, ['self', 'cellReverseSort']) : uksort($aBreaks, ['self', 'cellSort']); |
160
|
|
|
|
161
|
15 |
|
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
|
15 |
|
} |
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
|
15 |
|
protected function adjustComments($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) |
189
|
|
|
{ |
190
|
15 |
|
$aComments = $pSheet->getComments(); |
191
|
15 |
|
$aNewComments = []; // the new array of all comments |
192
|
|
|
|
193
|
15 |
|
foreach ($aComments as $key => &$value) { |
194
|
|
|
// Any comments inside a deleted range will be ignored |
195
|
13 |
|
if (!self::cellAddressInDeleteRange($key, $beforeRow, $pNumRows, $beforeColumnIndex, $pNumCols)) { |
196
|
|
|
// Otherwise build a new array of comments indexed by the adjusted cell reference |
197
|
13 |
|
$newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows); |
|
|
|
|
198
|
13 |
|
$aNewComments[$newReference] = $value; |
199
|
|
|
} |
200
|
|
|
} |
201
|
|
|
// Replace the comments array with the new set of comments |
202
|
15 |
|
$pSheet->setComments($aNewComments); |
203
|
15 |
|
} |
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
|
15 |
View Code Duplication |
protected function adjustHyperlinks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) |
|
|
|
|
216
|
|
|
{ |
217
|
15 |
|
$aHyperlinkCollection = $pSheet->getHyperlinkCollection(); |
218
|
15 |
|
($pNumCols > 0 || $pNumRows > 0) ? |
219
|
15 |
|
uksort($aHyperlinkCollection, ['self', 'cellReverseSort']) : uksort($aHyperlinkCollection, ['self', 'cellSort']); |
220
|
|
|
|
221
|
15 |
|
foreach ($aHyperlinkCollection as $key => $value) { |
222
|
13 |
|
$newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows); |
|
|
|
|
223
|
13 |
|
if ($key != $newReference) { |
224
|
13 |
|
$pSheet->setHyperlink($newReference, $value); |
225
|
13 |
|
$pSheet->setHyperlink($key, null); |
226
|
|
|
} |
227
|
|
|
} |
228
|
15 |
|
} |
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
|
15 |
View Code Duplication |
protected function adjustDataValidations($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) |
|
|
|
|
241
|
|
|
{ |
242
|
15 |
|
$aDataValidationCollection = $pSheet->getDataValidationCollection(); |
243
|
15 |
|
($pNumCols > 0 || $pNumRows > 0) ? |
244
|
15 |
|
uksort($aDataValidationCollection, ['self', 'cellReverseSort']) : uksort($aDataValidationCollection, ['self', 'cellSort']); |
245
|
|
|
|
246
|
15 |
|
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
|
15 |
|
} |
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
|
15 |
|
protected function adjustMergeCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) |
|
|
|
|
266
|
|
|
{ |
267
|
15 |
|
$aMergeCells = $pSheet->getMergeCells(); |
268
|
15 |
|
$aNewMergeCells = []; // the new array of all merge cells |
269
|
15 |
|
foreach ($aMergeCells as $key => &$value) { |
270
|
14 |
|
$newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows); |
|
|
|
|
271
|
14 |
|
$aNewMergeCells[$newReference] = $newReference; |
272
|
|
|
} |
273
|
15 |
|
$pSheet->setMergeCells($aNewMergeCells); // replace the merge cells array |
274
|
15 |
|
} |
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
|
15 |
|
protected function adjustProtectedCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) |
|
|
|
|
287
|
|
|
{ |
288
|
15 |
|
$aProtectedCells = $pSheet->getProtectedCells(); |
289
|
15 |
|
($pNumCols > 0 || $pNumRows > 0) ? |
290
|
15 |
|
uksort($aProtectedCells, ['self', 'cellReverseSort']) : uksort($aProtectedCells, ['self', 'cellSort']); |
291
|
15 |
|
foreach ($aProtectedCells as $key => $value) { |
292
|
12 |
|
$newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows); |
|
|
|
|
293
|
12 |
|
if ($key != $newReference) { |
294
|
12 |
|
$pSheet->protectCells($newReference, $value, true); |
|
|
|
|
295
|
12 |
|
$pSheet->unprotectCells($key); |
296
|
|
|
} |
297
|
|
|
} |
298
|
15 |
|
} |
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
|
15 |
|
protected function adjustColumnDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) |
|
|
|
|
311
|
|
|
{ |
312
|
15 |
|
$aColumnDimensions = array_reverse($pSheet->getColumnDimensions(), true); |
313
|
15 |
|
if (!empty($aColumnDimensions)) { |
314
|
15 |
View Code Duplication |
foreach ($aColumnDimensions as $objColumnDimension) { |
|
|
|
|
315
|
15 |
|
$newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1', $pBefore, $pNumCols, $pNumRows); |
|
|
|
|
316
|
15 |
|
list($newReference) = Coordinate::coordinateFromString($newReference); |
317
|
15 |
|
if ($objColumnDimension->getColumnIndex() != $newReference) { |
318
|
15 |
|
$objColumnDimension->setColumnIndex($newReference); |
319
|
|
|
} |
320
|
|
|
} |
321
|
15 |
|
$pSheet->refreshColumnDimensions(); |
322
|
|
|
} |
323
|
15 |
|
} |
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
|
15 |
|
protected function adjustRowDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) |
|
|
|
|
336
|
|
|
{ |
337
|
15 |
|
$aRowDimensions = array_reverse($pSheet->getRowDimensions(), true); |
338
|
15 |
|
if (!empty($aRowDimensions)) { |
339
|
2 |
View Code Duplication |
foreach ($aRowDimensions as $objRowDimension) { |
|
|
|
|
340
|
2 |
|
$newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex(), $pBefore, $pNumCols, $pNumRows); |
|
|
|
|
341
|
2 |
|
list(, $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
|
15 |
|
} |
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
|
15 |
|
public function insertNewBefore($pBefore, $pNumCols, $pNumRows, Worksheet $pSheet) |
370
|
|
|
{ |
371
|
15 |
|
$remove = ($pNumCols < 0 || $pNumRows < 0); |
372
|
15 |
|
$allCoordinates = $pSheet->getCoordinates(); |
373
|
|
|
|
374
|
|
|
// Get coordinate of $pBefore |
375
|
15 |
|
list($beforeColumn, $beforeRow) = Coordinate::coordinateFromString($pBefore); |
376
|
15 |
|
$beforeColumnIndex = Coordinate::columnIndexFromString($beforeColumn); |
377
|
|
|
|
378
|
|
|
// Clear cells if we are removing columns or rows |
379
|
15 |
|
$highestColumn = $pSheet->getHighestColumn(); |
380
|
15 |
|
$highestRow = $pSheet->getHighestRow(); |
381
|
|
|
|
382
|
|
|
// 1. Clear column strips if we are removing columns |
383
|
15 |
View Code Duplication |
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
|
15 |
View Code Duplication |
if ($pNumRows < 0 && $beforeRow - 1 + $pNumRows > 0) { |
|
|
|
|
398
|
14 |
|
for ($i = $beforeColumnIndex - 1; $i <= Coordinate::columnIndexFromString($highestColumn) - 1; ++$i) { |
399
|
14 |
|
for ($j = $beforeRow + $pNumRows; $j <= $beforeRow - 1; ++$j) { |
400
|
14 |
|
$coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; |
401
|
14 |
|
$pSheet->removeConditionalStyles($coordinate); |
402
|
14 |
|
if ($pSheet->cellExists($coordinate)) { |
403
|
13 |
|
$pSheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); |
404
|
13 |
|
$pSheet->getCell($coordinate)->setXfIndex(0); |
405
|
|
|
} |
406
|
|
|
} |
407
|
|
|
} |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
// Loop through cells, bottom-up, and change cell coordinate |
411
|
15 |
|
if ($remove) { |
412
|
|
|
// It's faster to reverse and pop than to use unshift, especially with large cell collections |
413
|
15 |
|
$allCoordinates = array_reverse($allCoordinates); |
414
|
|
|
} |
415
|
15 |
|
while ($coordinate = array_pop($allCoordinates)) { |
416
|
15 |
|
$cell = $pSheet->getCell($coordinate); |
417
|
15 |
|
$cellIndex = Coordinate::columnIndexFromString($cell->getColumn()); |
418
|
|
|
|
419
|
15 |
|
if ($cellIndex - 1 + $pNumCols < 0) { |
420
|
12 |
|
continue; |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
// New coordinate |
424
|
15 |
|
$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
|
15 |
|
if (($cellIndex >= $beforeColumnIndex) && ($cell->getRow() >= $beforeRow)) { |
428
|
|
|
// Update cell styles |
429
|
15 |
|
$pSheet->getCell($newCoordinate)->setXfIndex($cell->getXfIndex()); |
430
|
|
|
|
431
|
|
|
// Insert this cell at its new location |
432
|
15 |
|
if ($cell->getDataType() == DataType::TYPE_FORMULA) { |
433
|
|
|
// Formula should be adjusted |
434
|
13 |
|
$pSheet->getCell($newCoordinate) |
435
|
13 |
|
->setValue($this->updateFormulaReferences($cell->getValue(), $pBefore, $pNumCols, $pNumRows, $pSheet->getTitle())); |
|
|
|
|
436
|
|
|
} else { |
437
|
|
|
// Formula should not be adjusted |
438
|
15 |
|
$pSheet->getCell($newCoordinate)->setValue($cell->getValue()); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
// Clear the original cell |
442
|
15 |
|
$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
|
13 |
|
$cell->setValue($this->updateFormulaReferences($cell->getValue(), $pBefore, $pNumCols, $pNumRows, $pSheet->getTitle())); |
449
|
|
|
} |
450
|
|
|
} |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
// Duplicate styles for the newly inserted cells |
454
|
15 |
|
$highestColumn = $pSheet->getHighestColumn(); |
455
|
15 |
|
$highestRow = $pSheet->getHighestRow(); |
456
|
|
|
|
457
|
15 |
|
if ($pNumCols > 0 && $beforeColumnIndex - 2 > 0) { |
458
|
12 |
|
for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) { |
459
|
|
|
// Style |
460
|
12 |
|
$coordinate = Coordinate::stringFromColumnIndex($beforeColumnIndex - 1) . $i; |
461
|
12 |
View Code Duplication |
if ($pSheet->cellExists($coordinate)) { |
|
|
|
|
462
|
12 |
|
$xfIndex = $pSheet->getCell($coordinate)->getXfIndex(); |
463
|
12 |
|
$conditionalStyles = $pSheet->conditionalStylesExists($coordinate) ? |
464
|
12 |
|
$pSheet->getConditionalStyles($coordinate) : false; |
465
|
12 |
|
for ($j = $beforeColumnIndex; $j <= $beforeColumnIndex - 1 + $pNumCols; ++$j) { |
466
|
12 |
|
$pSheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex); |
467
|
12 |
|
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
|
15 |
|
if ($pNumRows > 0 && $beforeRow - 1 > 0) { |
480
|
13 |
|
for ($i = $beforeColumnIndex; $i <= Coordinate::columnIndexFromString($highestColumn); ++$i) { |
481
|
|
|
// Style |
482
|
13 |
|
$coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); |
483
|
13 |
View Code Duplication |
if ($pSheet->cellExists($coordinate)) { |
|
|
|
|
484
|
13 |
|
$xfIndex = $pSheet->getCell($coordinate)->getXfIndex(); |
485
|
13 |
|
$conditionalStyles = $pSheet->conditionalStylesExists($coordinate) ? |
486
|
13 |
|
$pSheet->getConditionalStyles($coordinate) : false; |
487
|
13 |
|
for ($j = $beforeRow; $j <= $beforeRow - 1 + $pNumRows; ++$j) { |
488
|
13 |
|
$pSheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); |
489
|
13 |
|
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
|
15 |
|
$this->adjustColumnDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); |
503
|
|
|
|
504
|
|
|
// Update worksheet: row dimensions |
505
|
15 |
|
$this->adjustRowDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); |
506
|
|
|
|
507
|
|
|
// Update worksheet: page breaks |
508
|
15 |
|
$this->adjustPageBreaks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); |
509
|
|
|
|
510
|
|
|
// Update worksheet: comments |
511
|
15 |
|
$this->adjustComments($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); |
512
|
|
|
|
513
|
|
|
// Update worksheet: hyperlinks |
514
|
15 |
|
$this->adjustHyperlinks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); |
515
|
|
|
|
516
|
|
|
// Update worksheet: data validations |
517
|
15 |
|
$this->adjustDataValidations($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); |
518
|
|
|
|
519
|
|
|
// Update worksheet: merge cells |
520
|
15 |
|
$this->adjustMergeCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); |
521
|
|
|
|
522
|
|
|
// Update worksheet: protected cells |
523
|
15 |
|
$this->adjustProtectedCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); |
524
|
|
|
|
525
|
|
|
// Update worksheet: autofilter |
526
|
15 |
|
$autoFilter = $pSheet->getAutoFilter(); |
527
|
15 |
|
$autoFilterRange = $autoFilter->getRange(); |
528
|
15 |
|
if (!empty($autoFilterRange)) { |
529
|
|
|
if ($pNumCols != 0) { |
530
|
|
|
$autoFilterColumns = $autoFilter->getColumns(); |
531
|
|
|
if (count($autoFilterColumns) > 0) { |
532
|
|
|
sscanf($pBefore, '%[A-Z]%d', $column, $row); |
|
|
|
|
533
|
|
|
$columnIndex = Coordinate::columnIndexFromString($column); |
534
|
|
|
list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($autoFilterRange); |
535
|
|
|
if ($columnIndex <= $rangeEnd[0]) { |
536
|
|
|
if ($pNumCols < 0) { |
537
|
|
|
// If we're actually deleting any columns that fall within the autofilter range, |
538
|
|
|
// then we delete any rules for those columns |
539
|
|
|
$deleteColumn = $columnIndex + $pNumCols - 1; |
540
|
|
|
$deleteCount = abs($pNumCols); |
541
|
|
|
for ($i = 1; $i <= $deleteCount; ++$i) { |
542
|
|
|
if (isset($autoFilterColumns[Coordinate::stringFromColumnIndex($deleteColumn + 1)])) { |
543
|
|
|
$autoFilter->clearColumn(Coordinate::stringFromColumnIndex($deleteColumn + 1)); |
544
|
|
|
} |
545
|
|
|
++$deleteColumn; |
546
|
|
|
} |
547
|
|
|
} |
548
|
|
|
$startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; |
549
|
|
|
|
550
|
|
|
// Shuffle columns in autofilter range |
551
|
|
|
if ($pNumCols > 0) { |
552
|
|
|
$startColRef = $startCol; |
553
|
|
|
$endColRef = $rangeEnd[0]; |
554
|
|
|
$toColRef = $rangeEnd[0] + $pNumCols; |
555
|
|
|
|
556
|
|
|
do { |
557
|
|
|
$autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); |
558
|
|
|
--$endColRef; |
559
|
|
|
--$toColRef; |
560
|
|
|
} while ($startColRef <= $endColRef); |
561
|
|
|
} else { |
562
|
|
|
// For delete, we shuffle from beginning to end to avoid overwriting |
563
|
|
|
$startColID = Coordinate::stringFromColumnIndex($startCol); |
564
|
|
|
$toColID = Coordinate::stringFromColumnIndex($startCol + $pNumCols); |
565
|
|
|
$endColID = Coordinate::stringFromColumnIndex($rangeEnd[0] + 1); |
566
|
|
|
do { |
567
|
|
|
$autoFilter->shiftColumn($startColID, $toColID); |
568
|
|
|
++$startColID; |
569
|
|
|
++$toColID; |
570
|
|
|
} while ($startColID != $endColID); |
571
|
|
|
} |
572
|
|
|
} |
573
|
|
|
} |
574
|
|
|
} |
575
|
|
|
$pSheet->setAutoFilter($this->updateCellReference($autoFilterRange, $pBefore, $pNumCols, $pNumRows)); |
|
|
|
|
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
// Update worksheet: freeze pane |
579
|
15 |
|
if ($pSheet->getFreezePane()) { |
580
|
1 |
|
$splitCell = $pSheet->getFreezePane(); |
581
|
1 |
|
$topLeftCell = $pSheet->getTopLeftCell(); |
582
|
|
|
|
583
|
1 |
|
$splitCell = $this->updateCellReference($splitCell, $pBefore, $pNumCols, $pNumRows); |
584
|
1 |
|
$topLeftCell = $this->updateCellReference($topLeftCell, $pBefore, $pNumCols, $pNumRows); |
585
|
|
|
|
586
|
1 |
|
$pSheet->freezePane($splitCell, $topLeftCell); |
587
|
|
|
} |
588
|
|
|
|
589
|
|
|
// Page setup |
590
|
15 |
|
if ($pSheet->getPageSetup()->isPrintAreaSet()) { |
591
|
|
|
$pSheet->getPageSetup()->setPrintArea($this->updateCellReference($pSheet->getPageSetup()->getPrintArea(), $pBefore, $pNumCols, $pNumRows)); |
592
|
|
|
} |
593
|
|
|
|
594
|
|
|
// Update worksheet: drawings |
595
|
15 |
|
$aDrawings = $pSheet->getDrawingCollection(); |
596
|
15 |
|
foreach ($aDrawings as $objDrawing) { |
597
|
13 |
|
$newReference = $this->updateCellReference($objDrawing->getCoordinates(), $pBefore, $pNumCols, $pNumRows); |
598
|
13 |
|
if ($objDrawing->getCoordinates() != $newReference) { |
599
|
13 |
|
$objDrawing->setCoordinates($newReference); |
600
|
|
|
} |
601
|
|
|
} |
602
|
|
|
|
603
|
|
|
// Update workbook: named ranges |
604
|
15 |
|
if (count($pSheet->getParent()->getNamedRanges()) > 0) { |
605
|
1 |
|
foreach ($pSheet->getParent()->getNamedRanges() as $namedRange) { |
606
|
1 |
|
if ($namedRange->getWorksheet()->getHashCode() == $pSheet->getHashCode()) { |
607
|
1 |
|
$namedRange->setRange($this->updateCellReference($namedRange->getRange(), $pBefore, $pNumCols, $pNumRows)); |
608
|
|
|
} |
609
|
|
|
} |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
// Garbage collect |
613
|
15 |
|
$pSheet->garbageCollect(); |
614
|
15 |
|
} |
615
|
|
|
|
616
|
|
|
/** |
617
|
|
|
* Update references within formulas. |
618
|
|
|
* |
619
|
|
|
* @param string $pFormula Formula to update |
620
|
|
|
* @param int $pBefore Insert before this one |
621
|
|
|
* @param int $pNumCols Number of columns to insert |
622
|
|
|
* @param int $pNumRows Number of rows to insert |
623
|
|
|
* @param string $sheetName Worksheet name/title |
624
|
|
|
* |
625
|
|
|
* @throws Exception |
626
|
|
|
* |
627
|
|
|
* @return string Updated formula |
628
|
|
|
*/ |
629
|
16 |
|
public function updateFormulaReferences($pFormula = '', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0, $sheetName = '') |
630
|
|
|
{ |
631
|
|
|
// Update cell references in the formula |
632
|
16 |
|
$formulaBlocks = explode('"', $pFormula); |
633
|
16 |
|
$i = false; |
634
|
16 |
|
foreach ($formulaBlocks as &$formulaBlock) { |
635
|
|
|
// Ignore blocks that were enclosed in quotes (alternating entries in the $formulaBlocks array after the explode) |
636
|
16 |
|
if ($i = !$i) { |
637
|
16 |
|
$adjustCount = 0; |
638
|
16 |
|
$newCellTokens = $cellTokens = []; |
639
|
|
|
// Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5) |
|
|
|
|
640
|
16 |
|
$matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); |
641
|
16 |
|
if ($matchCount > 0) { |
642
|
|
|
foreach ($matches as $match) { |
643
|
|
|
$fromString = ($match[2] > '') ? $match[2] . '!' : ''; |
644
|
|
|
$fromString .= $match[3] . ':' . $match[4]; |
645
|
|
|
$modified3 = substr($this->updateCellReference('$A' . $match[3], $pBefore, $pNumCols, $pNumRows), 2); |
|
|
|
|
646
|
|
|
$modified4 = substr($this->updateCellReference('$A' . $match[4], $pBefore, $pNumCols, $pNumRows), 2); |
647
|
|
|
|
648
|
|
View Code Duplication |
if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { |
|
|
|
|
649
|
|
|
if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) { |
650
|
|
|
$toString = ($match[2] > '') ? $match[2] . '!' : ''; |
651
|
|
|
$toString .= $modified3 . ':' . $modified4; |
652
|
|
|
// 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 |
653
|
|
|
$column = 100000; |
654
|
|
|
$row = 10000000 + trim($match[3], '$'); |
655
|
|
|
$cellIndex = $column . $row; |
656
|
|
|
|
657
|
|
|
$newCellTokens[$cellIndex] = preg_quote($toString); |
658
|
|
|
$cellTokens[$cellIndex] = '/(?<!\d\$\!)' . preg_quote($fromString) . '(?!\d)/i'; |
659
|
|
|
++$adjustCount; |
660
|
|
|
} |
661
|
|
|
} |
662
|
|
|
} |
663
|
|
|
} |
664
|
|
|
// Search for column ranges (e.g. 'Sheet1'!C:E or C:E) with or without $ absolutes (e.g. $C:E) |
665
|
16 |
|
$matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); |
666
|
16 |
|
if ($matchCount > 0) { |
667
|
|
|
foreach ($matches as $match) { |
668
|
|
|
$fromString = ($match[2] > '') ? $match[2] . '!' : ''; |
669
|
|
|
$fromString .= $match[3] . ':' . $match[4]; |
670
|
|
|
$modified3 = substr($this->updateCellReference($match[3] . '$1', $pBefore, $pNumCols, $pNumRows), 0, -2); |
671
|
|
|
$modified4 = substr($this->updateCellReference($match[4] . '$1', $pBefore, $pNumCols, $pNumRows), 0, -2); |
672
|
|
|
|
673
|
|
View Code Duplication |
if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { |
|
|
|
|
674
|
|
|
if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) { |
675
|
|
|
$toString = ($match[2] > '') ? $match[2] . '!' : ''; |
676
|
|
|
$toString .= $modified3 . ':' . $modified4; |
677
|
|
|
// 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 |
678
|
|
|
$column = Coordinate::columnIndexFromString(trim($match[3], '$')) + 100000; |
679
|
|
|
$row = 10000000; |
680
|
|
|
$cellIndex = $column . $row; |
681
|
|
|
|
682
|
|
|
$newCellTokens[$cellIndex] = preg_quote($toString); |
683
|
|
|
$cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString) . '(?![A-Z])/i'; |
684
|
|
|
++$adjustCount; |
685
|
|
|
} |
686
|
|
|
} |
687
|
|
|
} |
688
|
|
|
} |
689
|
|
|
// Search for cell ranges (e.g. 'Sheet1'!A3:C5 or A3:C5) with or without $ absolutes (e.g. $A1:C$5) |
690
|
16 |
|
$matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); |
691
|
16 |
|
if ($matchCount > 0) { |
692
|
14 |
|
foreach ($matches as $match) { |
693
|
14 |
|
$fromString = ($match[2] > '') ? $match[2] . '!' : ''; |
694
|
14 |
|
$fromString .= $match[3] . ':' . $match[4]; |
695
|
14 |
|
$modified3 = $this->updateCellReference($match[3], $pBefore, $pNumCols, $pNumRows); |
696
|
14 |
|
$modified4 = $this->updateCellReference($match[4], $pBefore, $pNumCols, $pNumRows); |
697
|
|
|
|
698
|
14 |
|
if ($match[3] . $match[4] !== $modified3 . $modified4) { |
699
|
14 |
View Code Duplication |
if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) { |
|
|
|
|
700
|
14 |
|
$toString = ($match[2] > '') ? $match[2] . '!' : ''; |
701
|
14 |
|
$toString .= $modified3 . ':' . $modified4; |
702
|
14 |
|
list($column, $row) = Coordinate::coordinateFromString($match[3]); |
703
|
|
|
// 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 |
704
|
14 |
|
$column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000; |
705
|
14 |
|
$row = trim($row, '$') + 10000000; |
706
|
14 |
|
$cellIndex = $column . $row; |
707
|
|
|
|
708
|
14 |
|
$newCellTokens[$cellIndex] = preg_quote($toString); |
709
|
14 |
|
$cellTokens[$cellIndex] = '/(?<![A-Z]\$\!)' . preg_quote($fromString) . '(?!\d)/i'; |
710
|
14 |
|
++$adjustCount; |
711
|
|
|
} |
712
|
|
|
} |
713
|
|
|
} |
714
|
|
|
} |
715
|
|
|
// Search for cell references (e.g. 'Sheet1'!A3 or C5) with or without $ absolutes (e.g. $A1 or C$5) |
716
|
16 |
|
$matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); |
717
|
|
|
|
718
|
16 |
|
if ($matchCount > 0) { |
719
|
16 |
|
foreach ($matches as $match) { |
720
|
16 |
|
$fromString = ($match[2] > '') ? $match[2] . '!' : ''; |
721
|
16 |
|
$fromString .= $match[3]; |
722
|
|
|
|
723
|
16 |
|
$modified3 = $this->updateCellReference($match[3], $pBefore, $pNumCols, $pNumRows); |
724
|
16 |
View Code Duplication |
if ($match[3] !== $modified3) { |
|
|
|
|
725
|
16 |
|
if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) { |
726
|
16 |
|
$toString = ($match[2] > '') ? $match[2] . '!' : ''; |
727
|
16 |
|
$toString .= $modified3; |
728
|
16 |
|
list($column, $row) = Coordinate::coordinateFromString($match[3]); |
729
|
|
|
// 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 |
730
|
16 |
|
$column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000; |
731
|
16 |
|
$row = trim($row, '$') + 10000000; |
732
|
16 |
|
$cellIndex = $row . $column; |
733
|
|
|
|
734
|
16 |
|
$newCellTokens[$cellIndex] = preg_quote($toString); |
735
|
16 |
|
$cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString) . '(?!\d)/i'; |
736
|
16 |
|
++$adjustCount; |
737
|
|
|
} |
738
|
|
|
} |
739
|
|
|
} |
740
|
|
|
} |
741
|
16 |
|
if ($adjustCount > 0) { |
742
|
16 |
|
if ($pNumCols > 0 || $pNumRows > 0) { |
743
|
16 |
|
krsort($cellTokens); |
744
|
16 |
|
krsort($newCellTokens); |
745
|
|
|
} else { |
746
|
13 |
|
ksort($cellTokens); |
747
|
13 |
|
ksort($newCellTokens); |
748
|
|
|
} // Update cell references in the formula |
749
|
16 |
|
$formulaBlock = str_replace('\\', '', preg_replace($cellTokens, $newCellTokens, $formulaBlock)); |
750
|
|
|
} |
751
|
|
|
} |
752
|
|
|
} |
753
|
16 |
|
unset($formulaBlock); |
754
|
|
|
|
755
|
|
|
// Then rebuild the formula string |
756
|
16 |
|
return implode('"', $formulaBlocks); |
757
|
|
|
} |
758
|
|
|
|
759
|
|
|
/** |
760
|
|
|
* Update cell reference. |
761
|
|
|
* |
762
|
|
|
* @param string $pCellRange Cell range |
763
|
|
|
* @param int $pBefore Insert before this one |
764
|
|
|
* @param int $pNumCols Number of columns to increment |
765
|
|
|
* @param int $pNumRows Number of rows to increment |
766
|
|
|
* |
767
|
|
|
* @throws Exception |
768
|
|
|
* |
769
|
|
|
* @return string Updated cell range |
770
|
|
|
*/ |
771
|
18 |
|
public function updateCellReference($pCellRange = 'A1', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0) |
772
|
|
|
{ |
773
|
|
|
// Is it in another worksheet? Will not have to update anything. |
774
|
18 |
|
if (strpos($pCellRange, '!') !== false) { |
775
|
|
|
return $pCellRange; |
776
|
|
|
// Is it a range or a single cell? |
777
|
18 |
|
} elseif (strpos($pCellRange, ':') === false && strpos($pCellRange, ',') === false) { |
778
|
|
|
// Single cell |
779
|
18 |
|
return $this->updateSingleCellReference($pCellRange, $pBefore, $pNumCols, $pNumRows); |
|
|
|
|
780
|
14 |
|
} elseif (strpos($pCellRange, ':') !== false || strpos($pCellRange, ',') !== false) { |
781
|
|
|
// Range |
782
|
14 |
|
return $this->updateCellRange($pCellRange, $pBefore, $pNumCols, $pNumRows); |
|
|
|
|
783
|
|
|
} |
784
|
|
|
// Return original |
785
|
|
|
return $pCellRange; |
786
|
|
|
} |
787
|
|
|
|
788
|
|
|
/** |
789
|
|
|
* Update named formulas (i.e. containing worksheet references / named ranges). |
790
|
|
|
* |
791
|
|
|
* @param Spreadsheet $spreadsheet Object to update |
792
|
|
|
* @param string $oldName Old name (name to replace) |
793
|
|
|
* @param string $newName New name |
794
|
|
|
*/ |
795
|
31 |
|
public function updateNamedFormulas(Spreadsheet $spreadsheet, $oldName = '', $newName = '') |
796
|
|
|
{ |
797
|
31 |
|
if ($oldName == '') { |
798
|
|
|
return; |
799
|
|
|
} |
800
|
|
|
|
801
|
31 |
|
foreach ($spreadsheet->getWorksheetIterator() as $sheet) { |
802
|
31 |
|
foreach ($sheet->getCoordinates(false) as $coordinate) { |
803
|
29 |
|
$cell = $sheet->getCell($coordinate); |
804
|
29 |
|
if (($cell !== null) && ($cell->getDataType() == DataType::TYPE_FORMULA)) { |
805
|
20 |
|
$formula = $cell->getValue(); |
806
|
20 |
|
if (strpos($formula, $oldName) !== false) { |
807
|
|
|
$formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula); |
808
|
|
|
$formula = str_replace($oldName . '!', $newName . '!', $formula); |
809
|
31 |
|
$cell->setValueExplicit($formula, DataType::TYPE_FORMULA); |
810
|
|
|
} |
811
|
|
|
} |
812
|
|
|
} |
813
|
|
|
} |
814
|
31 |
|
} |
815
|
|
|
|
816
|
|
|
/** |
817
|
|
|
* Update cell range. |
818
|
|
|
* |
819
|
|
|
* @param string $pCellRange Cell range (e.g. 'B2:D4', 'B:C' or '2:3') |
820
|
|
|
* @param int $pBefore Insert before this one |
821
|
|
|
* @param int $pNumCols Number of columns to increment |
822
|
|
|
* @param int $pNumRows Number of rows to increment |
823
|
|
|
* |
824
|
|
|
* @throws Exception |
825
|
|
|
* |
826
|
|
|
* @return string Updated cell range |
827
|
|
|
*/ |
828
|
14 |
|
private function updateCellRange($pCellRange = 'A1:A1', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0) |
829
|
|
|
{ |
830
|
14 |
|
if (strpos($pCellRange, ':') !== false || strpos($pCellRange, ',') !== false) { |
831
|
|
|
// Update range |
832
|
14 |
|
$range = Coordinate::splitRange($pCellRange); |
833
|
14 |
|
$ic = count($range); |
834
|
14 |
|
for ($i = 0; $i < $ic; ++$i) { |
835
|
14 |
|
$jc = count($range[$i]); |
836
|
14 |
|
for ($j = 0; $j < $jc; ++$j) { |
837
|
14 |
|
if (ctype_alpha($range[$i][$j])) { |
838
|
|
|
$r = Coordinate::coordinateFromString($this->updateSingleCellReference($range[$i][$j] . '1', $pBefore, $pNumCols, $pNumRows)); |
|
|
|
|
839
|
|
|
$range[$i][$j] = $r[0]; |
840
|
14 |
|
} elseif (ctype_digit($range[$i][$j])) { |
841
|
|
|
$r = Coordinate::coordinateFromString($this->updateSingleCellReference('A' . $range[$i][$j], $pBefore, $pNumCols, $pNumRows)); |
842
|
|
|
$range[$i][$j] = $r[1]; |
843
|
|
|
} else { |
844
|
14 |
|
$range[$i][$j] = $this->updateSingleCellReference($range[$i][$j], $pBefore, $pNumCols, $pNumRows); |
845
|
|
|
} |
846
|
|
|
} |
847
|
|
|
} |
848
|
|
|
|
849
|
|
|
// Recreate range string |
850
|
14 |
|
return Coordinate::buildRange($range); |
851
|
|
|
} |
852
|
|
|
|
853
|
|
|
throw new Exception('Only cell ranges may be passed to this method.'); |
854
|
|
|
} |
855
|
|
|
|
856
|
|
|
/** |
857
|
|
|
* Update single cell reference. |
858
|
|
|
* |
859
|
|
|
* @param string $pCellReference Single cell reference |
860
|
|
|
* @param int $pBefore Insert before this one |
861
|
|
|
* @param int $pNumCols Number of columns to increment |
862
|
|
|
* @param int $pNumRows Number of rows to increment |
863
|
|
|
* |
864
|
|
|
* @throws Exception |
865
|
|
|
* |
866
|
|
|
* @return string Updated cell reference |
867
|
|
|
*/ |
868
|
18 |
|
private function updateSingleCellReference($pCellReference = 'A1', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0) |
869
|
|
|
{ |
870
|
18 |
|
if (strpos($pCellReference, ':') === false && strpos($pCellReference, ',') === false) { |
871
|
|
|
// Get coordinate of $pBefore |
872
|
18 |
|
list($beforeColumn, $beforeRow) = Coordinate::coordinateFromString($pBefore); |
873
|
|
|
|
874
|
|
|
// Get coordinate of $pCellReference |
875
|
18 |
|
list($newColumn, $newRow) = Coordinate::coordinateFromString($pCellReference); |
876
|
|
|
|
877
|
|
|
// Verify which parts should be updated |
878
|
18 |
|
$updateColumn = (($newColumn[0] != '$') && ($beforeColumn[0] != '$') && (Coordinate::columnIndexFromString($newColumn) >= Coordinate::columnIndexFromString($beforeColumn))); |
879
|
18 |
|
$updateRow = (($newRow[0] != '$') && ($beforeRow[0] != '$') && $newRow >= $beforeRow); |
880
|
|
|
|
881
|
|
|
// Create new column reference |
882
|
18 |
|
if ($updateColumn) { |
883
|
18 |
|
$newColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($newColumn) + $pNumCols); |
884
|
|
|
} |
885
|
|
|
|
886
|
|
|
// Create new row reference |
887
|
18 |
|
if ($updateRow) { |
888
|
17 |
|
$newRow = $newRow + $pNumRows; |
889
|
|
|
} |
890
|
|
|
|
891
|
|
|
// Return new reference |
892
|
18 |
|
return $newColumn . $newRow; |
893
|
|
|
} |
894
|
|
|
|
895
|
|
|
throw new Exception('Only single cell references may be passed to this method.'); |
896
|
|
|
} |
897
|
|
|
|
898
|
|
|
/** |
899
|
|
|
* __clone implementation. Cloning should not be allowed in a Singleton! |
900
|
|
|
* |
901
|
|
|
* @throws Exception |
902
|
|
|
*/ |
903
|
|
|
final public function __clone() |
904
|
|
|
{ |
905
|
|
|
throw new Exception('Cloning a Singleton is not allowed!'); |
906
|
|
|
} |
907
|
|
|
} |
908
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.