2
2
3
3
namespace PhpOffice \PhpSpreadsheet \Calculation \Engine \Operands ;
4
4
5
+ use PhpOffice \PhpSpreadsheet \Calculation \Calculation ;
5
6
use PhpOffice \PhpSpreadsheet \Calculation \Exception ;
7
+ use PhpOffice \PhpSpreadsheet \Cell \Cell ;
8
+ use PhpOffice \PhpSpreadsheet \Cell \Coordinate ;
9
+ use PhpOffice \PhpSpreadsheet \Worksheet \Table ;
6
10
7
11
final class StructuredReference implements Operand
8
12
{
@@ -11,8 +15,37 @@ final class StructuredReference implements Operand
11
15
private const OPEN_BRACE = '[ ' ;
12
16
private const CLOSE_BRACE = '] ' ;
13
17
18
+ private const ITEM_SPECIFIER_ALL = '#All ' ;
19
+ private const ITEM_SPECIFIER_HEADERS = '#Headers ' ;
20
+ private const ITEM_SPECIFIER_DATA = '#Data ' ;
21
+ private const ITEM_SPECIFIER_TOTALS = '#Totals ' ;
22
+ private const ITEM_SPECIFIER_THIS_ROW = '#This Row ' ;
23
+
24
+ private const ITEM_SPECIFIER_ROWS_SET = [
25
+ self ::ITEM_SPECIFIER_ALL ,
26
+ self ::ITEM_SPECIFIER_HEADERS ,
27
+ self ::ITEM_SPECIFIER_DATA ,
28
+ self ::ITEM_SPECIFIER_TOTALS ,
29
+ ];
30
+
31
+ private const TABLE_REFERENCE = '/([\p{L}_ \\\\][\p{L}\p{N}\._]+)?(\[(?:[^\]\[]+|(?R))*+\])/miu ' ;
32
+
14
33
private string $ value ;
15
34
35
+ private string $ tableName ;
36
+
37
+ private string $ reference ;
38
+
39
+ private ?int $ headersRow ;
40
+
41
+ private int $ firstDataRow ;
42
+
43
+ private int $ lastDataRow ;
44
+
45
+ private ?int $ totalsRow ;
46
+
47
+ private array $ columns ;
48
+
16
49
public function __construct (string $ structuredReference )
17
50
{
18
51
$ this ->value = $ structuredReference ;
@@ -42,8 +75,250 @@ public static function fromParser(string $formula, int $index, array $matches):
42
75
return new self ($ val );
43
76
}
44
77
78
+ /**
79
+ * @throws Exception
80
+ * @throws \PhpOffice\PhpSpreadsheet\Exception
81
+ */
82
+ public function parse (Cell $ cell ): string
83
+ {
84
+ $ this ->getTableStructure ($ cell );
85
+ $ cellRange = ($ this ->isRowReference ()) ? $ this ->getRowReference ($ cell ) : $ this ->getColumnReference ();
86
+
87
+ return $ cellRange ;
88
+ }
89
+
90
+ private function isRowReference (): bool
91
+ {
92
+ return strpos ($ this ->value , '[@ ' ) !== false
93
+ || strpos ($ this ->value , '[ ' . self ::ITEM_SPECIFIER_THIS_ROW . '] ' ) !== false ;
94
+ }
95
+
96
+ /**
97
+ * @throws Exception
98
+ * @throws \PhpOffice\PhpSpreadsheet\Exception
99
+ */
100
+ private function getTableStructure (Cell $ cell ): void
101
+ {
102
+ preg_match (self ::TABLE_REFERENCE , $ this ->value , $ matches );
103
+
104
+ $ this ->tableName = $ matches [1 ];
105
+ $ table = ($ this ->tableName === '' )
106
+ ? $ this ->getTableForCell ($ cell )
107
+ : $ this ->getTableByName ($ cell );
108
+ $ this ->reference = $ matches [2 ];
109
+ $ tableRange = Coordinate::getRangeBoundaries ($ table ->getRange ());
110
+
111
+ $ this ->headersRow = ($ table ->getShowHeaderRow ()) ? (int ) $ tableRange [0 ][1 ] : null ;
112
+ $ this ->firstDataRow = ($ table ->getShowHeaderRow ()) ? (int ) $ tableRange [0 ][1 ] + 1 : $ tableRange [0 ][1 ];
113
+ $ this ->totalsRow = ($ table ->getShowTotalsRow ()) ? (int ) $ tableRange [1 ][1 ] : null ;
114
+ $ this ->lastDataRow = ($ table ->getShowTotalsRow ()) ? (int ) $ tableRange [1 ][1 ] - 1 : $ tableRange [1 ][1 ];
115
+
116
+ $ this ->columns = $ this ->getColumns ($ cell , $ tableRange );
117
+ }
118
+
119
+ /**
120
+ * @throws Exception
121
+ * @throws \PhpOffice\PhpSpreadsheet\Exception
122
+ */
123
+ private function getTableForCell (Cell $ cell ): Table
124
+ {
125
+ $ tables = $ cell ->getWorksheet ()->getTableCollection ();
126
+ foreach ($ tables as $ table ) {
127
+ /** @var Table $table */
128
+ $ range = $ table ->getRange ();
129
+ if ($ cell ->isInRange ($ range ) === true ) {
130
+ $ this ->tableName = $ table ->getName ();
131
+
132
+ return $ table ;
133
+ }
134
+ }
135
+
136
+ throw new Exception ('Table for Structured Reference cannot be identified ' );
137
+ }
138
+
139
+ /**
140
+ * @throws Exception
141
+ * @throws \PhpOffice\PhpSpreadsheet\Exception
142
+ */
143
+ private function getTableByName (Cell $ cell ): Table
144
+ {
145
+ $ table = $ cell ->getWorksheet ()->getTableByName ($ this ->tableName );
146
+
147
+ if ($ table === null ) {
148
+ throw new Exception ("Table {$ this ->tableName } for Structured Reference cannot be located " );
149
+ }
150
+
151
+ return $ table ;
152
+ }
153
+
154
+ private function getColumns (Cell $ cell , array $ tableRange ): array
155
+ {
156
+ $ worksheet = $ cell ->getWorksheet ();
157
+ $ cellReference = $ cell ->getCoordinate ();
158
+
159
+ $ columns = [];
160
+ $ lastColumn = ++$ tableRange [1 ][0 ];
161
+ for ($ column = $ tableRange [0 ][0 ]; $ column !== $ lastColumn ; ++$ column ) {
162
+ $ columns [$ column ] = $ worksheet
163
+ ->getCell ($ column . $ this ->headersRow )
164
+ ->getCalculatedValue ();
165
+ }
166
+
167
+ $ cell = $ worksheet ->getCell ($ cellReference );
168
+
169
+ return $ columns ;
170
+ }
171
+
172
+ private function getRowReference (Cell $ cell ): string
173
+ {
174
+ $ reference = str_replace ("\u{a0}" , ' ' , $ this ->reference );
175
+ /** @var string $reference */
176
+ $ reference = str_replace ('[ ' . self ::ITEM_SPECIFIER_THIS_ROW . '], ' , '' , $ reference );
177
+
178
+ foreach ($ this ->columns as $ columnId => $ columnName ) {
179
+ $ columnName = str_replace ("\u{a0}" , ' ' , $ columnName );
180
+ $ cellReference = $ columnId . $ cell ->getRow ();
181
+ $ pattern1 = '/\[ ' . preg_quote ($ columnName ) . '\]/miu ' ;
182
+ $ pattern2 = '/@ ' . preg_quote ($ columnName ) . '/miu ' ;
183
+ /** @var string $reference */
184
+ if (preg_match ($ pattern1 , $ reference ) === 1 ) {
185
+ $ reference = preg_replace ($ pattern1 , $ cellReference , $ reference );
186
+ } elseif (preg_match ($ pattern2 , $ reference ) === 1 ) {
187
+ $ reference = preg_replace ($ pattern2 , $ cellReference , $ reference );
188
+ }
189
+ }
190
+
191
+ /** @var string $reference */
192
+ return $ this ->validateParsedReference (trim ($ reference , '[]@, ' ));
193
+ }
194
+
195
+ /**
196
+ * @throws Exception
197
+ * @throws \PhpOffice\PhpSpreadsheet\Exception
198
+ */
199
+ private function getColumnReference (): string
200
+ {
201
+ $ reference = str_replace ("\u{a0}" , ' ' , $ this ->reference );
202
+ $ startRow = ($ this ->totalsRow === null ) ? $ this ->lastDataRow : $ this ->totalsRow ;
203
+ $ endRow = ($ this ->headersRow === null ) ? $ this ->firstDataRow : $ this ->headersRow ;
204
+
205
+ [$ startRow , $ endRow ] = $ this ->getRowsForColumnReference ($ reference , $ startRow , $ endRow );
206
+ $ reference = $ this ->getColumnsForColumnReference ($ reference , $ startRow , $ endRow );
207
+
208
+ $ reference = trim ($ reference , '[]@, ' );
209
+ if (substr_count ($ reference , ': ' ) > 1 ) {
210
+ $ cells = explode (': ' , $ reference );
211
+ $ firstCell = array_shift ($ cells );
212
+ $ lastCell = array_pop ($ cells );
213
+ $ reference = "{$ firstCell }: {$ lastCell }" ;
214
+ }
215
+
216
+ return $ this ->validateParsedReference ($ reference );
217
+ }
218
+
219
+ /**
220
+ * @throws Exception
221
+ * @throws \PhpOffice\PhpSpreadsheet\Exception
222
+ */
223
+ private function validateParsedReference (string $ reference ): string
224
+ {
225
+ if (preg_match ('/^ ' . Calculation::CALCULATION_REGEXP_CELLREF . ': ' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu ' , $ reference ) !== 1 ) {
226
+ if (preg_match ('/^ ' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu ' , $ reference ) !== 1 ) {
227
+ throw new Exception ("Invalid Structured Reference {$ this ->reference } {$ reference }" );
228
+ }
229
+ }
230
+
231
+ return $ reference ;
232
+ }
233
+
234
+ private function fullData (int $ startRow , int $ endRow ): string
235
+ {
236
+ $ columns = array_keys ($ this ->columns );
237
+ $ firstColumn = array_shift ($ columns );
238
+ $ lastColumn = (empty ($ columns )) ? $ firstColumn : array_pop ($ columns );
239
+
240
+ return "{$ firstColumn }{$ startRow }: {$ lastColumn }{$ endRow }" ;
241
+ }
242
+
243
+ private function getMinimumRow (string $ reference ): int
244
+ {
245
+ switch ($ reference ) {
246
+ case self ::ITEM_SPECIFIER_ALL :
247
+ case self ::ITEM_SPECIFIER_HEADERS :
248
+ return $ this ->headersRow ?? $ this ->firstDataRow ;
249
+ case self ::ITEM_SPECIFIER_DATA :
250
+ return $ this ->firstDataRow ;
251
+ case self ::ITEM_SPECIFIER_TOTALS :
252
+ return $ this ->totalsRow ?? $ this ->lastDataRow ;
253
+ }
254
+
255
+ return $ this ->headersRow ?? $ this ->firstDataRow ;
256
+ }
257
+
258
+ private function getMaximumRow (string $ reference ): int
259
+ {
260
+ switch ($ reference ) {
261
+ case self ::ITEM_SPECIFIER_HEADERS :
262
+ return $ this ->headersRow ?? $ this ->firstDataRow ;
263
+ case self ::ITEM_SPECIFIER_DATA :
264
+ return $ this ->lastDataRow ;
265
+ case self ::ITEM_SPECIFIER_ALL :
266
+ case self ::ITEM_SPECIFIER_TOTALS :
267
+ return $ this ->totalsRow ?? $ this ->lastDataRow ;
268
+ }
269
+
270
+ return $ this ->totalsRow ?? $ this ->lastDataRow ;
271
+ }
272
+
45
273
public function value (): string
46
274
{
47
275
return $ this ->value ;
48
276
}
277
+
278
+ /**
279
+ * @return array<int, int>
280
+ */
281
+ private function getRowsForColumnReference (string &$ reference , int $ startRow , int $ endRow ): array
282
+ {
283
+ $ rowsSelected = false ;
284
+ foreach (self ::ITEM_SPECIFIER_ROWS_SET as $ rowReference ) {
285
+ $ pattern = '/\[ ' . $ rowReference . '\]/mui ' ;
286
+ /** @var string $reference */
287
+ if (preg_match ($ pattern , $ reference ) === 1 ) {
288
+ $ rowsSelected = true ;
289
+ $ startRow = min ($ startRow , $ this ->getMinimumRow ($ rowReference ));
290
+ $ endRow = max ($ endRow , $ this ->getMaximumRow ($ rowReference ));
291
+ $ reference = preg_replace ($ pattern , '' , $ reference );
292
+ }
293
+ }
294
+ if ($ rowsSelected === false ) {
295
+ // If there isn't any Special Item Identifier specified, then the selection defaults to data rows only.
296
+ $ startRow = $ this ->firstDataRow ;
297
+ $ endRow = $ this ->lastDataRow ;
298
+ }
299
+
300
+ return [$ startRow , $ endRow ];
301
+ }
302
+
303
+ private function getColumnsForColumnReference (string $ reference , int $ startRow , int $ endRow ): string
304
+ {
305
+ $ columnsSelected = false ;
306
+ foreach ($ this ->columns as $ columnId => $ columnName ) {
307
+ $ columnName = str_replace ("\u{a0}" , ' ' , $ columnName );
308
+ $ cellFrom = "{$ columnId }{$ startRow }" ;
309
+ $ cellTo = "{$ columnId }{$ endRow }" ;
310
+ $ cellReference = ($ cellFrom === $ cellTo ) ? $ cellFrom : "{$ cellFrom }: {$ cellTo }" ;
311
+ $ pattern = '/\[ ' . preg_quote ($ columnName ) . '\]/mui ' ;
312
+ if (preg_match ($ pattern , $ reference ) === 1 ) {
313
+ $ columnsSelected = true ;
314
+ $ reference = preg_replace ($ pattern , $ cellReference , $ reference );
315
+ }
316
+ /** @var string $reference */
317
+ }
318
+ if ($ columnsSelected === false ) {
319
+ return $ this ->fullData ($ startRow , $ endRow );
320
+ }
321
+
322
+ return $ reference ;
323
+ }
49
324
}
0 commit comments