Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
 
3
namespace PhpOffice\PhpSpreadsheet\Calculation;
4
 
5
/**
6
 * PARTLY BASED ON:
7
 * Copyright (c) 2007 E. W. Bachtal, Inc.
8
 *
9
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
10
 * and associated documentation files (the "Software"), to deal in the Software without restriction,
11
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
12
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
13
 * subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in all copies or substantial
16
 * portions of the Software.
17
 *
18
 * The software is provided "as is", without warranty of any kind, express or implied, including but not
19
 * limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In
20
 * no event shall the authors or copyright holders be liable for any claim, damages or other liability,
21
 * whether in an action of contract, tort or otherwise, arising from, out of or in connection with the
22
 * software or the use or other dealings in the software.
23
 *
24
 * https://ewbi.blogs.com/develops/2007/03/excel_formula_p.html
25
 * https://ewbi.blogs.com/develops/2004/12/excel_formula_p.html
26
 */
27
class FormulaParser
28
{
29
    // Character constants
30
    const QUOTE_DOUBLE = '"';
31
    const QUOTE_SINGLE = '\'';
32
    const BRACKET_CLOSE = ']';
33
    const BRACKET_OPEN = '[';
34
    const BRACE_OPEN = '{';
35
    const BRACE_CLOSE = '}';
36
    const PAREN_OPEN = '(';
37
    const PAREN_CLOSE = ')';
38
    const SEMICOLON = ';';
39
    const WHITESPACE = ' ';
40
    const COMMA = ',';
41
    const ERROR_START = '#';
42
 
43
    const OPERATORS_SN = '+-';
44
    const OPERATORS_INFIX = '+-*/^&=><';
45
    const OPERATORS_POSTFIX = '%';
46
 
47
    /**
48
     * Formula.
49
     */
50
    private string $formula;
51
 
52
    /**
53
     * Tokens.
54
     *
55
     * @var FormulaToken[]
56
     */
57
    private array $tokens = [];
58
 
59
    /**
60
     * Create a new FormulaParser.
61
     *
62
     * @param ?string $formula Formula to parse
63
     */
64
    public function __construct(?string $formula = '')
65
    {
66
        // Check parameters
67
        if ($formula === null) {
68
            throw new Exception('Invalid parameter passed: formula');
69
        }
70
 
71
        // Initialise values
72
        $this->formula = trim($formula);
73
        // Parse!
74
        $this->parseToTokens();
75
    }
76
 
77
    /**
78
     * Get Formula.
79
     */
80
    public function getFormula(): string
81
    {
82
        return $this->formula;
83
    }
84
 
85
    /**
86
     * Get Token.
87
     *
88
     * @param int $id Token id
89
     */
90
    public function getToken(int $id = 0): FormulaToken
91
    {
92
        if (isset($this->tokens[$id])) {
93
            return $this->tokens[$id];
94
        }
95
 
96
        throw new Exception("Token with id $id does not exist.");
97
    }
98
 
99
    /**
100
     * Get Token count.
101
     */
102
    public function getTokenCount(): int
103
    {
104
        return count($this->tokens);
105
    }
106
 
107
    /**
108
     * Get Tokens.
109
     *
110
     * @return FormulaToken[]
111
     */
112
    public function getTokens(): array
113
    {
114
        return $this->tokens;
115
    }
116
 
117
    /**
118
     * Parse to tokens.
119
     */
120
    private function parseToTokens(): void
121
    {
122
        // No attempt is made to verify formulas; assumes formulas are derived from Excel, where
123
        // they can only exist if valid; stack overflows/underflows sunk as nulls without exceptions.
124
 
125
        // Check if the formula has a valid starting =
126
        $formulaLength = strlen($this->formula);
127
        if ($formulaLength < 2 || $this->formula[0] != '=') {
128
            return;
129
        }
130
 
131
        // Helper variables
132
        $tokens1 = $tokens2 = $stack = [];
133
        $inString = $inPath = $inRange = $inError = false;
134
        $nextToken = null;
135
        //$token = $previousToken = null;
136
 
137
        $index = 1;
138
        $value = '';
139
 
140
        $ERRORS = ['#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!', '#N/A'];
141
        $COMPARATORS_MULTI = ['>=', '<=', '<>'];
142
 
143
        while ($index < $formulaLength) {
144
            // state-dependent character evaluation (order is important)
145
 
146
            // double-quoted strings
147
            // embeds are doubled
148
            // end marks token
149
            if ($inString) {
150
                if ($this->formula[$index] == self::QUOTE_DOUBLE) {
151
                    if ((($index + 2) <= $formulaLength) && ($this->formula[$index + 1] == self::QUOTE_DOUBLE)) {
152
                        $value .= self::QUOTE_DOUBLE;
153
                        ++$index;
154
                    } else {
155
                        $inString = false;
156
                        $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND, FormulaToken::TOKEN_SUBTYPE_TEXT);
157
                        $value = '';
158
                    }
159
                } else {
160
                    $value .= $this->formula[$index];
161
                }
162
                ++$index;
163
 
164
                continue;
165
            }
166
 
167
            // single-quoted strings (links)
168
            // embeds are double
169
            // end does not mark a token
170
            if ($inPath) {
171
                if ($this->formula[$index] == self::QUOTE_SINGLE) {
172
                    if ((($index + 2) <= $formulaLength) && ($this->formula[$index + 1] == self::QUOTE_SINGLE)) {
173
                        $value .= self::QUOTE_SINGLE;
174
                        ++$index;
175
                    } else {
176
                        $inPath = false;
177
                    }
178
                } else {
179
                    $value .= $this->formula[$index];
180
                }
181
                ++$index;
182
 
183
                continue;
184
            }
185
 
186
            // bracked strings (R1C1 range index or linked workbook name)
187
            // no embeds (changed to "()" by Excel)
188
            // end does not mark a token
189
            if ($inRange) {
190
                if ($this->formula[$index] == self::BRACKET_CLOSE) {
191
                    $inRange = false;
192
                }
193
                $value .= $this->formula[$index];
194
                ++$index;
195
 
196
                continue;
197
            }
198
 
199
            // error values
200
            // end marks a token, determined from absolute list of values
201
            if ($inError) {
202
                $value .= $this->formula[$index];
203
                ++$index;
204
                if (in_array($value, $ERRORS)) {
205
                    $inError = false;
206
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND, FormulaToken::TOKEN_SUBTYPE_ERROR);
207
                    $value = '';
208
                }
209
 
210
                continue;
211
            }
212
 
213
            // scientific notation check
214
            if (str_contains(self::OPERATORS_SN, $this->formula[$index])) {
215
                if (strlen($value) > 1) {
216
                    if (preg_match('/^[1-9]{1}(\.\d+)?E{1}$/', $this->formula[$index]) != 0) {
217
                        $value .= $this->formula[$index];
218
                        ++$index;
219
 
220
                        continue;
221
                    }
222
                }
223
            }
224
 
225
            // independent character evaluation (order not important)
226
 
227
            // establish state-dependent character evaluations
228
            if ($this->formula[$index] == self::QUOTE_DOUBLE) {
229
                if ($value !== '') {
230
                    // unexpected
231
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
232
                    $value = '';
233
                }
234
                $inString = true;
235
                ++$index;
236
 
237
                continue;
238
            }
239
 
240
            if ($this->formula[$index] == self::QUOTE_SINGLE) {
241
                if ($value !== '') {
242
                    // unexpected
243
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
244
                    $value = '';
245
                }
246
                $inPath = true;
247
                ++$index;
248
 
249
                continue;
250
            }
251
 
252
            if ($this->formula[$index] == self::BRACKET_OPEN) {
253
                $inRange = true;
254
                $value .= self::BRACKET_OPEN;
255
                ++$index;
256
 
257
                continue;
258
            }
259
 
260
            if ($this->formula[$index] == self::ERROR_START) {
261
                if ($value !== '') {
262
                    // unexpected
263
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
264
                    $value = '';
265
                }
266
                $inError = true;
267
                $value .= self::ERROR_START;
268
                ++$index;
269
 
270
                continue;
271
            }
272
 
273
            // mark start and end of arrays and array rows
274
            if ($this->formula[$index] == self::BRACE_OPEN) {
275
                if ($value !== '') {
276
                    // unexpected
277
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
278
                    $value = '';
279
                }
280
 
281
                $tmp = new FormulaToken('ARRAY', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
282
                $tokens1[] = $tmp;
283
                $stack[] = clone $tmp;
284
 
285
                $tmp = new FormulaToken('ARRAYROW', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
286
                $tokens1[] = $tmp;
287
                $stack[] = clone $tmp;
288
 
289
                ++$index;
290
 
291
                continue;
292
            }
293
 
294
            if ($this->formula[$index] == self::SEMICOLON) {
295
                if ($value !== '') {
296
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
297
                    $value = '';
298
                }
299
 
300
                /** @var FormulaToken $tmp */
301
                $tmp = array_pop($stack);
302
                $tmp->setValue('');
303
                $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
304
                $tokens1[] = $tmp;
305
 
306
                $tmp = new FormulaToken(',', FormulaToken::TOKEN_TYPE_ARGUMENT);
307
                $tokens1[] = $tmp;
308
 
309
                $tmp = new FormulaToken('ARRAYROW', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
310
                $tokens1[] = $tmp;
311
                $stack[] = clone $tmp;
312
 
313
                ++$index;
314
 
315
                continue;
316
            }
317
 
318
            if ($this->formula[$index] == self::BRACE_CLOSE) {
319
                if ($value !== '') {
320
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
321
                    $value = '';
322
                }
323
 
324
                /** @var FormulaToken $tmp */
325
                $tmp = array_pop($stack);
326
                $tmp->setValue('');
327
                $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
328
                $tokens1[] = $tmp;
329
 
330
                /** @var FormulaToken $tmp */
331
                $tmp = array_pop($stack);
332
                $tmp->setValue('');
333
                $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
334
                $tokens1[] = $tmp;
335
 
336
                ++$index;
337
 
338
                continue;
339
            }
340
 
341
            // trim white-space
342
            if ($this->formula[$index] == self::WHITESPACE) {
343
                if ($value !== '') {
344
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
345
                    $value = '';
346
                }
347
                $tokens1[] = new FormulaToken('', FormulaToken::TOKEN_TYPE_WHITESPACE);
348
                ++$index;
349
                while (($this->formula[$index] == self::WHITESPACE) && ($index < $formulaLength)) {
350
                    ++$index;
351
                }
352
 
353
                continue;
354
            }
355
 
356
            // multi-character comparators
357
            if (($index + 2) <= $formulaLength) {
358
                if (in_array(substr($this->formula, $index, 2), $COMPARATORS_MULTI)) {
359
                    if ($value !== '') {
360
                        $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
361
                        $value = '';
362
                    }
363
                    $tokens1[] = new FormulaToken(substr($this->formula, $index, 2), FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_LOGICAL);
364
                    $index += 2;
365
 
366
                    continue;
367
                }
368
            }
369
 
370
            // standard infix operators
371
            if (str_contains(self::OPERATORS_INFIX, $this->formula[$index])) {
372
                if ($value !== '') {
373
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
374
                    $value = '';
375
                }
376
                $tokens1[] = new FormulaToken($this->formula[$index], FormulaToken::TOKEN_TYPE_OPERATORINFIX);
377
                ++$index;
378
 
379
                continue;
380
            }
381
 
382
            // standard postfix operators (only one)
383
            if (str_contains(self::OPERATORS_POSTFIX, $this->formula[$index])) {
384
                if ($value !== '') {
385
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
386
                    $value = '';
387
                }
388
                $tokens1[] = new FormulaToken($this->formula[$index], FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX);
389
                ++$index;
390
 
391
                continue;
392
            }
393
 
394
            // start subexpression or function
395
            if ($this->formula[$index] == self::PAREN_OPEN) {
396
                if ($value !== '') {
397
                    $tmp = new FormulaToken($value, FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
398
                    $tokens1[] = $tmp;
399
                    $stack[] = clone $tmp;
400
                    $value = '';
401
                } else {
402
                    $tmp = new FormulaToken('', FormulaToken::TOKEN_TYPE_SUBEXPRESSION, FormulaToken::TOKEN_SUBTYPE_START);
403
                    $tokens1[] = $tmp;
404
                    $stack[] = clone $tmp;
405
                }
406
                ++$index;
407
 
408
                continue;
409
            }
410
 
411
            // function, subexpression, or array parameters, or operand unions
412
            if ($this->formula[$index] == self::COMMA) {
413
                if ($value !== '') {
414
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
415
                    $value = '';
416
                }
417
 
418
                /** @var FormulaToken $tmp */
419
                $tmp = array_pop($stack);
420
                $tmp->setValue('');
421
                $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
422
                $stack[] = $tmp;
423
 
424
                if ($tmp->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) {
425
                    $tokens1[] = new FormulaToken(',', FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_UNION);
426
                } else {
427
                    $tokens1[] = new FormulaToken(',', FormulaToken::TOKEN_TYPE_ARGUMENT);
428
                }
429
                ++$index;
430
 
431
                continue;
432
            }
433
 
434
            // stop subexpression
435
            if ($this->formula[$index] == self::PAREN_CLOSE) {
436
                if ($value !== '') {
437
                    $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
438
                    $value = '';
439
                }
440
 
441
                /** @var FormulaToken $tmp */
442
                $tmp = array_pop($stack);
443
                $tmp->setValue('');
444
                $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
445
                $tokens1[] = $tmp;
446
 
447
                ++$index;
448
 
449
                continue;
450
            }
451
 
452
            // token accumulation
453
            $value .= $this->formula[$index];
454
            ++$index;
455
        }
456
 
457
        // dump remaining accumulation
458
        if ($value !== '') {
459
            $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
460
        }
461
 
462
        // move tokenList to new set, excluding unnecessary white-space tokens and converting necessary ones to intersections
463
        $tokenCount = count($tokens1);
464
        for ($i = 0; $i < $tokenCount; ++$i) {
465
            $token = $tokens1[$i];
466
            if (isset($tokens1[$i - 1])) {
467
                $previousToken = $tokens1[$i - 1];
468
            } else {
469
                $previousToken = null;
470
            }
471
            if (isset($tokens1[$i + 1])) {
472
                $nextToken = $tokens1[$i + 1];
473
            } else {
474
                $nextToken = null;
475
            }
476
 
477
            if ($token->getTokenType() != FormulaToken::TOKEN_TYPE_WHITESPACE) {
478
                $tokens2[] = $token;
479
 
480
                continue;
481
            }
482
 
483
            if ($previousToken === null) {
484
                continue;
485
            }
486
 
487
            if (
488
                !(
489
                    (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP))
490
                || (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP))
491
                || ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
492
                )
493
            ) {
494
                continue;
495
            }
496
 
497
            if ($nextToken === null) {
498
                continue;
499
            }
500
 
501
            if (
502
                !(
503
                    (($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START))
504
                || (($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START))
505
                || ($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
506
                )
507
            ) {
508
                continue;
509
            }
510
 
511
            $tokens2[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_INTERSECTION);
512
        }
513
 
514
        // move tokens to final list, switching infix "-" operators to prefix when appropriate, switching infix "+" operators
515
        // to noop when appropriate, identifying operand and infix-operator subtypes, and pulling "@" from function names
516
        $this->tokens = [];
517
 
518
        $tokenCount = count($tokens2);
519
        for ($i = 0; $i < $tokenCount; ++$i) {
520
            $token = $tokens2[$i];
521
            if (isset($tokens2[$i - 1])) {
522
                $previousToken = $tokens2[$i - 1];
523
            } else {
524
                $previousToken = null;
525
            }
526
 
527
            if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '-') {
528
                if ($i == 0) {
529
                    $token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX);
530
                } elseif (
531
                    (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION)
532
                        && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP))
533
                    || (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION)
534
                        && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP))
