message = '' . wfMsgForContent( "pfunc_expr_$msg", htmlspecialchars( $parameter ) ) . '';
}
}
class ExprParser {
var $maxStackSize = 100;
var $precedence = array(
EXPR_NEGATIVE => 10,
EXPR_POSITIVE => 10,
EXPR_EXPONENT => 10,
EXPR_SINE => 9,
EXPR_COSINE => 9,
EXPR_TANGENS => 9,
EXPR_ARCSINE => 9,
EXPR_ARCCOS => 9,
EXPR_ARCTAN => 9,
EXPR_EXP => 9,
EXPR_LN => 9,
EXPR_ABS => 9,
EXPR_FLOOR => 9,
EXPR_TRUNC => 9,
EXPR_CEIL => 9,
EXPR_NOT => 9,
EXPR_POW => 8,
EXPR_TIMES => 7,
EXPR_DIVIDE => 7,
EXPR_MOD => 7,
EXPR_PLUS => 6,
EXPR_MINUS => 6,
EXPR_ROUND => 5,
EXPR_EQUALITY => 4,
EXPR_LESS => 4,
EXPR_GREATER => 4,
EXPR_LESSEQ => 4,
EXPR_GREATEREQ => 4,
EXPR_NOTEQ => 4,
EXPR_AND => 3,
EXPR_OR => 2,
EXPR_PI => 0,
EXPR_OPEN => -1,
EXPR_CLOSE => -1,
);
var $names = array(
EXPR_NEGATIVE => '-',
EXPR_POSITIVE => '+',
EXPR_NOT => 'not',
EXPR_TIMES => '*',
EXPR_DIVIDE => '/',
EXPR_MOD => 'mod',
EXPR_PLUS => '+',
EXPR_MINUS => '-',
EXPR_ROUND => 'round',
EXPR_EQUALITY => '=',
EXPR_LESS => '<',
EXPR_GREATER => '>',
EXPR_LESSEQ => '<=',
EXPR_GREATEREQ => '>=',
EXPR_NOTEQ => '<>',
EXPR_AND => 'and',
EXPR_OR => 'or',
EXPR_EXPONENT => 'e',
EXPR_SINE => 'sin',
EXPR_COSINE => 'cos',
EXPR_TANGENS => 'tan',
EXPR_ARCSINE => 'asin',
EXPR_ARCCOS => 'acos',
EXPR_ARCTAN => 'atan',
EXPR_LN => 'ln',
EXPR_EXP => 'exp',
EXPR_ABS => 'abs',
EXPR_FLOOR => 'floor',
EXPR_TRUNC => 'trunc',
EXPR_CEIL => 'ceil',
EXPR_POW => '^',
EXPR_PI => 'pi',
);
var $words = array(
'mod' => EXPR_MOD,
'and' => EXPR_AND,
'or' => EXPR_OR,
'not' => EXPR_NOT,
'round' => EXPR_ROUND,
'div' => EXPR_DIVIDE,
'e' => EXPR_EXPONENT,
'sin' => EXPR_SINE,
'cos' => EXPR_COSINE,
'tan' => EXPR_TANGENS,
'asin' => EXPR_ARCSINE,
'acos' => EXPR_ARCCOS,
'atan' => EXPR_ARCTAN,
'exp' => EXPR_EXP,
'ln' => EXPR_LN,
'abs' => EXPR_ABS,
'trunc' => EXPR_TRUNC,
'floor' => EXPR_FLOOR,
'ceil' => EXPR_CEIL,
'pi' => EXPR_PI,
);
/**
* Tests whether the fractional difference between two numbers
* is within EXPR_TOLERANCE of each other.
*/
function toleranceComparison( $a, $b ) {
if( $b == 0 || $a == 0 ) {
if( $a == $b ) {
return 0;
} elseif( $a > $b ) {
return 1;
} else {
return -1;
}
}
$c = (( $a / $b ) - ( $b / $a )) / 2.0;
if( abs( $c ) < EXPR_TOLERANCE ) {
return 0;
} elseif( $c > 0 ) {
return 1;
} else {
return -1;
}
}
/**
* Checks if $expr is an integer within EXPR_TOLERANCE
* If so, recast as integer and return, else return $expr unchanged.
*/
function checkInteger( $expr ) {
$intval = round($expr);
if( toleranceComparison( $expr, $intval ) == 0 ) {
return $intval;
} else {
return $expr;
}
}
/**
* 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.
*/
function doExpression( $expr ) {
$operands = array();
$operators = array();
# Unescape inequality operators
$expr = strtr( $expr, array( '<' => '<', '>' => '>',
'−' => '-', '−' => '-' ) );
$p = 0;
$end = strlen( $expr );
$expecting = 'expression';
while ( $p < $end ) {
if ( count( $operands ) > $this->maxStackSize || count( $operators ) > $this->maxStackSize ) {
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
if ( false !== strpos( EXPR_WHITE_CLASS, $char ) ) {
// Whitespace
$p += strspn( $expr, EXPR_WHITE_CLASS, $p );
continue;
} elseif ( false !== strpos( EXPR_NUMBER_CLASS, $char ) ) {
// Number
if ( $expecting != 'expression' ) {
throw new ExprError('unexpected_number');
}
// Find the rest of it
$length = strspn( $expr, EXPR_NUMBER_CLASS, $p );
// Convert it to float, silently removing double decimal points
$operands[] = floatval( 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( $this->words[$word] ) ){
throw new ExprError('unrecognised_word', $word);
}
$op = $this->words[$word];
switch( $op ) {
// constant
case EXPR_EXPONENT:
if ( $expecting != 'expression' ) {
continue;
}
$operands[] = exp(1);
$expecting = 'operator';
continue 2;
case EXPR_PI:
if ( $expecting != 'expression' ) {
throw new ExprError( 'unexpected_number' );
}
$operands[] = pi();
$expecting = 'operator';
continue 2;
// Unary operator
case EXPR_NOT:
case EXPR_SINE:
case EXPR_COSINE:
case EXPR_TANGENS:
case EXPR_ARCSINE:
case EXPR_ARCCOS:
case EXPR_ARCTAN:
case EXPR_EXP:
case EXPR_LN:
case EXPR_ABS:
case EXPR_FLOOR:
case EXPR_TRUNC:
case EXPR_CEIL:
if ( $expecting != 'expression' ) {
throw new ExprError( 'unexpected_operator', $word );
}
$operators[] = $op;
continue 2;
}
// Binary operator, fall through
$name = $word;
}
// Next the two-character operators
elseif ( $char2 == '<=' ) {
$name = $char2;
$op = EXPR_LESSEQ;
$p += 2;
} elseif ( $char2 == '>=' ) {
$name = $char2;
$op = EXPR_GREATEREQ;
$p += 2;
} elseif ( $char2 == '<>' || $char2 == '!=' ) {
$name = $char2;
$op = EXPR_NOTEQ;
$p += 2;
}
// Finally the single-character operators
elseif ( $char == '+' ) {
++$p;
if ( $expecting == 'expression' ) {
// Unary plus
$operators[] = EXPR_POSITIVE;
continue;
} else {
// Binary plus
$op = EXPR_PLUS;
}
} elseif ( $char == '-' ) {
++$p;
if ( $expecting == 'expression' ) {
// Unary minus
$operators[] = EXPR_NEGATIVE;
continue;
} else {
// Binary minus
$op = EXPR_MINUS;
}
} elseif ( $char == '*' ) {
$name = $char;
$op = EXPR_TIMES;
++$p;
} elseif ( $char == '/' ) {
$name = $char;
$op = EXPR_DIVIDE;
++$p;
} elseif ( $char == '^' ) {
$name = $char;
$op = EXPR_POW;
++$p;
} elseif ( $char == '(' ) {
if ( $expecting == 'operator' ) {
throw new ExprError('unexpected_operator', '(');
}
$operators[] = EXPR_OPEN;
++$p;
continue;
} elseif ( $char == ')' ) {
$lastOp = end( $operators );
while ( $lastOp && $lastOp != 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 = EXPR_EQUALITY;
++$p;
} elseif ( $char == '<' ) {
$name = $char;
$op = EXPR_LESS;
++$p;
} elseif ( $char == '>' ) {
$name = $char;
$op = EXPR_GREATER;
++$p;
} else {
throw new ExprError('unrecognised_punctuation', UtfNormal::cleanUp( $char ));
}
// Binary operator processing
if ( $expecting == 'expression' ) {
throw new ExprError('unexpected_operator', $name);
}
// Shunting yard magic
$lastOp = end( $operators );
while ( $lastOp && $this->precedence[$op] <= $this->precedence[$lastOp] ) {
$this->doOperation( $lastOp, $operands );
array_pop( $operators );
$lastOp = end( $operators );
}
$operators[] = $op;
$expecting = 'expression';
}
// Finish off the operator array
while ( $op = array_pop( $operators ) ) {
if ( $op == EXPR_OPEN ) {
throw new ExprError('unclosed_bracket');
}
$this->doOperation( $op, $operands );
}
return implode( "
\n", $operands );
}
function doOperation( $op, &$stack ) {
switch ( $op ) {
case EXPR_NEGATIVE:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
$stack[] = -$arg;
break;
case EXPR_POSITIVE:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
break;
case EXPR_TIMES:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = $left * $right;
break;
case EXPR_DIVIDE:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
if ( $right == 0 ) throw new ExprError('division_by_zero', $this->names[$op]);
$stack[] = $left / $right;
break;
case EXPR_MOD:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
if ( $right == 0 ) throw new ExprError('division_by_zero', $this->names[$op]);
$stack[] = $this->checkInteger( $left ) % $this->checkInteger( $right );
break;
case EXPR_PLUS:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = $left + $right;
break;
case EXPR_MINUS:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = $left - $right;
break;
case EXPR_AND:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = ( $left && $right ) ? 1 : 0;
break;
case EXPR_OR:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = ( $left || $right ) ? 1 : 0;
break;
case EXPR_EQUALITY:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = ( $this->toleranceComparison( $left, $right ) == 0 ) ? 1 : 0;
break;
case EXPR_NOT:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
$stack[] = (!$arg) ? 1 : 0;
break;
case EXPR_ROUND:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$digits = intval( array_pop( $stack ) );
$value = array_pop( $stack );
$stack[] = round( $value, $digits );
break;
case EXPR_LESS:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = ( $this->toleranceComparison( $left, $right ) < 0 ) ? 1 : 0;
break;
case EXPR_GREATER:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = ( $this->toleranceComparison( $left, $right ) > 0 ) ? 1 : 0;
break;
case EXPR_LESSEQ:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = ( $this->toleranceComparison( $left, $right ) <= 0 ) ? 1 : 0;
break;
case EXPR_GREATEREQ:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = ( $this->toleranceComparison( $left, $right ) >= 0 ) ? 1 : 0;
break;
case EXPR_NOTEQ:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = ( $this->toleranceComparison( $left, $right ) != 0 ) ? 1 : 0;
break;
case EXPR_EXPONENT:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
$stack[] = $left * pow(10, $this->checkInteger( $right ) );
break;
case EXPR_SINE:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
$stack[] = sin($arg);
break;
case EXPR_COSINE:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
$stack[] = cos($arg);
break;
case EXPR_TANGENS:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
$stack[] = tan($arg);
break;
case EXPR_ARCSINE:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
if ( $arg < -1 || $arg > 1 ) throw new ExprError('invalid_argument', $this->names[$op] );
$stack[] = asin($arg);
break;
case EXPR_ARCCOS:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
if ( $arg < -1 || $arg > 1 ) throw new ExprError('invalid_argument', $this->names[$op] );
$stack[] = acos($arg);
break;
case EXPR_ARCTAN:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
$stack[] = atan($arg);
break;
case EXPR_EXP:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
$stack[] = exp($arg);
break;
case EXPR_LN:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
if ( $arg <= 0 ) throw new ExprError('invalid_argument_ln', $this->names[$op]);
$stack[] = log($arg);
break;
case EXPR_ABS:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
$stack[] = abs($arg);
break;
case EXPR_FLOOR:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
$stack[] = floor( $this->checkInteger( $arg ) );
break;
case EXPR_TRUNC:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
$stack[] = (int)( $this->checkInteger( $arg ) );
break;
case EXPR_CEIL:
if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]);
$arg = array_pop( $stack );
$stack[] = ceil( $this->checkInteger( $arg ) );
break;
case EXPR_POW:
if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]);
$right = array_pop( $stack );
$left = array_pop( $stack );
if ( false === ($stack[] = pow($left, $right)) ) throw new ExprError('division_by_zero', $this->names[$op]);
break;
default:
// Should be impossible to reach here.
throw new ExprError('unknown_error');
}
}
}