1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Xls; |
4
|
|
|
|
5
|
|
|
class FormulaParser |
6
|
|
|
{ |
7
|
|
|
/** |
8
|
|
|
* The index of the character we are currently looking at |
9
|
|
|
* @var integer |
10
|
|
|
*/ |
11
|
|
|
protected $currentChar = 0; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* The token we are working on. |
15
|
|
|
* @var string |
16
|
|
|
*/ |
17
|
|
|
protected $currentToken = ''; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* The formula to parse |
21
|
|
|
* @var string |
22
|
|
|
*/ |
23
|
|
|
protected $formula = ''; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* The character ahead of the current char |
27
|
|
|
* @var string |
28
|
|
|
*/ |
29
|
|
|
protected $lookahead = ''; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* The parse tree to be generated |
33
|
|
|
* @var array |
34
|
|
|
*/ |
35
|
|
|
protected $parseTree = array(); |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Array of external sheets |
39
|
|
|
* @var array |
40
|
|
|
*/ |
41
|
|
|
protected $extSheets = array(); |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Array of sheet references in the form of REF structures |
45
|
|
|
* @var array |
46
|
|
|
*/ |
47
|
|
|
protected $references = array(); |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Convert a token to the proper ptg value. |
51
|
|
|
* |
52
|
|
|
* @param mixed $token The token to convert. |
53
|
|
|
* @return string the converted token |
54
|
|
|
* @throws \Exception |
55
|
|
|
*/ |
56
|
|
|
protected function convert($token) |
57
|
|
|
{ |
58
|
|
|
if (Token::isString($token)) { |
59
|
|
|
return $this->convertString($token); |
60
|
|
|
} elseif (is_numeric($token)) { |
61
|
|
|
return $this->convertNumber($token); |
62
|
|
|
} elseif (Token::isReference($token)) { |
63
|
|
|
return $this->convertRef2d($token); |
64
|
|
|
} elseif (Token::isExternalReference($token)) { |
65
|
|
|
return $this->convertRef3d($token); |
66
|
|
|
} elseif (Token::isRange($token)) { |
67
|
|
|
return $this->convertRange2d($token); |
68
|
|
|
} elseif (Token::isExternalRange($token)) { |
69
|
|
|
return $this->convertRange3d($token); |
70
|
|
|
} elseif (Ptg::exists($token)) { |
71
|
|
|
// operators (including parentheses) |
72
|
|
|
return pack("C", Ptg::get($token)); |
73
|
|
|
} elseif (Token::isArg($token)) { |
74
|
|
|
// if it's an argument, ignore the token (the argument remains) |
75
|
|
|
return ''; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
throw new \Exception("Unknown token $token"); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Convert a number token to ptgInt or ptgNum |
83
|
|
|
* |
84
|
|
|
* @param mixed $num an integer or double for conversion to its ptg value |
85
|
|
|
* @return string |
86
|
|
|
*/ |
87
|
|
|
protected function convertNumber($num) |
88
|
|
|
{ |
89
|
|
|
if (preg_match("/^\d+$/", $num) && $num <= 65535) { |
90
|
|
|
// Integer in the range 0..2**16-1 |
91
|
|
|
return pack("Cv", Ptg::get('ptgInt'), $num); |
92
|
|
|
} else { |
93
|
|
|
// A float |
94
|
|
|
return pack("Cd", Ptg::get('ptgNum'), $num); |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Convert a string token to ptgStr |
100
|
|
|
* |
101
|
|
|
* @param string $string A string for conversion to its ptg value. |
102
|
|
|
* @throws \Exception |
103
|
|
|
* @return string the converted token |
104
|
|
|
*/ |
105
|
|
|
protected function convertString($string) |
106
|
|
|
{ |
107
|
|
|
// chop away beggining and ending quotes |
108
|
|
|
$string = substr($string, 1, strlen($string) - 2); |
109
|
|
|
if (strlen($string) > Biff8::MAX_STR_LENGTH) { |
110
|
|
|
throw new \Exception("String is too long"); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
$encoding = 0; |
114
|
|
|
|
115
|
|
|
return pack("CCC", Ptg::get('ptgStr'), strlen($string), $encoding) . $string; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Convert a function to a ptgFunc or ptgFuncVarV depending on the number of |
120
|
|
|
* args that it takes. |
121
|
|
|
* |
122
|
|
|
* @param string $token The name of the function for convertion to ptg value. |
123
|
|
|
* @param integer $numArgs The number of arguments the function receives. |
124
|
|
|
* |
125
|
|
|
* @return string The packed ptg for the function |
126
|
|
|
*/ |
127
|
|
|
protected function convertFunction($token, $numArgs) |
128
|
|
|
{ |
129
|
|
|
$ptg = Functions::getPtg($token); |
130
|
|
|
$args = Functions::getArgsNumber($token); |
131
|
|
|
|
132
|
|
|
// Fixed number of args eg. TIME($i,$j,$k). |
133
|
|
|
if ($args >= 0) { |
134
|
|
|
return pack("Cv", Ptg::get('ptgFuncV'), $ptg); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
// Variable number of args eg. SUM($i,$j,$k, ..). |
138
|
|
|
return pack("CCv", Ptg::get('ptgFuncVarV'), $numArgs, $ptg); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Convert an Excel range such as A1:D4 to a ptgRefV. |
143
|
|
|
* |
144
|
|
|
* @param string $range An Excel range in the A1:A2 or A1..A2 format. |
145
|
|
|
* @return string |
146
|
|
|
*/ |
147
|
|
|
protected function convertRange2d($range) |
148
|
|
|
{ |
149
|
|
|
$separator = (Token::isRangeWithDots($range)) ? '..' : ':'; |
150
|
|
|
list($cell1, $cell2) = explode($separator, $range); |
151
|
|
|
|
152
|
|
|
// Convert the cell references |
153
|
|
|
list($row1, $col1) = $this->cellToPackedRowcol($cell1); |
154
|
|
|
list($row2, $col2) = $this->cellToPackedRowcol($cell2); |
155
|
|
|
|
156
|
|
|
$ptgArea = pack("C", Ptg::get('ptgArea')); |
157
|
|
|
|
158
|
|
|
return $ptgArea . $row1 . $row2 . $col1 . $col2; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to |
163
|
|
|
* a ptgArea3d. |
164
|
|
|
* |
165
|
|
|
* @param string $token An Excel range in the Sheet1!A1:A2 format. |
166
|
|
|
* @return string The packed ptgArea3d token |
167
|
|
|
*/ |
168
|
|
|
protected function convertRange3d($token) |
169
|
|
|
{ |
170
|
|
|
// Split the ref at the ! symbol |
171
|
|
|
list($extRef, $range) = explode('!', $token); |
172
|
|
|
|
173
|
|
|
// Convert the external reference part |
174
|
|
|
$extRef = $this->getRefIndex($extRef); |
175
|
|
|
|
176
|
|
|
// Split the range into 2 cell refs |
177
|
|
|
list($cell1, $cell2) = explode(':', $range); |
178
|
|
|
|
179
|
|
|
// Convert the cell references |
180
|
|
|
list($row1, $col1) = $this->cellToPackedRowcol($cell1); |
181
|
|
|
list($row2, $col2) = $this->cellToPackedRowcol($cell2); |
182
|
|
|
|
183
|
|
|
$ptgArea = pack("C", Ptg::get('ptgArea3dA')); |
184
|
|
|
|
185
|
|
|
return $ptgArea . $extRef . $row1 . $row2 . $col1 . $col2; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV. |
190
|
|
|
* |
191
|
|
|
* @param string $cell An Excel cell reference |
192
|
|
|
* @return string The cell in packed() format with the corresponding ptg |
193
|
|
|
*/ |
194
|
|
|
protected function convertRef2d($cell) |
195
|
|
|
{ |
196
|
|
|
list($row, $col) = $this->cellToPackedRowcol($cell); |
197
|
|
|
|
198
|
|
|
$ptgRef = pack("C", Ptg::get('ptgRefA')); |
199
|
|
|
|
200
|
|
|
return $ptgRef . $row . $col; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a |
205
|
|
|
* ptgRef3d. |
206
|
|
|
* |
207
|
|
|
* @param string $cell An Excel cell reference |
208
|
|
|
* @return string The packed ptgRef3d token |
209
|
|
|
*/ |
210
|
|
|
protected function convertRef3d($cell) |
211
|
|
|
{ |
212
|
|
|
// Split the ref at the ! symbol |
213
|
|
|
list($extRef, $cell) = explode('!', $cell); |
214
|
|
|
|
215
|
|
|
// Convert the external reference part |
216
|
|
|
$extRef = $this->getRefIndex($extRef); |
217
|
|
|
|
218
|
|
|
// Convert the cell reference part |
219
|
|
|
list($row, $col) = $this->cellToPackedRowcol($cell); |
220
|
|
|
|
221
|
|
|
$ptgRef = pack("C", Ptg::get('ptgRef3dA')); |
222
|
|
|
|
223
|
|
|
return $ptgRef . $extRef . $row . $col; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* @param string $str |
228
|
|
|
* |
229
|
|
|
* @return string |
230
|
|
|
*/ |
231
|
|
|
protected function removeTrailingQuotes($str) |
232
|
|
|
{ |
233
|
|
|
$str = preg_replace("/^'/", '', $str); // Remove leading ' if any. |
234
|
|
|
$str = preg_replace("/'$/", '', $str); // Remove trailing ' if any. |
235
|
|
|
|
236
|
|
|
return $str; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* @param $extRef |
241
|
|
|
* |
242
|
|
|
* @return array |
243
|
|
|
* @throws \Exception |
244
|
|
|
*/ |
245
|
|
|
protected function getRangeSheets($extRef) |
246
|
|
|
{ |
247
|
|
|
$extRef = $this->removeTrailingQuotes($extRef); |
248
|
|
|
|
249
|
|
|
// Check if there is a sheet range eg., Sheet1:Sheet2. |
250
|
|
|
if (preg_match("/:/", $extRef)) { |
251
|
|
|
list($sheetName1, $sheetName2) = explode(':', $extRef); |
252
|
|
|
|
253
|
|
|
$sheet1 = $this->getSheetIndex($sheetName1); |
254
|
|
|
if ($sheet1 == -1) { |
255
|
|
|
throw new \Exception("Unknown sheet name $sheetName1 in formula"); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
$sheet2 = $this->getSheetIndex($sheetName2); |
259
|
|
|
if ($sheet2 == -1) { |
260
|
|
|
throw new \Exception("Unknown sheet name $sheetName2 in formula"); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
// Reverse max and min sheet numbers if necessary |
264
|
|
|
if ($sheet1 > $sheet2) { |
265
|
|
|
list($sheet1, $sheet2) = array($sheet2, $sheet1); |
266
|
|
|
} |
267
|
|
|
} else { // Single sheet name only. |
268
|
|
|
$sheet1 = $this->getSheetIndex($extRef); |
269
|
|
|
if ($sheet1 == -1) { |
270
|
|
|
throw new \Exception("Unknown sheet name $extRef in formula"); |
271
|
|
|
} |
272
|
|
|
$sheet2 = $sheet1; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
return array($sheet1, $sheet2); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Look up the REF index that corresponds to an external sheet name |
280
|
|
|
* (or range). If it doesn't exist yet add it to the workbook's references |
281
|
|
|
* array. It assumes all sheet names given must exist. |
282
|
|
|
* |
283
|
|
|
* @param string $extRef The name of the external reference |
284
|
|
|
* |
285
|
|
|
* @throws \Exception |
286
|
|
|
* @return string The reference index in packed() format |
287
|
|
|
*/ |
288
|
|
|
protected function getRefIndex($extRef) |
289
|
|
|
{ |
290
|
|
|
list($sheet1, $sheet2) = $this->getRangeSheets($extRef); |
291
|
|
|
|
292
|
|
|
$index = $this->addRef($sheet1, $sheet2); |
293
|
|
|
|
294
|
|
|
return pack('v', $index); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Add reference and return its index |
299
|
|
|
* @param int $sheet1 |
300
|
|
|
* @param int $sheet2 |
301
|
|
|
* |
302
|
|
|
* @return int |
303
|
|
|
*/ |
304
|
|
|
public function addRef($sheet1, $sheet2) |
305
|
|
|
{ |
306
|
|
|
// assume all references belong to this document |
307
|
|
|
$supbookIndex = 0x00; |
308
|
|
|
$ref = pack('vvv', $supbookIndex, $sheet1, $sheet2); |
309
|
|
|
|
310
|
|
|
$index = array_search($ref, $this->references); |
311
|
|
|
if ($index === false) { |
312
|
|
|
// if REF was not found add it to references array |
313
|
|
|
$this->references[] = $ref; |
314
|
|
|
$index = count($this->references); |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
return $index; |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* Look up the index that corresponds to an external sheet name. The hash of |
322
|
|
|
* sheet names is updated by the addworksheet() method of the |
323
|
|
|
* Workbook class. |
324
|
|
|
* |
325
|
|
|
* @param string $sheetName |
326
|
|
|
* |
327
|
|
|
* @return integer The sheet index, -1 if the sheet was not found |
328
|
|
|
*/ |
329
|
|
|
protected function getSheetIndex($sheetName) |
330
|
|
|
{ |
331
|
|
|
if (!isset($this->extSheets[$sheetName])) { |
332
|
|
|
return -1; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
return $this->extSheets[$sheetName]; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* This method is used to update the array of sheet names. It is |
340
|
|
|
* called by the addWorksheet() method of the |
341
|
|
|
* Workbook class. |
342
|
|
|
* |
343
|
|
|
* @see Workbook::addWorksheet() |
344
|
|
|
* @param string $name The name of the worksheet being added |
345
|
|
|
* @param integer $index The index of the worksheet being added |
346
|
|
|
*/ |
347
|
|
|
public function addSheet($name, $index) |
348
|
|
|
{ |
349
|
|
|
$this->extSheets[$name] = $index; |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
/** |
353
|
|
|
* pack() row and column into the required 3 or 4 byte format. |
354
|
|
|
* |
355
|
|
|
* @param string $cellAddress The Excel cell reference to be packed |
356
|
|
|
* |
357
|
|
|
* @throws \Exception |
358
|
|
|
* @return array Array containing the row and column in packed() format |
359
|
|
|
*/ |
360
|
|
|
protected function cellToPackedRowcol($cellAddress) |
361
|
|
|
{ |
362
|
|
|
$cellAddress = strtoupper($cellAddress); |
363
|
|
|
$cell = Cell::createFromAddress($cellAddress); |
364
|
|
|
|
365
|
|
|
// Set the high bits to indicate if row or col are relative. |
366
|
|
|
$col = $cell->getCol(); |
367
|
|
|
$col |= (int)$cell->isColRelative() << 14; |
368
|
|
|
$col |= (int)$cell->isRowRelative() << 15; |
369
|
|
|
$col = pack('v', $col); |
370
|
|
|
|
371
|
|
|
$row = pack('v', $cell->getRow()); |
372
|
|
|
|
373
|
|
|
return array($row, $col); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* Advance to the next valid token. |
378
|
|
|
* |
379
|
|
|
*/ |
380
|
|
|
protected function advance() |
381
|
|
|
{ |
382
|
|
|
$token = ''; |
383
|
|
|
|
384
|
|
|
$position = $this->eatWhitespace(); |
385
|
|
|
$formulaLength = strlen($this->formula); |
386
|
|
|
|
387
|
|
|
while ($position < $formulaLength) { |
388
|
|
|
$token .= $this->formula[$position]; |
389
|
|
View Code Duplication |
if ($position < $formulaLength - 1) { |
|
|
|
|
390
|
|
|
$this->lookahead = $this->formula[$position + 1]; |
391
|
|
|
} else { |
392
|
|
|
$this->lookahead = ''; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
if ($this->match($token) != '') { |
396
|
|
|
$this->currentChar = $position + 1; |
397
|
|
|
$this->currentToken = $token; |
398
|
|
|
return; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
View Code Duplication |
if ($position < ($formulaLength - 2)) { |
|
|
|
|
402
|
|
|
$this->lookahead = $this->formula[$position + 2]; |
403
|
|
|
} else { |
404
|
|
|
// if we run out of characters lookahead becomes empty |
405
|
|
|
$this->lookahead = ''; |
406
|
|
|
} |
407
|
|
|
$position++; |
408
|
|
|
} |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* @return int |
413
|
|
|
*/ |
414
|
|
|
protected function eatWhitespace() |
415
|
|
|
{ |
416
|
|
|
$position = $this->currentChar; |
417
|
|
|
$formulaLength = strlen($this->formula); |
418
|
|
|
|
419
|
|
|
// eat up white spaces |
420
|
|
|
if ($position < $formulaLength) { |
421
|
|
|
while ($this->formula{$position} == " ") { |
422
|
|
|
$position++; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
if ($position < ($formulaLength - 1)) { |
426
|
|
|
$this->lookahead = $this->formula{$position + 1}; |
427
|
|
|
} |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
return $position; |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* Checks if it's a valid token. |
435
|
|
|
* |
436
|
|
|
* @param string $token The token to check. |
437
|
|
|
* @return string The checked token |
438
|
|
|
*/ |
439
|
|
|
protected function match($token) |
440
|
|
|
{ |
441
|
|
|
if (Token::isDeterministic($token)) { |
442
|
|
|
return $token; |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
if (Token::isLtOrGt($token)) { |
446
|
|
|
if (!Token::isPossibleLookahead($token, $this->lookahead)) { |
447
|
|
|
// it's not a GE, LTE or NE token |
448
|
|
|
return $token; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
return ''; |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
return $this->processDefaultCase($token); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* @param string $token |
459
|
|
|
* |
460
|
|
|
* @return string |
461
|
|
|
*/ |
462
|
|
|
protected function processDefaultCase($token) |
463
|
|
|
{ |
464
|
|
|
if ($this->isReference($token) |
465
|
|
|
|| $this->isExternalReference($token) |
466
|
|
|
|| $this->isAnyRange($token) |
467
|
|
|
|| $this->isNumber($token) |
468
|
|
|
|| Token::isString($token) |
469
|
|
|
|| $this->isFunctionCall($token) |
470
|
|
|
) { |
471
|
|
|
return $token; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
return ''; |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
/** |
478
|
|
|
* @return bool |
479
|
|
|
*/ |
480
|
|
|
protected function lookaheadHasNumber() |
481
|
|
|
{ |
482
|
|
|
return preg_match("/[0-9]/", $this->lookahead) === 1; |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
/** |
486
|
|
|
* @return bool |
487
|
|
|
*/ |
488
|
|
|
protected function isLookaheadDotOrColon() |
489
|
|
|
{ |
490
|
|
|
return $this->lookahead === '.' || $this->lookahead === ':'; |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
/** |
494
|
|
|
* @param string $token |
495
|
|
|
* |
496
|
|
|
* @return bool |
497
|
|
|
*/ |
498
|
|
|
protected function isAnyRange($token) |
499
|
|
|
{ |
500
|
|
|
return Token::isAnyRange($token) |
501
|
|
|
&& !$this->lookaheadHasNumber(); |
502
|
|
|
} |
503
|
|
|
|
504
|
|
|
/** |
505
|
|
|
* @param string $token |
506
|
|
|
* |
507
|
|
|
* @return bool |
508
|
|
|
*/ |
509
|
|
|
protected function isReference($token) |
510
|
|
|
{ |
511
|
|
|
return Token::isReference($token) |
512
|
|
|
&& !$this->lookaheadHasNumber() |
513
|
|
|
&& !$this->isLookaheadDotOrColon() |
514
|
|
|
&& $this->lookahead != '!'; |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
/** |
518
|
|
|
* @param string $token |
519
|
|
|
* |
520
|
|
|
* @return bool |
521
|
|
|
*/ |
522
|
|
|
protected function isExternalReference($token) |
523
|
|
|
{ |
524
|
|
|
return Token::isExternalReference($token) |
525
|
|
|
&& !$this->lookaheadHasNumber() |
526
|
|
|
&& !$this->isLookaheadDotOrColon(); |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
/** |
530
|
|
|
* If it's a number (check that it's not a sheet name or range) |
531
|
|
|
* @param string $token |
532
|
|
|
* |
533
|
|
|
* @return bool |
534
|
|
|
*/ |
535
|
|
|
protected function isNumber($token) |
536
|
|
|
{ |
537
|
|
|
return is_numeric($token) |
538
|
|
|
&& (!is_numeric($token . $this->lookahead) || $this->lookahead == '') |
539
|
|
|
&& $this->lookahead != '!' |
540
|
|
|
&& $this->lookahead != ':'; |
541
|
|
|
} |
542
|
|
|
|
543
|
|
|
/** |
544
|
|
|
* @param string $token |
545
|
|
|
* |
546
|
|
|
* @return bool |
547
|
|
|
*/ |
548
|
|
|
protected function isFunctionCall($token) |
549
|
|
|
{ |
550
|
|
|
return Token::isFunctionCall($token) |
551
|
|
|
&& $this->lookahead == "("; |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
/** |
555
|
|
|
* The parsing method. It parses a formula. |
556
|
|
|
* |
557
|
|
|
* @param string $formula The formula to parse, without the initial equal sign (=). |
558
|
|
|
*/ |
559
|
|
|
public function parse($formula) |
560
|
|
|
{ |
561
|
|
|
$this->parseTree = array(); |
562
|
|
|
$this->currentChar = 0; |
563
|
|
|
$this->currentToken = ''; |
564
|
|
|
$this->formula = $formula; |
565
|
|
|
$this->lookahead = (isset($formula[1])) ? $formula[1] : ''; |
566
|
|
|
$this->advance(); |
567
|
|
|
$this->parseTree = $this->condition(); |
|
|
|
|
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
/** |
571
|
|
|
* It parses a condition. It assumes the following rule: |
572
|
|
|
* Cond -> Expr [(">" | "<") Expr] |
573
|
|
|
* |
574
|
|
|
* @return array The parsed ptg'd tree |
575
|
|
|
*/ |
576
|
|
View Code Duplication |
protected function condition() |
|
|
|
|
577
|
|
|
{ |
578
|
|
|
$result = $this->expression(); |
579
|
|
|
|
580
|
|
|
if (Token::isComparison($this->currentToken) || Token::isConcat($this->currentToken)) { |
581
|
|
|
$ptg = Token::getPtg($this->currentToken); |
582
|
|
|
$this->advance(); |
583
|
|
|
$result = $this->createTree($ptg, $result, $this->expression()); |
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
return $result; |
587
|
|
|
} |
588
|
|
|
|
589
|
|
|
/** |
590
|
|
|
* It parses a expression. It assumes the following rule: |
591
|
|
|
* Expr -> Term [("+" | "-") Term] |
592
|
|
|
* -> "string" |
593
|
|
|
* -> "-" Term |
594
|
|
|
* |
595
|
|
|
* @return array The parsed ptg'd tree |
596
|
|
|
*/ |
597
|
|
|
protected function expression() |
598
|
|
|
{ |
599
|
|
|
// If it's a string return a string node |
600
|
|
|
if (Token::isString($this->currentToken)) { |
601
|
|
|
$result = $this->createTree($this->currentToken, '', ''); |
602
|
|
|
$this->advance(); |
603
|
|
|
|
604
|
|
|
return $result; |
605
|
|
|
} elseif ($this->currentToken == Token::TOKEN_SUB) { |
606
|
|
|
// catch "-" Term |
607
|
|
|
$this->advance(); |
608
|
|
|
|
609
|
|
|
return $this->createTree('ptgUminus', $this->expression(), ''); |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
$result = $this->term(); |
613
|
|
|
|
614
|
|
|
while (Token::isAddOrSub($this->currentToken)) { |
615
|
|
|
$ptg = Token::getPtg($this->currentToken); |
616
|
|
|
$this->advance(); |
617
|
|
|
$result = $this->createTree($ptg, $result, $this->term()); |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
return $result; |
621
|
|
|
} |
622
|
|
|
|
623
|
|
|
/** |
624
|
|
|
* This function just introduces a ptgParen element in the tree, so that Excel |
625
|
|
|
* doesn't get confused when working with a parenthesized formula afterwards. |
626
|
|
|
* |
627
|
|
|
* @see _fact() |
628
|
|
|
* @return array The parsed ptg'd tree |
629
|
|
|
*/ |
630
|
|
|
protected function parenthesizedExpression() |
631
|
|
|
{ |
632
|
|
|
return $this->createTree('ptgParen', $this->expression(), ''); |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
/** |
636
|
|
|
* It parses a term. It assumes the following rule: |
637
|
|
|
* Term -> Fact [("*" | "/") Fact] |
638
|
|
|
* |
639
|
|
|
* @return array The parsed ptg'd tree |
640
|
|
|
*/ |
641
|
|
View Code Duplication |
protected function term() |
|
|
|
|
642
|
|
|
{ |
643
|
|
|
$result = $this->fact(); |
644
|
|
|
|
645
|
|
|
while (Token::isMulOrDiv($this->currentToken)) { |
646
|
|
|
$ptg = Token::getPtg($this->currentToken); |
647
|
|
|
$this->advance(); |
648
|
|
|
$result = $this->createTree($ptg, $result, $this->fact()); |
649
|
|
|
} |
650
|
|
|
|
651
|
|
|
return $result; |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
/** |
655
|
|
|
* It parses a factor. It assumes the following rule: |
656
|
|
|
* Fact -> ( Expr ) |
657
|
|
|
* | CellRef |
658
|
|
|
* | CellRange |
659
|
|
|
* | Number |
660
|
|
|
* | Function |
661
|
|
|
* @throws \Exception |
662
|
|
|
* @return array The parsed ptg'd tree |
663
|
|
|
*/ |
664
|
|
|
protected function fact() |
665
|
|
|
{ |
666
|
|
|
if ($this->currentToken == Token::TOKEN_OPEN) { |
667
|
|
|
$this->advance(); // eat the "(" |
668
|
|
|
|
669
|
|
|
$result = $this->parenthesizedExpression(); |
670
|
|
|
if ($this->currentToken != Token::TOKEN_CLOSE) { |
671
|
|
|
throw new \Exception("')' token expected."); |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
$this->advance(); // eat the ")" |
675
|
|
|
|
676
|
|
|
return $result; |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
if (Token::isAnyReference($this->currentToken)) { |
680
|
|
|
$result = $this->createTree($this->currentToken, '', ''); |
681
|
|
|
$this->advance(); |
682
|
|
|
|
683
|
|
|
return $result; |
684
|
|
|
} elseif (Token::isAnyRange($this->currentToken)) { |
685
|
|
|
$result = $this->currentToken; |
686
|
|
|
$this->advance(); |
687
|
|
|
|
688
|
|
|
return $result; |
689
|
|
|
} elseif (is_numeric($this->currentToken)) { |
690
|
|
|
$result = $this->createTree($this->currentToken, '', ''); |
691
|
|
|
$this->advance(); |
692
|
|
|
|
693
|
|
|
return $result; |
694
|
|
|
} elseif (Token::isFunctionCall($this->currentToken)) { |
695
|
|
|
$result = $this->func(); |
696
|
|
|
|
697
|
|
|
return $result; |
698
|
|
|
} |
699
|
|
|
|
700
|
|
|
throw new \Exception( |
701
|
|
|
"Syntax error: " . $this->currentToken . |
702
|
|
|
", lookahead: " . $this->lookahead . |
703
|
|
|
", current char: " . $this->currentChar |
704
|
|
|
); |
705
|
|
|
} |
706
|
|
|
|
707
|
|
|
/** |
708
|
|
|
* It parses a function call. It assumes the following rule: |
709
|
|
|
* Func -> ( Expr [,Expr]* ) |
710
|
|
|
* @throws \Exception |
711
|
|
|
* @return string|array The parsed ptg'd tree |
712
|
|
|
*/ |
713
|
|
|
protected function func() |
714
|
|
|
{ |
715
|
|
|
$numArgs = 0; // number of arguments received |
716
|
|
|
$function = strtoupper($this->currentToken); |
717
|
|
|
$result = ''; // initialize result |
718
|
|
|
|
719
|
|
|
$this->advance(); |
720
|
|
|
$this->advance(); // eat the "(" |
721
|
|
|
|
722
|
|
|
while ($this->currentToken != ')') { |
723
|
|
|
if ($numArgs > 0) { |
724
|
|
|
if (!Token::isCommaOrSemicolon($this->currentToken)) { |
725
|
|
|
throw new \Exception( |
726
|
|
|
"Syntax error: comma expected in " . |
727
|
|
|
"function $function, arg #{$numArgs}" |
728
|
|
|
); |
729
|
|
|
} |
730
|
|
|
|
731
|
|
|
$this->advance(); // eat the "," or ";" |
732
|
|
|
} else { |
733
|
|
|
$result = ''; |
734
|
|
|
} |
735
|
|
|
|
736
|
|
|
$result = $this->createTree('arg', $result, $this->condition()); |
737
|
|
|
|
738
|
|
|
$numArgs++; |
739
|
|
|
} |
740
|
|
|
|
741
|
|
|
$args = Functions::getArgsNumber($function); |
742
|
|
|
if ($args >= 0 && $args != $numArgs) { |
743
|
|
|
// If fixed number of args eg. TIME($i,$j,$k). Check that the number of args is valid. |
744
|
|
|
throw new \Exception("Incorrect number of arguments in function $function() "); |
745
|
|
|
} |
746
|
|
|
|
747
|
|
|
$result = $this->createTree($function, $result, $numArgs); |
748
|
|
|
$this->advance(); // eat the ")" |
749
|
|
|
|
750
|
|
|
return $result; |
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
/** |
754
|
|
|
* Creates a tree. In fact an array which may have one or two arrays (sub-trees) |
755
|
|
|
* as elements. |
756
|
|
|
* |
757
|
|
|
* @param mixed $value The value of this node. |
758
|
|
|
* @param mixed $left The left array (sub-tree) or a final node. |
759
|
|
|
* @param mixed $right The right array (sub-tree) or a final node. |
760
|
|
|
* @return array A tree |
761
|
|
|
*/ |
762
|
|
|
protected function createTree($value, $left, $right) |
763
|
|
|
{ |
764
|
|
|
return array( |
765
|
|
|
'value' => $value, |
766
|
|
|
'left' => $left, |
767
|
|
|
'right' => $right |
768
|
|
|
); |
769
|
|
|
} |
770
|
|
|
|
771
|
|
|
/** |
772
|
|
|
* Builds a string containing the tree in reverse polish notation (What you |
773
|
|
|
* would use in a HP calculator stack). |
774
|
|
|
* The following tree: |
775
|
|
|
* |
776
|
|
|
* + |
777
|
|
|
* / \ |
778
|
|
|
* 2 3 |
779
|
|
|
* |
780
|
|
|
* produces: "23+" |
781
|
|
|
* |
782
|
|
|
* The following tree: |
783
|
|
|
* |
784
|
|
|
* + |
785
|
|
|
* / \ |
786
|
|
|
* 3 * |
787
|
|
|
* / \ |
788
|
|
|
* 6 A1 |
789
|
|
|
* |
790
|
|
|
* produces: "36A1*+" |
791
|
|
|
* |
792
|
|
|
* In fact all operands, functions, references, etc... are written as ptg's |
793
|
|
|
* |
794
|
|
|
* @param array $tree The optional tree to convert. |
795
|
|
|
* @return string The tree in reverse polish notation |
796
|
|
|
*/ |
797
|
|
|
protected function toReversePolish($tree) |
798
|
|
|
{ |
799
|
|
|
if (!is_array($tree)) { |
800
|
|
|
return $this->convert($tree); |
801
|
|
|
} |
802
|
|
|
|
803
|
|
|
// if it's a function convert it here (so we can set it's arguments) |
804
|
|
|
if ($this->isFunction($tree['value'])) { |
805
|
|
|
return $this->getFunctionPolish($tree); |
806
|
|
|
} |
807
|
|
|
|
808
|
|
|
$polish = $this->getTreePartPolish($tree['left']); |
809
|
|
|
$polish .= $this->getTreePartPolish($tree['right']); |
810
|
|
|
$polish .= $this->convert($tree['value']); |
811
|
|
|
|
812
|
|
|
return $polish; |
813
|
|
|
} |
814
|
|
|
|
815
|
|
|
/** |
816
|
|
|
* @param $tree |
817
|
|
|
* |
818
|
|
|
* @return string |
819
|
|
|
*/ |
820
|
|
|
protected function getFunctionPolish($tree) |
821
|
|
|
{ |
822
|
|
|
$polish = ''; |
823
|
|
|
|
824
|
|
|
// left subtree for a function is always an array. |
825
|
|
|
if ($tree['left'] != '') { |
826
|
|
|
$polish .= $this->toReversePolish($tree['left']); |
827
|
|
|
} |
828
|
|
|
|
829
|
|
|
$polish .= $this->convertFunction($tree['value'], $tree['right']); |
830
|
|
|
|
831
|
|
|
return $polish; |
832
|
|
|
} |
833
|
|
|
|
834
|
|
|
/** |
835
|
|
|
* @param $value |
836
|
|
|
* |
837
|
|
|
* @return bool |
838
|
|
|
*/ |
839
|
|
|
protected function isFunction($value) |
840
|
|
|
{ |
841
|
|
|
return Token::isFunctionCall($value) |
842
|
|
|
&& !Token::isReference($value) |
843
|
|
|
&& !Token::isRangeWithDots($value) |
844
|
|
|
&& !is_numeric($value) |
845
|
|
|
&& !Ptg::exists($value); |
846
|
|
|
} |
847
|
|
|
|
848
|
|
|
/** |
849
|
|
|
* @param $part |
850
|
|
|
* |
851
|
|
|
* @return string |
852
|
|
|
* @throws \Exception |
853
|
|
|
*/ |
854
|
|
|
protected function getTreePartPolish($part) |
855
|
|
|
{ |
856
|
|
|
$polish = ''; |
857
|
|
|
|
858
|
|
|
if (is_array($part)) { |
859
|
|
|
$polish .= $this->toReversePolish($part); |
860
|
|
|
} elseif ($part != '') { |
861
|
|
|
// It's a final node |
862
|
|
|
$polish .= $this->convert($part); |
863
|
|
|
} |
864
|
|
|
|
865
|
|
|
return $polish; |
866
|
|
|
} |
867
|
|
|
|
868
|
|
|
/** |
869
|
|
|
* @return array |
870
|
|
|
*/ |
871
|
|
|
public function getReferences() |
872
|
|
|
{ |
873
|
|
|
return $this->references; |
874
|
|
|
} |
875
|
|
|
|
876
|
|
|
/** |
877
|
|
|
* @param $formula |
878
|
|
|
* |
879
|
|
|
* @return string |
880
|
|
|
*/ |
881
|
|
|
public function getReversePolish($formula) |
882
|
|
|
{ |
883
|
|
|
$this->parse($formula); |
884
|
|
|
|
885
|
|
|
return $this->toReversePolish($this->parseTree); |
886
|
|
|
} |
887
|
|
|
} |
888
|
|
|
|
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.