aboutsummaryrefslogblamecommitdiff
path: root/src/NXP/MathExecutor.php
blob: 482b4b746a60bc839591734909990c6ff88911b9 (plain) (tree)
1
2
3
4
5
6
7
8
9
     
 
   





                                                                          
   
 

              



                            



                                               
 



                     







                                 
 





                                 
 





                                 










                     




                                 
















                                         











                                                                                                                                             

     

                               


                                     
       


                                                             

                     

     

                               



                                     
       
                                                                 
     






                                                                      

     

                               


                                      
                         
                           
       
                                             
     


                                                                    
 
                                             












































                                                               



                         
      












                                                                                    
      
                         
                        

                         
                                                                
     




                                                     
 

                                           

         

                                          
 

                                                                                
             
 
                                       

         
                            

     
       
                                

                         
                                                  
     









                                                                                                       
                 
                      
 


                                           
 






                                                                                                        





                                                                         
                     
                                                                          

                      
 

                                                                              



                                                               




                                               
 
                    



                                                        
         

     



                         
                                                  
     
                                                                      



                                                       
         
 

                                                         
 

                                          
 
                                                     


                                                               

















                                                                                
         

     
       
                                     



                                                                          

                                       
                                




                                               
 











                                                                     
                                                                                                            
                          
 



                                                                     
                                                                                                    
                          
 
                        



                                                            

             
 
                                                  
 
                                       
                                                                            



                       
 
<?php

/**
 * This file is part of the MathExecutor package
 *
 * (c) Alexander Kiryukhin
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace NXP;

use NXP\Classes\Func;
use NXP\Classes\Operand;
use NXP\Classes\Token;
use NXP\Classes\TokenParser;
use NXP\Exception\IncorrectExpressionException;
use NXP\Exception\UnknownFunctionException;
use NXP\Exception\UnknownOperatorException;
use NXP\Exception\UnknownTokenException;

/**
 * Class MathExecutor
 * @package NXP
 */
class MathExecutor
{
    /**
     * Available operators
     *
     * @var array
     */
    private $operators = array();

    /**
     * Available functions
     *
     * @var array
     */
    private $functions = array();

    /**
     * Available variables
     *
     * @var array
     */
    private $variables = array();

    /**
     * @var \SplStack
     */
    private $stack;

    /**
     * @var \SplQueue
     */
    private $queue;

    /**
     * Base math operators
     */
    public function __construct()
    {
        $this->addDefaults();
    }

    public function __clone()
    {
        $this->variables = array();
        $this->operators = array();
        $this->functions = array();

        $this->addDefaults();
    }

    /**
     * Set default operands and functions
     */
    protected function addDefaults()
    {
        $this->addOperator(new Operand('+', 1, Operand::LEFT_ASSOCIATED, Operand::BINARY, function ($op1, $op2) { return $op1+$op2; }));
        $this->addOperator(new Operand('-', 1, Operand::LEFT_ASSOCIATED, Operand::BINARY, function ($op1, $op2) { return $op1-$op2; }));
        $this->addOperator(new Operand('*', 2, Operand::LEFT_ASSOCIATED, Operand::BINARY, function ($op1, $op2) { return $op1*$op2; }));
        $this->addOperator(new Operand('/', 2, Operand::LEFT_ASSOCIATED, Operand::BINARY, function ($op1, $op2) { return $op1/$op2; }));
        $this->addOperator(new Operand('^', 3, Operand::LEFT_ASSOCIATED, Operand::BINARY, function ($op1, $op2) { return pow($op1,$op2); }));

        $this->addFunction(new Func('sin',  function ($arg) { return sin($arg); }));
        $this->addFunction(new Func('cos',  function ($arg) { return cos($arg); }));
        $this->addFunction(new Func('tn',   function ($arg) { return tan($arg); }));
        $this->addFunction(new Func('asin', function ($arg) { return asin($arg); }));
        $this->addFunction(new Func('acos', function ($arg) { return acos($arg); }));
        $this->addFunction(new Func('atn',  function ($arg) { return atan($arg); }));
    }

    /**
     * Add operator to executor
     *
     * @param  Operand      $operator
     * @return MathExecutor
     */
    public function addOperator(Operand $operator)
    {
        $this->operators[$operator->getSymbol()] = $operator;

        return $this;
    }

    /**
     * Add function to executor
     *
     * @param  string       $name
     * @param  callable     $function
     * @return MathExecutor
     */
    public function addFunction($name, callable $function = null)
    {
        if ($name instanceof Func) {
            $this->functions[$name->getName()] = $name->getCallback();
        } else {
            $this->functions[$name] = $function;
        }

        return $this;
    }

    /**
     * Add variable to executor
     *
     * @param  string        $variable
     * @param  integer|float $value
     * @throws \Exception
     * @return MathExecutor
     */
    public function setVar($variable, $value)
    {
        if (!is_numeric($value)) {
            throw new \Exception("Variable value must be a number");
        }

        $this->variables[$variable] = $value;

        return $this;
    }

    /**
     * Add variables to executor
     *
     * @param  array        $variables
     * @param  bool         $clear     Clear previous variables
     * @return MathExecutor
     */
    public function setVars(array $variables, $clear = true)
    {
        if ($clear) {
            $this->removeVars();
        }

        foreach ($variables as $name => $value) {
            $this->setVar($name, $value);
        }

        return $this;
    }

    /**
     * Remove variable from executor
     *
     * @param  string       $variable
     * @return MathExecutor
     */
    public function removeVar($variable)
    {
        unset ($this->variables[$variable]);

        return $this;
    }

