10, self::EXPR_POSITIVE => 10, self::EXPR_EXPONENT => 10, self::EXPR_SINE => 9, self::EXPR_COSINE => 9, self::EXPR_TANGENS => 9, self::EXPR_ARCSINE => 9, self::EXPR_ARCCOS => 9, self::EXPR_ARCTAN => 9, self::EXPR_EXP => 9, self::EXPR_LN => 9, self::EXPR_ABS => 9, self::EXPR_FLOOR => 9, self::EXPR_TRUNC => 9, self::EXPR_CEIL => 9, self::EXPR_NOT => 9, self::EXPR_SQRT => 9, self::EXPR_POW => 8, self::EXPR_TIMES => 7, self::EXPR_DIVIDE => 7, self::EXPR_MOD => 7, self::EXPR_FMOD => 7, self::EXPR_PLUS => 6, self::EXPR_MINUS => 6, self::EXPR_ROUND => 5, self::EXPR_EQUALITY => 4, self::EXPR_LESS => 4, self::EXPR_GREATER => 4, self::EXPR_LESSEQ => 4, self::EXPR_GREATEREQ => 4, self::EXPR_NOTEQ => 4, self::EXPR_AND => 3, self::EXPR_OR => 2, self::EXPR_PI => 0, self::EXPR_OPEN => -1, self::EXPR_CLOSE => -1, ]; private const NAMES = [ self::EXPR_NEGATIVE => '-', self::EXPR_POSITIVE => '+', self::EXPR_NOT => 'not', self::EXPR_TIMES => '*', self::EXPR_DIVIDE => '/', self::EXPR_MOD => 'mod', self::EXPR_FMOD => 'fmod', self::EXPR_PLUS => '+', self::EXPR_MINUS => '-', self::EXPR_ROUND => 'round', self::EXPR_EQUALITY => '=', self::EXPR_LESS => '<', self::EXPR_GREATER => '>', self::EXPR_LESSEQ => '<=', self::EXPR_GREATEREQ => '>=', self::EXPR_NOTEQ => '<>', self::EXPR_AND => 'and', self::EXPR_OR => 'or', self::EXPR_EXPONENT => 'e', self::EXPR_SINE => 'sin', self::EXPR_COSINE => 'cos', self::EXPR_TANGENS => 'tan', self::EXPR_ARCSINE => 'asin', self::EXPR_ARCCOS => 'acos', self::EXPR_ARCTAN => 'atan', self::EXPR_LN => 'ln', self::EXPR_EXP => 'exp', self::EXPR_ABS => 'abs', self::EXPR_FLOOR => 'floor', self::EXPR_TRUNC => 'trunc', self::EXPR_CEIL => 'ceil', self::EXPR_POW => '^', self::EXPR_PI => 'pi', self::EXPR_SQRT => 'sqrt', ]; private const WORDS = [ 'mod' => self::EXPR_MOD, 'fmod' => self::EXPR_FMOD, 'and' => self::EXPR_AND, 'or' => self::EXPR_OR, 'not' => self::EXPR_NOT, 'round' => self::EXPR_ROUND, 'div' => self::EXPR_DIVIDE, 'e' => self::EXPR_EXPONENT, 'sin' => self::EXPR_SINE, 'cos' => self::EXPR_COSINE, 'tan' => self::EXPR_TANGENS, 'asin' => self::EXPR_ARCSINE, 'acos' => self::EXPR_ARCCOS, 'atan' => self::EXPR_ARCTAN, 'exp' => self::EXPR_EXP, 'ln' => self::EXPR_LN, 'abs' => self::EXPR_ABS, 'trunc' => self::EXPR_TRUNC, 'floor' => self::EXPR_FLOOR, 'ceil' => self::EXPR_CEIL, 'pi' => self::EXPR_PI, 'sqrt' => self::EXPR_SQRT, ]; /** * 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( "
\n", $operands ); } /** * @param int $op * @param array &$stack * @throws ExprError */ public function doOperation( $op, &$stack ) { switch ( $op ) { case self::EXPR_NEGATIVE: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $stack[] = -$arg; break; case self::EXPR_POSITIVE: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } break; case self::EXPR_TIMES: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = $left * $right; break; case self::EXPR_DIVIDE: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); if ( !$right ) { throw new ExprError( 'division_by_zero', self::NAMES[$op] ); } $stack[] = $left / $right; break; case self::EXPR_MOD: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = (int)array_pop( $stack ); $left = (int)array_pop( $stack ); if ( !$right ) { throw new ExprError( 'division_by_zero', self::NAMES[$op] ); } $stack[] = $left % $right; break; case self::EXPR_FMOD: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = (double)array_pop( $stack ); $left = (double)array_pop( $stack ); if ( !$right ) { throw new ExprError( 'division_by_zero', self::NAMES[$op] ); } $stack[] = fmod( $left, $right ); break; case self::EXPR_PLUS: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = $left + $right; break; case self::EXPR_MINUS: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = $left - $right; break; case self::EXPR_AND: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = ( $left && $right ) ? 1 : 0; break; case self::EXPR_OR: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = ( $left || $right ) ? 1 : 0; break; case self::EXPR_EQUALITY: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = ( $left == $right ) ? 1 : 0; break; case self::EXPR_NOT: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $stack[] = ( !$arg ) ? 1 : 0; break; case self::EXPR_ROUND: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $digits = (int)array_pop( $stack ); $value = array_pop( $stack ); $stack[] = round( $value, $digits ); break; case self::EXPR_LESS: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = ( $left < $right ) ? 1 : 0; break; case self::EXPR_GREATER: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = ( $left > $right ) ? 1 : 0; break; case self::EXPR_LESSEQ: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = ( $left <= $right ) ? 1 : 0; break; case self::EXPR_GREATEREQ: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = ( $left >= $right ) ? 1 : 0; break; case self::EXPR_NOTEQ: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = ( $left != $right ) ? 1 : 0; break; case self::EXPR_EXPONENT: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $stack[] = $left * pow( 10, $right ); break; case self::EXPR_SINE: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $stack[] = sin( $arg ); break; case self::EXPR_COSINE: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $stack[] = cos( $arg ); break; case self::EXPR_TANGENS: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $stack[] = tan( $arg ); break; case self::EXPR_ARCSINE: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); if ( $arg < -1 || $arg > 1 ) { throw new ExprError( 'invalid_argument', self::NAMES[$op] ); } $stack[] = asin( $arg ); break; case self::EXPR_ARCCOS: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); if ( $arg < -1 || $arg > 1 ) { throw new ExprError( 'invalid_argument', self::NAMES[$op] ); } $stack[] = acos( $arg ); break; case self::EXPR_ARCTAN: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $stack[] = atan( $arg ); break; case self::EXPR_EXP: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $stack[] = exp( $arg ); break; case self::EXPR_LN: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); if ( $arg <= 0 ) { throw new ExprError( 'invalid_argument_ln', self::NAMES[$op] ); } $stack[] = log( $arg ); break; case self::EXPR_ABS: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $stack[] = abs( $arg ); break; case self::EXPR_FLOOR: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $stack[] = floor( $arg ); break; case self::EXPR_TRUNC: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $stack[] = (int)$arg; break; case self::EXPR_CEIL: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $stack[] = ceil( $arg ); break; case self::EXPR_POW: if ( count( $stack ) < 2 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $right = array_pop( $stack ); $left = array_pop( $stack ); $result = pow( $left, $right ); if ( $result === false ) { throw new ExprError( 'division_by_zero', self::NAMES[$op] ); } $stack[] = $result; break; case self::EXPR_SQRT: if ( count( $stack ) < 1 ) { throw new ExprError( 'missing_operand', self::NAMES[$op] ); } $arg = array_pop( $stack ); $result = sqrt( $arg ); if ( is_nan( $result ) ) { throw new ExprError( 'not_a_number', self::NAMES[$op] ); } $stack[] = $result; break; default: // Should be impossible to reach here. // @codeCoverageIgnoreStart throw new ExprError( 'unknown_error' ); // @codeCoverageIgnoreEnd } } }