<?php if ( !defined( 'MEDIAWIKI' ) ) { die( 'This file is a MediaWiki extension, it is not a valid entry point' ); } // Character classes define( 'EXPR_WHITE_CLASS', " \t\r\n" ); define( 'EXPR_NUMBER_CLASS', '0123456789.' ); // Token types define( 'EXPR_WHITE', 1 ); define( 'EXPR_NUMBER', 2 ); define( 'EXPR_NEGATIVE', 3 ); define( 'EXPR_POSITIVE', 4 ); define( 'EXPR_PLUS', 5 ); define( 'EXPR_MINUS', 6 ); define( 'EXPR_TIMES', 7 ); define( 'EXPR_DIVIDE', 8 ); define( 'EXPR_MOD', 9 ); define( 'EXPR_OPEN', 10 ); define( 'EXPR_CLOSE', 11 ); define( 'EXPR_AND', 12 ); define( 'EXPR_OR', 13 ); define( 'EXPR_NOT', 14 ); define( 'EXPR_EQUALITY', 15 ); define( 'EXPR_LESS', 16 ); define( 'EXPR_GREATER', 17 ); define( 'EXPR_LESSEQ', 18 ); define( 'EXPR_GREATEREQ', 19 ); define( 'EXPR_NOTEQ', 20 ); define( 'EXPR_ROUND', 21 ); class ExprError extends Exception { public function __construct($msg, $parameter = ''){ $this->message = '<strong class="error">' . wfMsgForContent( "pfunc_expr_$msg", htmlspecialchars( $parameter ) ) . '</strong>'; } } class ExprParser { var $maxStackSize = 100; var $precedence = array( EXPR_NEGATIVE => 10, EXPR_POSITIVE => 10, EXPR_NOT => 9, EXPR_TIMES => 8, EXPR_DIVIDE => 8, EXPR_MOD => 8, 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_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', ); var $words = array( 'mod' => EXPR_MOD, 'and' => EXPR_AND, 'or' => EXPR_OR, 'not' => EXPR_NOT, 'round' => EXPR_ROUND, 'div' => EXPR_DIVIDE ); /** * 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 ( !preg_match( '/[^A-Za-z]/', $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]; // Unary operator switch($op){ case EXPR_NOT: 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 == '(' ) { 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( "<br />\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[] = $left % $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[] = ( $left == $right ) ? 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[] = ( $left < $right ) ? 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[] = ( $left > $right ) ? 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[] = ( $left <= $right ) ? 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[] = ( $left >= $right ) ? 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[] = ( $left != $right ) ? 1 : 0; break; default: // Should be impossible to reach here. throw new ExprError('unknown_error'); } } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 6192 | Sam Stafford |
A handful of Mediawiki extensions, skins, and whatnot. Not all of these are authored by me, but I want them all in one place and I need to keep track of any tweaks made. |