    /**
     * Remove all variables
     */
    public function removeVars()
    {
        $this->variables = array();

        return $this;
    }

    /**
     * Execute expression
     *
     * @param $expression
     * @return int|float
     */
    public function execute($expression)
    {
        $reversePolishNotation = $this->convertToReversePolishNotation($expression);
        $result = $this->calculateReversePolishNotation($reversePolishNotation);

        return $result;
    }

    /**
     * Convert expression from normal expression form to RPN
     *
     * @param $expression
     * @return \SplQueue
     * @throws \Exception
     */
    private function convertToReversePolishNotation($expression)
    {
        $this->stack = new \SplStack();
        $this->queue = new \SplQueue();

        $tokenParser = new TokenParser();
        $input = $tokenParser->tokenize($expression);

        foreach ($input as $token) {
            $this->categorizeToken($token);
        }

        while (!$this->stack->isEmpty()) {
            $token = $this->stack->pop();

            if ($token->getType() != Token::OPERATOR) {
                throw new \Exception('Opening bracket without closing bracket');
            }

            $this->queue->push($token);
        }

        return $this->queue;
    }

    /**
     * @param  Token      $token
     * @throws \Exception
     */
    private function categorizeToken(Token $token)
    {
        switch ($token->getType()) {
            case Token::NUMBER :
                $this->queue->push($token);
                break;

            case Token::STRING:
                if (array_key_exists($token->getValue(), $this->variables)) {
                    $this->queue->push(new Token(Token::NUMBER, $this->variables[$token->getValue()]));
                } else {
                    $this->stack->push($token);
                }
                break;

            case Token::LEFT_BRACKET:
                $this->stack->push($token);
                break;

            case Token::RIGHT_BRACKET:
                $previousToken = $this->stack->pop();
                while (!$this->stack->isEmpty() && ($previousToken->getType() != Token::LEFT_BRACKET)) {
                    $this->queue->push($previousToken);
                    $previousToken = $this->stack->pop();
                }
                if ((!$this->stack->isEmpty()) && ($this->stack->top()->getType() == Token::STRING)) {
                    $funcName = $this->stack->pop()->getValue();
                    if (!array_key_exists($funcName, $this->functions)) {
                        throw new UnknownFunctionException(sprintf(
                            'Unknown function: "%s".',
                            $funcName
                        ));
                    }
                    $this->queue->push(new Token(Token::FUNC, $funcName));
                }
                break;

            case Token::OPERATOR:
                if (!array_key_exists($token->getValue(), $this->operators)) {
                    throw new UnknownOperatorException(sprintf(
                        'Unknown operator: "%s".',
                        $token->getValue()
                    ));
                }

                $this->proceedOperator($token);
                $this->stack->push($token);
                break;

            default:
                throw new UnknownTokenException(sprintf(
                    'Unknown token: "%s".',
                    $token->getValue()
                ));
        }
    }

    /**
     * @param $token
     * @throws \Exception
     */
    private function proceedOperator(Token $token)
    {
        if (!array_key_exists($token->getValue(), $this->operators)) {
            throw new UnknownOperatorException(sprintf(
                'Unknown operator: "%s".',
                $token->getValue()
            ));
        }

        /** @var Operand $operator */
        $operator = $this->operators[$token->getValue()];

        while (!$this->stack->isEmpty()) {
            $top = $this->stack->top();

            if ($top->getType() == Token::OPERATOR) {
                /** @var Operand $operator */
                $operator = $this->operators[$top->getValue()];
                $priority = $operator->getPriority();
                if ( $operator->getAssociation() == Operand::RIGHT_ASSOCIATED) {
                    if (($priority > $operator->getPriority())) {
                        $this->queue->push($this->stack->pop());
                    } else {
                        return;
                    }
                } else {
                    if (($priority >= $operator->getPriority())) {
                        $this->queue->push($this->stack->pop());
                    } else {
                        return;
                    }
                }
            } elseif ($top->getType() == Token::STRING) {
                $this->queue->push($this->stack->pop());
            } else {
                return;
            }
        }
    }

    /**
     * @param  \SplQueue  $expression
     * @return mixed
     * @throws \Exception
     */
    private function calculateReversePolishNotation(\SplQueue $expression)
    {
        $this->stack = new \SplStack();
        /** @var Token $token */
        foreach ($expression as $token) {
            switch ($token->getType()) {
                case Token::NUMBER :
                    $this->stack->push($token);
                    break;

                case Token::OPERATOR:
                    /** @var Operand $operator */
                    $operator = $this->operators[$token->getValue()];
                    if ($operator->getType() == Operand::BINARY) {
                        $arg2 = $this->stack->pop()->getValue();
                        $arg1 = $this->stack->pop()->getValue();
                    } else {
                        $arg2 = null;
                        $arg1 = $this->stack->pop()->getValue();
                    }
                    $callback = $operator->getCallback();

                    $this->stack->push(new Token(Token::NUMBER, (call_user_func($callback, $arg1, $arg2))));
                    break;

                case Token::FUNC:
                    /** @var Func $function */
                    $callback = $this->functions[$token->getValue()];
                    $arg = $this->stack->pop()->getValue();
                    $this->stack->push(new Token(Token::NUMBER, (call_user_func($callback, $arg))));
                    break;

                default:
                    throw new UnknownTokenException(sprintf(
                        'Unknown token: "%s".',
                        $token->getValue()
                    ));
            }
        }

        $result = $this->stack->pop()->getValue();

        if (!$this->stack->isEmpty()) {
            throw new IncorrectExpressionException('Incorrect expression.');
        }

        return $result;
    }
}