FormulaParser::processDefaultCase()   B
last analyzed

Complexity

Conditions 7
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 14
rs 8.2222
cc 7
eloc 9
nc 2
nop 1
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->condition() can also be of type string. However, the property $parseTree is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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