function ctools_math_expr::nfx
Convert infix to postfix notation.
Parameters
string $expr: The expression to convert.
Return value
array|bool The expression as an ordered list of postfix action tokens.
1 call to ctools_math_expr::nfx()
- ctools_math_expr::evaluate in includes/
math-expr.inc - Evaluate the expression.
File
-
includes/
math-expr.inc, line 410
Class
- ctools_math_expr
- ctools_math_expr Class.
Code
private function nfx($expr) {
$index = 0;
$stack = new ctools_math_expr_stack();
// Postfix form of expression, to be passed to pfx().
$output = array();
// @todo: Because the expr can contain string operands, using strtolower here is a bug.
$expr = trim(strtolower($expr));
// We use this in syntax-checking the expression and determining when
// '-' is a negation.
$expecting_op = FALSE;
while (TRUE) {
$op = substr($expr, $index, 1);
// Get the first character at the current index, and if the second
// character is an =, add it to our op as well (accounts for <=).
if (substr($expr, $index + 1, 1) === '=') {
$op = substr($expr, $index, 2);
$index++;
}
// Find out if we're currently at the beginning of a number/variable/
// function/parenthesis/operand.
$ex = preg_match('/^([a-z]\\w*\\(?|\\d+(?:\\.\\d*)?|\\.\\d+|\\()/', (string) substr($expr, $index), $match);
// Is it a negation instead of a minus?
if ($op === '-' and !$expecting_op) {
// Put a negation on the stack.
$stack->push('_');
$index++;
}
elseif ($op == '_') {
return $this->trigger("illegal character '_'");
}
elseif ((isset($this->ops[$op]) || $ex) && $expecting_op) {
// Are we expecting an operator but have a num, var, func, or
// open-paren?
if ($ex) {
$op = '*';
// It's an implicit multiplication.
$index--;
}
// Heart of the algorithm:
while ($stack->count() > 0 && ($o2 = $stack->last()) && isset($this->ops[$o2]) && (!empty($this->ops[$op]['right']) ? $this->ops[$op]['precedence'] < $this->ops[$o2]['precedence'] : $this->ops[$op]['precedence'] <= $this->ops[$o2]['precedence'])) {
// Pop stuff off the stack into the output.
$output[] = $stack->pop();
}
// Many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
// finally put OUR operator onto the stack.
$stack->push($op);
$index++;
$expecting_op = FALSE;
}
elseif ($op === ')') {
// Pop off the stack back to the last '('.
while (($o2 = $stack->pop()) !== '(') {
if (is_null($o2)) {
return $this->trigger("unexpected ')'");
}
else {
$output[] = $o2;
}
}
// Did we just close a function?
if (preg_match("/^([a-z]\\w*)\\(\$/", (string) $stack->last(2), $matches)) {
// Get the function name.
$fnn = $matches[1];
// See how many arguments there were (cleverly stored on the stack,
// thank you).
$arg_count = $stack->pop();
// Pop the function and push onto the output.
$output[] = $stack->pop();
// Check the argument count:
if (isset($this->funcs[$fnn])) {
$fdef = $this->funcs[$fnn];
$max_arguments = isset($fdef['max arguments']) ? $fdef['max arguments'] : $fdef['arguments'];
if ($arg_count > $max_arguments) {
return $this->trigger("too many arguments ({$arg_count} given, {$max_arguments} expected)");
}
}
elseif (array_key_exists($fnn, $this->userfuncs)) {
$fdef = $this->userfuncs[$fnn];
if ($arg_count !== count($fdef['args'])) {
return $this->trigger("wrong number of arguments ({$arg_count} given, " . count($fdef['args']) . ' expected)');
}
}
else {
// Did we somehow push a non-function on the stack? this should
// never happen.
return $this->trigger('internal error');
}
}
$index++;
}
elseif ($op === ',' && $expecting_op) {
$index++;
$expecting_op = FALSE;
}
elseif ($op === '(' && !$expecting_op) {
$stack->push('(');
$index++;
}
elseif ($ex && !$expecting_op) {
// Make sure there was a function.
if (preg_match("/^([a-z]\\w*)\\(\$/", (string) $stack->last(3), $matches)) {
// Pop the argument expression stuff and push onto the output:
while (($o2 = $stack->pop()) !== '(') {
// Oops, never had a '('.
if (is_null($o2)) {
return $this->trigger("unexpected argument in {$expr} {$o2}");
}
else {
$output[] = $o2;
}
}
// Increment the argument count.
$stack->push($stack->pop() + 1);
// Put the ( back on, we'll need to pop back to it again.
$stack->push('(');
}
// Do we now have a function/variable/number?
$expecting_op = TRUE;
$val = (string) $match[1];
if (preg_match("/^([a-z]\\w*)\\(\$/", $val, $matches)) {
// May be func, or variable w/ implicit multiplication against
// parentheses...
if (isset($this->funcs[$matches[1]]) or array_key_exists($matches[1], $this->userfuncs)) {
$stack->push($val);
$stack->push(0);
$stack->push('(');
$expecting_op = FALSE;
}
else {
$val = $matches[1];
$output[] = $val;
}
}
else {
$output[] = $val;
}
$index += strlen($val);
}
elseif ($op === ')') {
// Miscellaneous error checking.
return $this->trigger("unexpected ')'");
}
elseif (isset($this->ops[$op]) and !$expecting_op) {
return $this->trigger("unexpected operator '{$op}'");
}
elseif ($op === '"') {
// Fetch a quoted string.
$string = (string) substr($expr, $index);
if (preg_match('/"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"/s', $string, $matches)) {
$string = $matches[0];
// Trim the quotes off:
$output[] = $string;
$index += strlen($string);
$expecting_op = TRUE;
}
else {
return $this->trigger('open quote without close quote.');
}
}
else {
// I don't even want to know what you did to get here.
return $this->trigger("an unexpected error occurred at {$op}");
}
if ($index === strlen($expr)) {
if (isset($this->ops[$op])) {
// Did we end with an operator? bad.
return $this->trigger("operator '{$op}' lacks operand");
}
else {
break;
}
}
// Step the index past whitespace (pretty much turns whitespace into
// implicit multiplication if no operator is there).
while (substr($expr, $index, 1) === ' ') {
$index++;
}
}
// Pop everything off the stack and push onto output:
while (!is_null($op = $stack->pop())) {
// If there are (s on the stack, ()s were unbalanced.
if ($op === '(') {
return $this->trigger("expecting ')'");
}
$output[] = $op;
}
return $output;
}