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