MediaWiki REL1_34
ExprParser.php
Go to the documentation of this file.
1<?php
20
21use UtfNormal\Validator;
22
23// @codeCoverageIgnoreStart
24
25// Character classes
26define( 'EXPR_WHITE_CLASS', " \t\r\n" );
27define( 'EXPR_NUMBER_CLASS', '0123456789.' );
28
29// Token types
30define( 'EXPR_WHITE', 1 );
31define( 'EXPR_NUMBER', 2 );
32define( 'EXPR_NEGATIVE', 3 );
33define( 'EXPR_POSITIVE', 4 );
34define( 'EXPR_PLUS', 5 );
35define( 'EXPR_MINUS', 6 );
36define( 'EXPR_TIMES', 7 );
37define( 'EXPR_DIVIDE', 8 );
38define( 'EXPR_MOD', 9 );
39define( 'EXPR_OPEN', 10 );
40define( 'EXPR_CLOSE', 11 );
41define( 'EXPR_AND', 12 );
42define( 'EXPR_OR', 13 );
43define( 'EXPR_NOT', 14 );
44define( 'EXPR_EQUALITY', 15 );
45define( 'EXPR_LESS', 16 );
46define( 'EXPR_GREATER', 17 );
47define( 'EXPR_LESSEQ', 18 );
48define( 'EXPR_GREATEREQ', 19 );
49define( 'EXPR_NOTEQ', 20 );
50define( 'EXPR_ROUND', 21 );
51define( 'EXPR_EXPONENT', 22 );
52define( 'EXPR_SINE', 23 );
53define( 'EXPR_COSINE', 24 );
54define( 'EXPR_TANGENS', 25 );
55define( 'EXPR_ARCSINE', 26 );
56define( 'EXPR_ARCCOS', 27 );
57define( 'EXPR_ARCTAN', 28 );
58define( 'EXPR_EXP', 29 );
59define( 'EXPR_LN', 30 );
60define( 'EXPR_ABS', 31 );
61define( 'EXPR_FLOOR', 32 );
62define( 'EXPR_TRUNC', 33 );
63define( 'EXPR_CEIL', 34 );
64define( 'EXPR_POW', 35 );
65define( 'EXPR_PI', 36 );
66define( 'EXPR_FMOD', 37 );
67define( 'EXPR_SQRT', 38 );
68
69// @codeCoverageIgnoreEnd
70
72 public $maxStackSize = 100;
73
74 public $precedence = [
75 EXPR_NEGATIVE => 10,
76 EXPR_POSITIVE => 10,
77 EXPR_EXPONENT => 10,
78 EXPR_SINE => 9,
79 EXPR_COSINE => 9,
80 EXPR_TANGENS => 9,
81 EXPR_ARCSINE => 9,
82 EXPR_ARCCOS => 9,
83 EXPR_ARCTAN => 9,
84 EXPR_EXP => 9,
85 EXPR_LN => 9,
86 EXPR_ABS => 9,
87 EXPR_FLOOR => 9,
88 EXPR_TRUNC => 9,
89 EXPR_CEIL => 9,
90 EXPR_NOT => 9,
91 EXPR_SQRT => 9,
92 EXPR_POW => 8,
93 EXPR_TIMES => 7,
94 EXPR_DIVIDE => 7,
95 EXPR_MOD => 7,
96 EXPR_FMOD => 7,
97 EXPR_PLUS => 6,
98 EXPR_MINUS => 6,
99 EXPR_ROUND => 5,
100 EXPR_EQUALITY => 4,
101 EXPR_LESS => 4,
102 EXPR_GREATER => 4,
103 EXPR_LESSEQ => 4,
104 EXPR_GREATEREQ => 4,
105 EXPR_NOTEQ => 4,
106 EXPR_AND => 3,
107 EXPR_OR => 2,
108 EXPR_PI => 0,
109 EXPR_OPEN => -1,
110 EXPR_CLOSE => -1,
111 ];
112
113 public $names = [
114 EXPR_NEGATIVE => '-',
115 EXPR_POSITIVE => '+',
116 EXPR_NOT => 'not',
117 EXPR_TIMES => '*',
118 EXPR_DIVIDE => '/',
119 EXPR_MOD => 'mod',
120 EXPR_FMOD => 'fmod',
121 EXPR_PLUS => '+',
122 EXPR_MINUS => '-',
123 EXPR_ROUND => 'round',
124 EXPR_EQUALITY => '=',
125 EXPR_LESS => '<',
126 EXPR_GREATER => '>',
127 EXPR_LESSEQ => '<=',
128 EXPR_GREATEREQ => '>=',
129 EXPR_NOTEQ => '<>',
130 EXPR_AND => 'and',
131 EXPR_OR => 'or',
132 EXPR_EXPONENT => 'e',
133 EXPR_SINE => 'sin',
134 EXPR_COSINE => 'cos',
135 EXPR_TANGENS => 'tan',
136 EXPR_ARCSINE => 'asin',
137 EXPR_ARCCOS => 'acos',
138 EXPR_ARCTAN => 'atan',
139 EXPR_LN => 'ln',
140 EXPR_EXP => 'exp',
141 EXPR_ABS => 'abs',
142 EXPR_FLOOR => 'floor',
143 EXPR_TRUNC => 'trunc',
144 EXPR_CEIL => 'ceil',
145 EXPR_POW => '^',
146 EXPR_PI => 'pi',
147 EXPR_SQRT => 'sqrt',
148 ];
149
150 public $words = [
151 'mod' => EXPR_MOD,
152 'fmod' => EXPR_FMOD,
153 'and' => EXPR_AND,
154 'or' => EXPR_OR,
155 'not' => EXPR_NOT,
156 'round' => EXPR_ROUND,
157 'div' => EXPR_DIVIDE,
158 'e' => EXPR_EXPONENT,
159 'sin' => EXPR_SINE,
160 'cos' => EXPR_COSINE,
161 'tan' => EXPR_TANGENS,
162 'asin' => EXPR_ARCSINE,
163 'acos' => EXPR_ARCCOS,
164 'atan' => EXPR_ARCTAN,
165 'exp' => EXPR_EXP,
166 'ln' => EXPR_LN,
167 'abs' => EXPR_ABS,
168 'trunc' => EXPR_TRUNC,
169 'floor' => EXPR_FLOOR,
170 'ceil' => EXPR_CEIL,
171 'pi' => EXPR_PI,
172 'sqrt' => EXPR_SQRT,
173 ];
174
185 public function doExpression( $expr ) {
186 $operands = [];
187 $operators = [];
188
189 # Unescape inequality operators
190 $expr = strtr( $expr, [ '&lt;' => '<', '&gt;' => '>',
191 '&minus;' => '-', '−' => '-' ] );
192
193 $p = 0;
194 $end = strlen( $expr );
195 $expecting = 'expression';
196 $name = '';
197
198 while ( $p < $end ) {
199 if ( count( $operands ) > $this->maxStackSize || count( $operators ) > $this->maxStackSize ) {
200 throw new ExprError( 'stack_exhausted' );
201 }
202 $char = $expr[$p];
203 $char2 = substr( $expr, $p, 2 );
204
205 // Mega if-elseif-else construct
206 // Only binary operators fall through for processing at the bottom, the rest
207 // finish their processing and continue
208
209 // First the unlimited length classes
210
211 // @phan-suppress-next-line PhanParamSuspiciousOrder false positive
212 if ( false !== strpos( EXPR_WHITE_CLASS, $char ) ) {
213 // Whitespace
214 $p += strspn( $expr, EXPR_WHITE_CLASS, $p );
215 continue;
216 // @phan-suppress-next-line PhanParamSuspiciousOrder false positive
217 } elseif ( false !== strpos( EXPR_NUMBER_CLASS, $char ) ) {
218 // Number
219 if ( $expecting !== 'expression' ) {
220 throw new ExprError( 'unexpected_number' );
221 }
222
223 // Find the rest of it
224 $length = strspn( $expr, EXPR_NUMBER_CLASS, $p );
225 // Convert it to float, silently removing double decimal points
226 $operands[] = (float)substr( $expr, $p, $length );
227 $p += $length;
228 $expecting = 'operator';
229 continue;
230 } elseif ( ctype_alpha( $char ) ) {
231 // Word
232 // Find the rest of it
233 $remaining = substr( $expr, $p );
234 if ( !preg_match( '/^[A-Za-z]*/', $remaining, $matches ) ) {
235 // This should be unreachable
236 throw new ExprError( 'preg_match_failure' );
237 }
238 $word = strtolower( $matches[0] );
239 $p += strlen( $word );
240
241 // Interpret the word
242 if ( !isset( $this->words[$word] ) ) {
243 throw new ExprError( 'unrecognised_word', $word );
244 }
245 $op = $this->words[$word];
246 switch ( $op ) {
247 // constant
248 case EXPR_EXPONENT:
249 if ( $expecting !== 'expression' ) {
250 break;
251 }
252 $operands[] = exp( 1 );
253 $expecting = 'operator';
254 continue 2;
255 case EXPR_PI:
256 if ( $expecting !== 'expression' ) {
257 throw new ExprError( 'unexpected_number' );
258 }
259 $operands[] = pi();
260 $expecting = 'operator';
261 continue 2;
262 // Unary operator
263 case EXPR_NOT:
264 case EXPR_SINE:
265 case EXPR_COSINE:
266 case EXPR_TANGENS:
267 case EXPR_ARCSINE:
268 case EXPR_ARCCOS:
269 case EXPR_ARCTAN:
270 case EXPR_EXP:
271 case EXPR_LN:
272 case EXPR_ABS:
273 case EXPR_FLOOR:
274 case EXPR_TRUNC:
275 case EXPR_CEIL:
276 case EXPR_SQRT:
277 if ( $expecting !== 'expression' ) {
278 throw new ExprError( 'unexpected_operator', $word );
279 }
280 $operators[] = $op;
281 continue 2;
282 }
283 // Binary operator, fall through
284 $name = $word;
285 } elseif ( $char2 === '<=' ) {
286 $name = $char2;
287 $op = EXPR_LESSEQ;
288 $p += 2;
289 } elseif ( $char2 === '>=' ) {
290 $name = $char2;
291 $op = EXPR_GREATEREQ;
292 $p += 2;
293 } elseif ( $char2 === '<>' || $char2 === '!=' ) {
294 $name = $char2;
295 $op = EXPR_NOTEQ;
296 $p += 2;
297 } elseif ( $char === '+' ) {
298 ++$p;
299 if ( $expecting === 'expression' ) {
300 // Unary plus
301 $operators[] = EXPR_POSITIVE;
302 continue;
303 } else {
304 // Binary plus
305 $op = EXPR_PLUS;
306 }
307 } elseif ( $char === '-' ) {
308 ++$p;
309 if ( $expecting === 'expression' ) {
310 // Unary minus
311 $operators[] = EXPR_NEGATIVE;
312 continue;
313 } else {
314 // Binary minus
315 $op = EXPR_MINUS;
316 }
317 } elseif ( $char === '*' ) {
318 $name = $char;
319 $op = EXPR_TIMES;
320 ++$p;
321 } elseif ( $char === '/' ) {
322 $name = $char;
323 $op = EXPR_DIVIDE;
324 ++$p;
325 } elseif ( $char === '^' ) {
326 $name = $char;
327 $op = EXPR_POW;
328 ++$p;
329 } elseif ( $char === '(' ) {
330 if ( $expecting === 'operator' ) {
331 throw new ExprError( 'unexpected_operator', '(' );
332 }
333 $operators[] = EXPR_OPEN;
334 ++$p;
335 continue;
336 } elseif ( $char === ')' ) {
337 $lastOp = end( $operators );
338 while ( $lastOp && $lastOp != EXPR_OPEN ) {
339 $this->doOperation( $lastOp, $operands );
340 array_pop( $operators );
341 $lastOp = end( $operators );
342 }
343 if ( $lastOp ) {
344 array_pop( $operators );
345 } else {
346 throw new ExprError( 'unexpected_closing_bracket' );
347 }
348 $expecting = 'operator';
349 ++$p;
350 continue;
351 } elseif ( $char === '=' ) {
352 $name = $char;
353 $op = EXPR_EQUALITY;
354 ++$p;
355 } elseif ( $char === '<' ) {
356 $name = $char;
357 $op = EXPR_LESS;
358 ++$p;
359 } elseif ( $char === '>' ) {
360 $name = $char;
361 $op = EXPR_GREATER;
362 ++$p;
363 } else {
364 $utfExpr = Validator::cleanUp( substr( $expr, $p ) );
365 throw new ExprError( 'unrecognised_punctuation', mb_substr( $utfExpr, 0, 1 ) );
366 }
367
368 // Binary operator processing
369 if ( $expecting === 'expression' ) {
370 throw new ExprError( 'unexpected_operator', $name );
371 }
372
373 // Shunting yard magic
374 $lastOp = end( $operators );
375 while ( $lastOp && $this->precedence[$op] <= $this->precedence[$lastOp] ) {
376 $this->doOperation( $lastOp, $operands );
377 array_pop( $operators );
378 $lastOp = end( $operators );
379 }
380 $operators[] = $op;
381 $expecting = 'expression';
382 }
383
384 // Finish off the operator array
385 // @codingStandardsIgnoreStart
386 while ( $op = array_pop( $operators ) ) {
387 // @codingStandardsIgnoreEnd
388 if ( $op == EXPR_OPEN ) {
389 throw new ExprError( 'unclosed_bracket' );
390 }
391 $this->doOperation( $op, $operands );
392 }
393
394 return implode( "<br />\n", $operands );
395 }
396
402 public function doOperation( $op, &$stack ) {
403 switch ( $op ) {
404 case EXPR_NEGATIVE:
405 if ( count( $stack ) < 1 ) {
406 throw new ExprError( 'missing_operand', $this->names[$op] );
407 }
408 $arg = array_pop( $stack );
409 $stack[] = -$arg;
410 break;
411 case EXPR_POSITIVE:
412 if ( count( $stack ) < 1 ) {
413 throw new ExprError( 'missing_operand', $this->names[$op] );
414 }
415 break;
416 case EXPR_TIMES:
417 if ( count( $stack ) < 2 ) {
418 throw new ExprError( 'missing_operand', $this->names[$op] );
419 }
420 $right = array_pop( $stack );
421 $left = array_pop( $stack );
422 $stack[] = $left * $right;
423 break;
424 case EXPR_DIVIDE:
425 if ( count( $stack ) < 2 ) {
426 throw new ExprError( 'missing_operand', $this->names[$op] );
427 }
428 $right = array_pop( $stack );
429 $left = array_pop( $stack );
430 if ( !$right ) {
431 throw new ExprError( 'division_by_zero', $this->names[$op] );
432 }
433 $stack[] = $left / $right;
434 break;
435 case EXPR_MOD:
436 if ( count( $stack ) < 2 ) {
437 throw new ExprError( 'missing_operand', $this->names[$op] );
438 }
439 $right = (int)array_pop( $stack );
440 $left = (int)array_pop( $stack );
441 if ( !$right ) {
442 throw new ExprError( 'division_by_zero', $this->names[$op] );
443 }
444 $stack[] = $left % $right;
445 break;
446 case EXPR_FMOD:
447 if ( count( $stack ) < 2 ) {
448 throw new ExprError( 'missing_operand', $this->names[$op] );
449 }
450 $right = (double)array_pop( $stack );
451 $left = (double)array_pop( $stack );
452 if ( !$right ) {
453 throw new ExprError( 'division_by_zero', $this->names[$op] );
454 }
455 $stack[] = fmod( $left, $right );
456 break;
457 case EXPR_PLUS:
458 if ( count( $stack ) < 2 ) {
459 throw new ExprError( 'missing_operand', $this->names[$op] );
460 }
461 $right = array_pop( $stack );
462 $left = array_pop( $stack );
463 $stack[] = $left + $right;
464 break;
465 case EXPR_MINUS:
466 if ( count( $stack ) < 2 ) {
467 throw new ExprError( 'missing_operand', $this->names[$op] );
468 }
469 $right = array_pop( $stack );
470 $left = array_pop( $stack );
471 $stack[] = $left - $right;
472 break;
473 case EXPR_AND:
474 if ( count( $stack ) < 2 ) {
475 throw new ExprError( 'missing_operand', $this->names[$op] );
476 }
477 $right = array_pop( $stack );
478 $left = array_pop( $stack );
479 $stack[] = ( $left && $right ) ? 1 : 0;
480 break;
481 case EXPR_OR:
482 if ( count( $stack ) < 2 ) {
483 throw new ExprError( 'missing_operand', $this->names[$op] );
484 }
485 $right = array_pop( $stack );
486 $left = array_pop( $stack );
487 $stack[] = ( $left || $right ) ? 1 : 0;
488 break;
489 case EXPR_EQUALITY:
490 if ( count( $stack ) < 2 ) {
491 throw new ExprError( 'missing_operand', $this->names[$op] );
492 }
493 $right = array_pop( $stack );
494 $left = array_pop( $stack );
495 $stack[] = ( $left == $right ) ? 1 : 0;
496 break;
497 case EXPR_NOT:
498 if ( count( $stack ) < 1 ) {
499 throw new ExprError( 'missing_operand', $this->names[$op] );
500 }
501 $arg = array_pop( $stack );
502 $stack[] = ( !$arg ) ? 1 : 0;
503 break;
504 case EXPR_ROUND:
505 if ( count( $stack ) < 2 ) {
506 throw new ExprError( 'missing_operand', $this->names[$op] );
507 }
508 $digits = (int)array_pop( $stack );
509 $value = array_pop( $stack );
510 $stack[] = round( $value, $digits );
511 break;
512 case EXPR_LESS:
513 if ( count( $stack ) < 2 ) {
514 throw new ExprError( 'missing_operand', $this->names[$op] );
515 }
516 $right = array_pop( $stack );
517 $left = array_pop( $stack );
518 $stack[] = ( $left < $right ) ? 1 : 0;
519 break;
520 case EXPR_GREATER:
521 if ( count( $stack ) < 2 ) {
522 throw new ExprError( 'missing_operand', $this->names[$op] );
523 }
524 $right = array_pop( $stack );
525 $left = array_pop( $stack );
526 $stack[] = ( $left > $right ) ? 1 : 0;
527 break;
528 case EXPR_LESSEQ:
529 if ( count( $stack ) < 2 ) {
530 throw new ExprError( 'missing_operand', $this->names[$op] );
531 }
532 $right = array_pop( $stack );
533 $left = array_pop( $stack );
534 $stack[] = ( $left <= $right ) ? 1 : 0;
535 break;
536 case EXPR_GREATEREQ:
537 if ( count( $stack ) < 2 ) {
538 throw new ExprError( 'missing_operand', $this->names[$op] );
539 }
540 $right = array_pop( $stack );
541 $left = array_pop( $stack );
542 $stack[] = ( $left >= $right ) ? 1 : 0;
543 break;
544 case EXPR_NOTEQ:
545 if ( count( $stack ) < 2 ) {
546 throw new ExprError( 'missing_operand', $this->names[$op] );
547 }
548 $right = array_pop( $stack );
549 $left = array_pop( $stack );
550 $stack[] = ( $left != $right ) ? 1 : 0;
551 break;
552 case EXPR_EXPONENT:
553 if ( count( $stack ) < 2 ) {
554 throw new ExprError( 'missing_operand', $this->names[$op] );
555 }
556 $right = array_pop( $stack );
557 $left = array_pop( $stack );
558 $stack[] = $left * pow( 10, $right );
559 break;
560 case EXPR_SINE:
561 if ( count( $stack ) < 1 ) {
562 throw new ExprError( 'missing_operand', $this->names[$op] );
563 }
564 $arg = array_pop( $stack );
565 $stack[] = sin( $arg );
566 break;
567 case EXPR_COSINE:
568 if ( count( $stack ) < 1 ) {
569 throw new ExprError( 'missing_operand', $this->names[$op] );
570 }
571 $arg = array_pop( $stack );
572 $stack[] = cos( $arg );
573 break;
574 case EXPR_TANGENS:
575 if ( count( $stack ) < 1 ) {
576 throw new ExprError( 'missing_operand', $this->names[$op] );
577 }
578 $arg = array_pop( $stack );
579 $stack[] = tan( $arg );
580 break;
581 case EXPR_ARCSINE:
582 if ( count( $stack ) < 1 ) {
583 throw new ExprError( 'missing_operand', $this->names[$op] );
584 }
585 $arg = array_pop( $stack );
586 if ( $arg < -1 || $arg > 1 ) {
587 throw new ExprError( 'invalid_argument', $this->names[$op] );
588 }
589 $stack[] = asin( $arg );
590 break;
591 case EXPR_ARCCOS:
592 if ( count( $stack ) < 1 ) {
593 throw new ExprError( 'missing_operand', $this->names[$op] );
594 }
595 $arg = array_pop( $stack );
596 if ( $arg < -1 || $arg > 1 ) {
597 throw new ExprError( 'invalid_argument', $this->names[$op] );
598 }
599 $stack[] = acos( $arg );
600 break;
601 case EXPR_ARCTAN:
602 if ( count( $stack ) < 1 ) {
603 throw new ExprError( 'missing_operand', $this->names[$op] );
604 }
605 $arg = array_pop( $stack );
606 $stack[] = atan( $arg );
607 break;
608 case EXPR_EXP:
609 if ( count( $stack ) < 1 ) {
610 throw new ExprError( 'missing_operand', $this->names[$op] );
611 }
612 $arg = array_pop( $stack );
613 $stack[] = exp( $arg );
614 break;
615 case EXPR_LN:
616 if ( count( $stack ) < 1 ) {
617 throw new ExprError( 'missing_operand', $this->names[$op] );
618 }
619 $arg = array_pop( $stack );
620 if ( $arg <= 0 ) {
621 throw new ExprError( 'invalid_argument_ln', $this->names[$op] );
622 }
623 $stack[] = log( $arg );
624 break;
625 case EXPR_ABS:
626 if ( count( $stack ) < 1 ) {
627 throw new ExprError( 'missing_operand', $this->names[$op] );
628 }
629 $arg = array_pop( $stack );
630 $stack[] = abs( $arg );
631 break;
632 case EXPR_FLOOR:
633 if ( count( $stack ) < 1 ) {
634 throw new ExprError( 'missing_operand', $this->names[$op] );
635 }
636 $arg = array_pop( $stack );
637 $stack[] = floor( $arg );
638 break;
639 case EXPR_TRUNC:
640 if ( count( $stack ) < 1 ) {
641 throw new ExprError( 'missing_operand', $this->names[$op] );
642 }
643 $arg = array_pop( $stack );
644 $stack[] = (int)$arg;
645 break;
646 case EXPR_CEIL:
647 if ( count( $stack ) < 1 ) {
648 throw new ExprError( 'missing_operand', $this->names[$op] );
649 }
650 $arg = array_pop( $stack );
651 $stack[] = ceil( $arg );
652 break;
653 case EXPR_POW:
654 if ( count( $stack ) < 2 ) {
655 throw new ExprError( 'missing_operand', $this->names[$op] );
656 }
657 $right = array_pop( $stack );
658 $left = array_pop( $stack );
659 $result = pow( $left, $right );
660 if ( $result === false ) {
661 throw new ExprError( 'division_by_zero', $this->names[$op] );
662 }
663 $stack[] = $result;
664 break;
665 case EXPR_SQRT:
666 if ( count( $stack ) < 1 ) {
667 throw new ExprError( 'missing_operand', $this->names[$op] );
668 }
669 $arg = array_pop( $stack );
670 $result = sqrt( $arg );
671 if ( is_nan( $result ) ) {
672 throw new ExprError( 'not_a_number', $this->names[$op] );
673 }
674 $stack[] = $result;
675 break;
676 default:
677 // Should be impossible to reach here.
678 // @codeCoverageIgnoreStart
679 throw new ExprError( 'unknown_error' );
680 // @codeCoverageIgnoreEnd
681 }
682 }
683}
doExpression( $expr)
Evaluate a mathematical expression.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
Definition ExprError.php:19