/** mediawiki-1.37.0\extensions\ParserFunctions\includes\ParserFunctions.php
* {{#expr: expression }}
*
* @link https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##expr
*
* @param Parser $parser
* @param string $expr
* @return string
*/
public static function expr( Parser $parser, $expr = '' ) {
try {
return self::getExprParser()->doExpression( $expr );
} catch ( ExprError $e ) {
return '<strong class="error">' . htmlspecialchars( $e->getUserFriendlyMessage() ) . '</strong>';
}
}
/** mediawiki-1.37.0\extensions\ParserFunctions\includes\ExprParser.php
* Evaluate a mathematical expression
*
* The algorithm here is based on the infix to RPN algorithm given in
* http://montcs.bloomu.edu/~bobmon/Information/RPN/infix2rpn.shtml
* It's essentially the same as Dijkstra's shunting yard algorithm.
* @param string $expr
* @return string
* @throws ExprError
*/
public function doExpression( $expr ) {
$operands = [];
$operators = [];
# Unescape inequality operators
$expr = strtr( $expr, [ '<' => '<', '>' => '>',
'−' => '-', '−' => '-' ] );
$p = 0;
$end = strlen( $expr );
$expecting = 'expression';
$name = '';
while ( $p < $end ) {
if ( count( $operands ) > self::MAX_STACK_SIZE || count( $operators ) > self::MAX_STACK_SIZE ) {
throw new ExprError( 'stack_exhausted' );
}
$char = $expr[$p];
$char2 = substr( $expr, $p, 2 );
// Mega if-elseif-else construct
// Only binary operators fall through for processing at the bottom, the rest
// finish their processing and continue
// First the unlimited length classes
// @phan-suppress-next-line PhanParamSuspiciousOrder false positive
if ( strpos( self::EXPR_WHITE_CLASS, $char ) !== false ) {
// Whitespace
$p += strspn( $expr, self::EXPR_WHITE_CLASS, $p );
continue;
// @phan-suppress-next-line PhanParamSuspiciousOrder false positive
} elseif ( strpos( self::EXPR_NUMBER_CLASS, $char ) !== false ) {
// Number
if ( $expecting !== 'expression' ) {
throw new ExprError( 'unexpected_number' );
}
// Find the rest of it
$length = strspn( $expr, self::EXPR_NUMBER_CLASS, $p );
// Convert it to float, silently removing double decimal points
$operands[] = (float)substr( $expr, $p, $length );
$p += $length;
$expecting = 'operator';
continue;
} elseif ( ctype_alpha( $char ) ) {
// Word
// Find the rest of it
$remaining = substr( $expr, $p );
if ( !preg_match( '/^[A-Za-z]*/', $remaining, $matches ) ) {
// This should be unreachable
throw new ExprError( 'preg_match_failure' );
}
$word = strtolower( $matches[0] );
$p += strlen( $word );
// Interpret the word
if ( !isset( self::WORDS[$word] ) ) {
throw new ExprError( 'unrecognised_word', $word );
}
$op = self::WORDS[$word];
switch ( $op ) {
// constant
case self::EXPR_EXPONENT:
if ( $expecting !== 'expression' ) {
break;
}
$operands[] = exp( 1 );
$expecting = 'operator';
continue 2;
case self::EXPR_PI:
if ( $expecting !== 'expression' ) {
throw new ExprError( 'unexpected_number' );
}
$operands[] = pi();
$expecting = 'operator';
continue 2;
// Unary operator
case self::EXPR_NOT:
case self::EXPR_SINE:
case self::EXPR_COSINE:
case self::EXPR_TANGENS:
case self::EXPR_ARCSINE:
case self::EXPR_ARCCOS:
case self::EXPR_ARCTAN:
case self::EXPR_EXP:
case self::EXPR_LN:
case self::EXPR_ABS:
case self::EXPR_FLOOR:
case self::EXPR_TRUNC:
case self::EXPR_CEIL:
case self::EXPR_SQRT:
if ( $expecting !== 'expression' ) {
throw new ExprError( 'unexpected_operator', $word );
}
$operators[] = $op;
continue 2;
}
// Binary operator, fall through
$name = $word;
} elseif ( $char2 === '<=' ) {
$name = $char2;
$op = self::EXPR_LESSEQ;
$p += 2;
} elseif ( $char2 === '>=' ) {
$name = $char2;
$op = self::EXPR_GREATEREQ;
$p += 2;
} elseif ( $char2 === '<>' || $char2 === '!=' ) {
$name = $char2;
$op = self::EXPR_NOTEQ;
$p += 2;
} elseif ( $char === '+' ) {
++$p;
if ( $expecting === 'expression' ) {
// Unary plus
$operators[] = self::EXPR_POSITIVE;
continue;
} else {
// Binary plus
$op = self::EXPR_PLUS;
}
} elseif ( $char === '-' ) {
++$p;
if ( $expecting === 'expression' ) {
// Unary minus
$operators[] = self::EXPR_NEGATIVE;
continue;
} else {
// Binary minus
$op = self::EXPR_MINUS;
}
} elseif ( $char === '*' ) {
$name = $char;
$op = self::EXPR_TIMES;
++$p;
} elseif ( $char === '/' ) {
$name = $char;
$op = self::EXPR_DIVIDE;
++$p;
} elseif ( $char === '^' ) {
$name = $char;
$op = self::EXPR_POW;
++$p;
} elseif ( $char === '(' ) {
if ( $expecting === 'operator' ) {
throw new ExprError( 'unexpected_operator', '(' );
}
$operators[] = self::EXPR_OPEN;
++$p;
continue;
} elseif ( $char === ')' ) {
$lastOp = end( $operators );
while ( $lastOp && $lastOp !== self::EXPR_OPEN ) {
$this->doOperation( $lastOp, $operands );
array_pop( $operators );
$lastOp = end( $operators );
}
if ( $lastOp ) {
array_pop( $operators );
} else {
throw new ExprError( 'unexpected_closing_bracket' );
}
$expecting = 'operator';
++$p;
continue;
} elseif ( $char === '=' ) {
$name = $char;
$op = self::EXPR_EQUALITY;
++$p;
} elseif ( $char === '<' ) {
$name = $char;
$op = self::EXPR_LESS;
++$p;
} elseif ( $char === '>' ) {
$name = $char;
$op = self::EXPR_GREATER;
++$p;
} else {
$utfExpr = Validator::cleanUp( substr( $expr, $p ) );
throw new ExprError( 'unrecognised_punctuation', mb_substr( $utfExpr, 0, 1 ) );
}
// Binary operator processing
if ( $expecting === 'expression' ) {
throw new ExprError( 'unexpected_operator', $name );
}
// Shunting yard magic
$lastOp = end( $operators );
while ( $lastOp && self::PRECEDENCE[$op] <= self::PRECEDENCE[$lastOp] ) {
$this->doOperation( $lastOp, $operands );
array_pop( $operators );
$lastOp = end( $operators );
}
$operators[] = $op;
$expecting = 'expression';
}
// Finish off the operator array
// phpcs:ignore MediaWiki.ControlStructures.AssignmentInControlStructures.AssignmentInControlStructures
while ( $op = array_pop( $operators ) ) {
if ( $op === self::EXPR_OPEN ) {
throw new ExprError( 'unclosed_bracket' );
}
$this->doOperation( $op, $operands );
}
return implode( "<br />\n", $operands );
}