You are here

private function ctools_math_expr::nfx in Chaos Tool Suite (ctools) 7

Same name and namespace in other branches
  1. 6 includes/math-expr.inc \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+|\\()/', 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*)\\(\$/", $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*)\\(\$/", $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 = $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 = 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;
}