3 use UtfNormal\Validator;
5 if ( !defined( 'MEDIAWIKI' ) ) {
6 die( 'This file is a MediaWiki extension, it is not a valid entry point' );
10 define( 'EXPR_WHITE_CLASS', " \t\r\n" );
11 define( 'EXPR_NUMBER_CLASS', '0123456789.' );
14 define( 'EXPR_WHITE', 1 );
15 define( 'EXPR_NUMBER', 2 );
16 define( 'EXPR_NEGATIVE', 3 );
17 define( 'EXPR_POSITIVE', 4 );
18 define( 'EXPR_PLUS', 5 );
19 define( 'EXPR_MINUS', 6 );
20 define( 'EXPR_TIMES', 7 );
21 define( 'EXPR_DIVIDE', 8 );
22 define( 'EXPR_MOD', 9 );
23 define( 'EXPR_OPEN', 10 );
24 define( 'EXPR_CLOSE', 11 );
25 define( 'EXPR_AND', 12 );
26 define( 'EXPR_OR', 13 );
27 define( 'EXPR_NOT', 14 );
28 define( 'EXPR_EQUALITY', 15 );
29 define( 'EXPR_LESS', 16 );
30 define( 'EXPR_GREATER', 17 );
31 define( 'EXPR_LESSEQ', 18 );
32 define( 'EXPR_GREATEREQ', 19 );
33 define( 'EXPR_NOTEQ', 20 );
34 define( 'EXPR_ROUND', 21 );
35 define( 'EXPR_EXPONENT', 22 );
36 define( 'EXPR_SINE', 23 );
37 define( 'EXPR_COSINE', 24 );
38 define( 'EXPR_TANGENS', 25 );
39 define( 'EXPR_ARCSINE', 26 );
40 define( 'EXPR_ARCCOS', 27 );
41 define( 'EXPR_ARCTAN', 28 );
42 define( 'EXPR_EXP', 29 );
43 define( 'EXPR_LN', 30 );
44 define( 'EXPR_ABS', 31 );
45 define( 'EXPR_FLOOR', 32 );
46 define( 'EXPR_TRUNC', 33 );
47 define( 'EXPR_CEIL', 34 );
48 define( 'EXPR_POW', 35 );
49 define( 'EXPR_PI', 36 );
50 define( 'EXPR_FMOD', 37 );
51 define( 'EXPR_SQRT', 38 );
53 class ExprError extends Exception {
56 * @param $parameter string
58 public function __construct( $msg, $parameter = '' ) {
59 // Give grep a chance to find the usages:
60 // pfunc_expr_stack_exhausted, pfunc_expr_unexpected_number, pfunc_expr_preg_match_failure,
61 // pfunc_expr_unrecognised_word, pfunc_expr_unexpected_operator, pfunc_expr_missing_operand,
62 // pfunc_expr_unexpected_closing_bracket, pfunc_expr_unrecognised_punctuation,
63 // pfunc_expr_unclosed_bracket, pfunc_expr_division_by_zero, pfunc_expr_invalid_argument,
64 // pfunc_expr_invalid_argument_ln, pfunc_expr_unknown_error, pfunc_expr_not_a_number
65 $this->message = wfMessage( "pfunc_expr_$msg", $parameter )->inContentLanguage()->text();
70 public $maxStackSize = 100;
72 public $precedence = [
112 EXPR_NEGATIVE => '-',
113 EXPR_POSITIVE => '+',
121 EXPR_ROUND => 'round',
122 EXPR_EQUALITY => '=',
126 EXPR_GREATEREQ => '>=',
130 EXPR_EXPONENT => 'e',
132 EXPR_COSINE => 'cos',
133 EXPR_TANGENS => 'tan',
134 EXPR_ARCSINE => 'asin',
135 EXPR_ARCCOS => 'acos',
136 EXPR_ARCTAN => 'atan',
140 EXPR_FLOOR => 'floor',
141 EXPR_TRUNC => 'trunc',
154 'round' => EXPR_ROUND,
155 'div' => EXPR_DIVIDE,
156 'e' => EXPR_EXPONENT,
158 'cos' => EXPR_COSINE,
159 'tan' => EXPR_TANGENS,
160 'asin' => EXPR_ARCSINE,
161 'acos' => EXPR_ARCCOS,
162 'atan' => EXPR_ARCTAN,
166 'trunc' => EXPR_TRUNC,
167 'floor' => EXPR_FLOOR,
174 * Evaluate a mathematical expression
176 * The algorithm here is based on the infix to RPN algorithm given in
177 * http://montcs.bloomu.edu/~bobmon/Information/RPN/infix2rpn.shtml
178 * It's essentially the same as Dijkstra's shunting yard algorithm.
179 * @param $expr string
183 public function doExpression( $expr ) {
187 # Unescape inequality operators
188 $expr = strtr( $expr, [ '<' => '<', '>' => '>',
189 '−' => '-', '−' => '-' ] );
192 $end = strlen( $expr );
193 $expecting = 'expression';
196 while ( $p < $end ) {
197 if ( count( $operands ) > $this->maxStackSize || count( $operators ) > $this->maxStackSize ) {
198 throw new ExprError( 'stack_exhausted' );
201 $char2 = substr( $expr, $p, 2 );
203 // Mega if-elseif-else construct
204 // Only binary operators fall through for processing at the bottom, the rest
205 // finish their processing and continue
207 // First the unlimited length classes
209 if ( false !== strpos( EXPR_WHITE_CLASS, $char ) ) {
211 $p += strspn( $expr, EXPR_WHITE_CLASS, $p );
213 } elseif ( false !== strpos( EXPR_NUMBER_CLASS, $char ) ) {
215 if ( $expecting !== 'expression' ) {
216 throw new ExprError( 'unexpected_number' );
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 );
224 $expecting = 'operator';
226 } elseif ( ctype_alpha( $char ) ) {
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' );
234 $word = strtolower( $matches[0] );
235 $p += strlen( $word );
237 // Interpret the word
238 if ( !isset( $this->words[$word] ) ) {
239 throw new ExprError( 'unrecognised_word', $word );
241 $op = $this->words[$word];
245 if ( $expecting !== 'expression' ) {
248 $operands[] = exp( 1 );
249 $expecting = 'operator';
252 if ( $expecting !== 'expression' ) {
253 throw new ExprError( 'unexpected_number' );
256 $expecting = 'operator';
273 if ( $expecting !== 'expression' ) {
274 throw new ExprError( 'unexpected_operator', $word );
279 // Binary operator, fall through
283 // Next the two-character operators
284 elseif ( $char2 === '<=' ) {
288 } elseif ( $char2 === '>=' ) {
290 $op = EXPR_GREATEREQ;
292 } elseif ( $char2 === '<>' || $char2 === '!=' ) {
298 // Finally the single-character operators
299 elseif ( $char === '+' ) {
301 if ( $expecting === 'expression' ) {
303 $operators[] = EXPR_POSITIVE;
309 } elseif ( $char === '-' ) {
311 if ( $expecting === 'expression' ) {
313 $operators[] = EXPR_NEGATIVE;
319 } elseif ( $char === '*' ) {
323 } elseif ( $char === '/' ) {
327 } elseif ( $char === '^' ) {
331 } elseif ( $char === '(' ) {
332 if ( $expecting === 'operator' ) {
333 throw new ExprError( 'unexpected_operator', '(' );
335 $operators[] = EXPR_OPEN;
338 } elseif ( $char === ')' ) {
339 $lastOp = end( $operators );
340 while ( $lastOp && $lastOp != EXPR_OPEN ) {
341 $this->doOperation( $lastOp, $operands );
342 array_pop( $operators );
343 $lastOp = end( $operators );
346 array_pop( $operators );
348 throw new ExprError( 'unexpected_closing_bracket' );
350 $expecting = 'operator';
353 } elseif ( $char === '=' ) {
357 } elseif ( $char === '<' ) {
361 } elseif ( $char === '>' ) {
366 throw new ExprError( 'unrecognised_punctuation', Validator::cleanUp( $char ) );
369 // Binary operator processing
370 if ( $expecting === 'expression' ) {
371 throw new ExprError( 'unexpected_operator', $name );
374 // Shunting yard magic
375 $lastOp = end( $operators );
376 while ( $lastOp && $this->precedence[$op] <= $this->precedence[$lastOp] ) {
377 $this->doOperation( $lastOp, $operands );
378 array_pop( $operators );
379 $lastOp = end( $operators );
382 $expecting = 'expression';
385 // Finish off the operator array
386 // @codingStandardsIgnoreStart
387 while ( $op = array_pop( $operators ) ) {
388 // @codingStandardsIgnoreEnd
389 if ( $op == EXPR_OPEN ) {
390 throw new ExprError( 'unclosed_bracket' );
392 $this->doOperation( $op, $operands );
395 return implode( "<br />\n", $operands );
400 * @param $stack array
403 public function doOperation( $op, &$stack ) {
406 if ( count( $stack ) < 1 ) {
407 throw new ExprError( 'missing_operand', $this->names[$op] );
409 $arg = array_pop( $stack );
413 if ( count( $stack ) < 1 ) {
414 throw new ExprError( 'missing_operand', $this->names[$op] );
418 if ( count( $stack ) < 2 ) {
419 throw new ExprError( 'missing_operand', $this->names[$op] );
421 $right = array_pop( $stack );
422 $left = array_pop( $stack );
423 $stack[] = $left * $right;
426 if ( count( $stack ) < 2 ) {
427 throw new ExprError( 'missing_operand', $this->names[$op] );
429 $right = array_pop( $stack );
430 $left = array_pop( $stack );
432 throw new ExprError( 'division_by_zero', $this->names[$op] );
434 $stack[] = $left / $right;
437 if ( count( $stack ) < 2 ) {
438 throw new ExprError( 'missing_operand', $this->names[$op] );
440 $right = (int)array_pop( $stack );
441 $left = (int)array_pop( $stack );
443 throw new ExprError( 'division_by_zero', $this->names[$op] );
445 $stack[] = $left % $right;
448 if ( count( $stack ) < 2 ) {
449 throw new ExprError( 'missing_operand', $this->names[$op] );
451 $right = (double)array_pop( $stack );
452 $left = (double)array_pop( $stack );
454 throw new ExprError( 'division_by_zero', $this->names[$op] );
456 $stack[] = fmod( $left, $right );
459 if ( count( $stack ) < 2 ) {
460 throw new ExprError( 'missing_operand', $this->names[$op] );
462 $right = array_pop( $stack );
463 $left = array_pop( $stack );
464 $stack[] = $left + $right;
467 if ( count( $stack ) < 2 ) {
468 throw new ExprError( 'missing_operand', $this->names[$op] );
470 $right = array_pop( $stack );
471 $left = array_pop( $stack );
472 $stack[] = $left - $right;
475 if ( count( $stack ) < 2 ) {
476 throw new ExprError( 'missing_operand', $this->names[$op] );
478 $right = array_pop( $stack );
479 $left = array_pop( $stack );
480 $stack[] = ( $left && $right ) ? 1 : 0;
483 if ( count( $stack ) < 2 ) {
484 throw new ExprError( 'missing_operand', $this->names[$op] );
486 $right = array_pop( $stack );
487 $left = array_pop( $stack );
488 $stack[] = ( $left || $right ) ? 1 : 0;
491 if ( count( $stack ) < 2 ) {
492 throw new ExprError( 'missing_operand', $this->names[$op] );
494 $right = array_pop( $stack );
495 $left = array_pop( $stack );
496 $stack[] = ( $left == $right ) ? 1 : 0;
499 if ( count( $stack ) < 1 ) {
500 throw new ExprError( 'missing_operand', $this->names[$op] );
502 $arg = array_pop( $stack );
503 $stack[] = ( !$arg ) ? 1 : 0;
506 if ( count( $stack ) < 2 ) {
507 throw new ExprError( 'missing_operand', $this->names[$op] );
509 $digits = (int)array_pop( $stack );
510 $value = array_pop( $stack );
511 $stack[] = round( $value, $digits );
514 if ( count( $stack ) < 2 ) {
515 throw new ExprError( 'missing_operand', $this->names[$op] );
517 $right = array_pop( $stack );
518 $left = array_pop( $stack );
519 $stack[] = ( $left < $right ) ? 1 : 0;
522 if ( count( $stack ) < 2 ) {
523 throw new ExprError( 'missing_operand', $this->names[$op] );
525 $right = array_pop( $stack );
526 $left = array_pop( $stack );
527 $stack[] = ( $left > $right ) ? 1 : 0;
530 if ( count( $stack ) < 2 ) {
531 throw new ExprError( 'missing_operand', $this->names[$op] );
533 $right = array_pop( $stack );
534 $left = array_pop( $stack );
535 $stack[] = ( $left <= $right ) ? 1 : 0;
538 if ( count( $stack ) < 2 ) {
539 throw new ExprError( 'missing_operand', $this->names[$op] );
541 $right = array_pop( $stack );
542 $left = array_pop( $stack );
543 $stack[] = ( $left >= $right ) ? 1 : 0;
546 if ( count( $stack ) < 2 ) {
547 throw new ExprError( 'missing_operand', $this->names[$op] );
549 $right = array_pop( $stack );
550 $left = array_pop( $stack );
551 $stack[] = ( $left != $right ) ? 1 : 0;
554 if ( count( $stack ) < 2 ) {
555 throw new ExprError( 'missing_operand', $this->names[$op] );
557 $right = array_pop( $stack );
558 $left = array_pop( $stack );
559 $stack[] = $left * pow( 10, $right );
562 if ( count( $stack ) < 1 ) {
563 throw new ExprError( 'missing_operand', $this->names[$op] );
565 $arg = array_pop( $stack );
566 $stack[] = sin( $arg );
569 if ( count( $stack ) < 1 ) {
570 throw new ExprError( 'missing_operand', $this->names[$op] );
572 $arg = array_pop( $stack );
573 $stack[] = cos( $arg );
576 if ( count( $stack ) < 1 ) {
577 throw new ExprError( 'missing_operand', $this->names[$op] );
579 $arg = array_pop( $stack );
580 $stack[] = tan( $arg );
583 if ( count( $stack ) < 1 ) {
584 throw new ExprError( 'missing_operand', $this->names[$op] );
586 $arg = array_pop( $stack );
587 if ( $arg < -1 || $arg > 1 ) {
588 throw new ExprError( 'invalid_argument', $this->names[$op] );
590 $stack[] = asin( $arg );
593 if ( count( $stack ) < 1 ) {
594 throw new ExprError( 'missing_operand', $this->names[$op] );
596 $arg = array_pop( $stack );
597 if ( $arg < -1 || $arg > 1 ) {
598 throw new ExprError( 'invalid_argument', $this->names[$op] );
600 $stack[] = acos( $arg );
603 if ( count( $stack ) < 1 ) {
604 throw new ExprError( 'missing_operand', $this->names[$op] );
606 $arg = array_pop( $stack );
607 $stack[] = atan( $arg );
610 if ( count( $stack ) < 1 ) {
611 throw new ExprError( 'missing_operand', $this->names[$op] );
613 $arg = array_pop( $stack );
614 $stack[] = exp( $arg );
617 if ( count( $stack ) < 1 ) {
618 throw new ExprError( 'missing_operand', $this->names[$op] );
620 $arg = array_pop( $stack );
622 throw new ExprError( 'invalid_argument_ln', $this->names[$op] );
624 $stack[] = log( $arg );
627 if ( count( $stack ) < 1 ) {
628 throw new ExprError( 'missing_operand', $this->names[$op] );
630 $arg = array_pop( $stack );
631 $stack[] = abs( $arg );
634 if ( count( $stack ) < 1 ) {
635 throw new ExprError( 'missing_operand', $this->names[$op] );
637 $arg = array_pop( $stack );
638 $stack[] = floor( $arg );
641 if ( count( $stack ) < 1 ) {
642 throw new ExprError( 'missing_operand', $this->names[$op] );
644 $arg = array_pop( $stack );
645 $stack[] = (int)$arg;
648 if ( count( $stack ) < 1 ) {
649 throw new ExprError( 'missing_operand', $this->names[$op] );
651 $arg = array_pop( $stack );
652 $stack[] = ceil( $arg );
655 if ( count( $stack ) < 2 ) {
656 throw new ExprError( 'missing_operand', $this->names[$op] );
658 $right = array_pop( $stack );
659 $left = array_pop( $stack );
660 $result = pow( $left, $right );
661 if ( $result === false ) {
662 throw new ExprError( 'division_by_zero', $this->names[$op] );
667 if ( count( $stack ) < 1 ) {
668 throw new ExprError( 'missing_operand', $this->names[$op] );
670 $arg = array_pop( $stack );
671 $result = sqrt( $arg );
672 if ( is_nan( $result ) ) {
673 throw new ExprError( 'not_a_number', $this->names[$op] );
678 // Should be impossible to reach here.
679 throw new ExprError( 'unknown_error' );