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