535
                    || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX)
536
                    || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
537
                ) {
538
                    $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH);
539
                } else {
540
                    $token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX);
541
                }
542
 
543
                $this->tokens[] = $token;
544
 
545
                continue;
546
            }
547
 
548
            if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '+') {
549
                if ($i == 0) {
550
                    continue;
551
                } elseif (
552
                    (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION)
553
                        && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP))
554
                    || (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION)
555
                        && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP))
556
                    || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX)
557
                    || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
558
                ) {
559
                    $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH);
560
                } else {
561
                    continue;
562
                }
563
 
564
                $this->tokens[] = $token;
565
 
566
                continue;
567
            }
568
 
569
            if (
570
                $token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX
571
                && $token->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_NOTHING
572
            ) {
573
                if (str_contains('<>=', substr($token->getValue(), 0, 1))) {
574
                    $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_LOGICAL);
575
                } elseif ($token->getValue() == '&') {
576
                    $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_CONCATENATION);
577
                } else {
578
                    $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH);
579
                }
580
 
581
                $this->tokens[] = $token;
582
 
583
                continue;
584
            }
585
 
586
            if (
587
                $token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND
588
                && $token->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_NOTHING
589
            ) {
590
                if (!is_numeric($token->getValue())) {
591
                    if (strtoupper($token->getValue()) == 'TRUE' || strtoupper($token->getValue()) == 'FALSE') {
592
                        $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_LOGICAL);
593
                    } else {
594
                        $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_RANGE);
595
                    }
596
                } else {
597
                    $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_NUMBER);
598
                }
599
 
600
                $this->tokens[] = $token;
601
 
602
                continue;
603
            }
604
 
605
            if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) {
606
                if ($token->getValue() !== '') {
607
                    if (str_starts_with($token->getValue(), '@')) {
608
                        $token->setValue(substr($token->getValue(), 1));
609
                    }
610
                }
611
            }
612
 
613
            $this->tokens[] = $token;
614
        }
615
    }
616
}