| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- <?php
- /*
- * MODIFICATIONS
- * - Removed `\` for php5.2 support (does not support namespaces).
- * - Renamed with prefix to avoid naming collisions.
- * - Updated references to the EOS Stack class to reflect name changes.
- */
- /**
- * Equation Operating System Classes.
- *
- * This class was created for the safe parsing of mathematical equations
- * in PHP. There is a need for a way to successfully parse equations
- * in PHP that do NOT require the use of `eval`. `eval` at its core
- * opens the system using it to so many security vulnerabilities it is oft
- * suggested /never/ to use it, and for good reason. This class set will
- * successfully take an equation, parse it, and provide solutions to the
- * developer. It is a safe way to evaluate expressions without putting
- * the system at risk.
- *
- * 2015/07
- * - Added all real number factorial support
- * - Added gamma function to class
- *
- * 2014/08
- * - Added scientific notation support
- * - Added basic factorial support
- *
- * 2013/06 UPDATE:
- * - Added 'abs' (absolute value) support per tjbaron's update.
- *
- * 2013/04 UPDATE:
- * - Moved to native class functions for PHP5
- * - Removed deprecated `eregi` calls to `preg_match`
- * - Updated to PHPDoc comment syntax
- * - Added Exception throwing instead of silent exits
- * - Added additional variable prefix of '$', '&' is still allowed as well
- * - Fixed small implied multiplication problem
- *
- * @author Jon Lawrence <jlawrence11@gmail.com>
- * @copyright Copyright 2005-2015, Jon Lawrence
- * @license http://opensource.org/licenses/LGPL-2.1 LGPL 2.1 License
- * @package EOS
- * @version 2.2.1
- */
- require_once 'Stack.php';
- /**
- * Equation Operating System (EOS) Parser
- *
- * An EOS that can safely parse equations from unknown sources returning
- * the calculated value of it. Can also handle solving equations with
- * variables, if the variables are defined (useful for the Graph creation
- * that the second and extended class in this file provides. {@see eqGraph})
- * This class was created for PHP4 in 2005, updated to fully PHP5 in 2013.
- *
- * @author Jon Lawrence <jlawrence11@gmail.com>
- * @copyright Copyright �2005-2013, Jon Lawrence
- * @license http://opensource.org/licenses/LGPL-2.1 LGPL 2.1 License
- * @package Math
- * @subpackage EOS
- * @version 2.2.1
- */
- class NF_EOS_Parser {
- /**
- * No matching Open/Close pair
- */
- const E_NO_SET = 5500;
- /**
- * Division by 0
- */
- const E_DIV_ZERO = 5501;
- /**
- * No Equation
- */
- const E_NO_EQ = 5502;
- /**
- * No variable replacement available
- */
- const E_NO_VAR = 5503;
- /**
- * Not a number
- */
- const E_NAN = 5504;
- /**
- * @var bool Activate Debug output.
- * @see __construct()
- * @see solveIF()
- */
- public static $debug = FALSE;
- /**#@+
- *Private variables
- */
- private $postFix;
- private $inFix;
- /**#@-*/
- /**#@+
- * Protected variables
- */
- //What are opening and closing selectors
- protected $SEP = array('open' => array('(', '['), 'close' => array(')', ']'));
- //Top presedence following operator - not in use
- protected $SGL = array('!');
- //Order of operations arrays follow
- protected $ST = array('^', '!');
- protected $ST1 = array('/', '*', '%');
- protected $ST2 = array('+', '-');
- //Allowed functions
- protected $FNC = array('sin', 'cos', 'tan', 'csc', 'sec', 'cot', 'abs', 'log', 'log10', 'sqrt');
- /**#@-*/
- /**
- * Construct method
- *
- * Will initiate the class. If variable given, will assign to
- * internal variable to solve with this::solveIF() without needing
- * additional input. Initializing with a variable is not suggested.
- *
- * @see Parser::solveIF()
- * @param String $inFix Standard format equation
- */
- public function __construct($inFix = null) {
- if(defined('DEBUG') && DEBUG) {
- self::$debug = true;
- }
- $this->inFix = (isset($inFix)) ? $inFix : null;
- $this->postFix = array();
- }
- /**
- * Check Infix for opening closing pair matches.
- *
- * This function is meant to solely check to make sure every opening
- * statement has a matching closing one, and throws an exception if
- * it doesn't.
- *
- * @param String $infix Equation to check
- * @throws Exception if malformed.
- * @return Bool true if passes - throws an exception if not.
- */
- private function checkInfix($infix) {
- if(trim($infix) == "") {
- throw new Exception("No Equation given", NF_EOS_Parser::E_NO_EQ);
- }
- //Make sure we have the same number of '(' as we do ')'
- // and the same # of '[' as we do ']'
- if(substr_count($infix, '(') != substr_count($infix, ')')) {
- throw new Exception("Mismatched parenthesis in '{$infix}'", NF_EOS_Parser::E_NO_SET);
- } elseif(substr_count($infix, '[') != substr_count($infix, ']')) {
- throw new Exception("Mismatched brackets in '{$infix}'", NF_EOS_Parser::E_NO_SET);
- }
- $this->inFix = $infix;
- return true;
- }
- /**
- * Infix to Postfix
- *
- * Converts an infix (standard) equation to postfix (RPN) notation.
- * Sets the internal variable $this->postFix for the Parser::solvePF()
- * function to use.
- *
- * @link http://en.wikipedia.org/wiki/Infix_notation Infix Notation
- * @link http://en.wikipedia.org/wiki/Reverse_Polish_notation Reverse Polish Notation
- * @param String $infix A standard notation equation
- * @throws Exception When parenthesis are mismatched.
- * @return Array Fully formed RPN Stack
- */
- public function in2post($infix = null) {
- // if an equation was not passed, use the one that was passed in the constructor
- $infix = (isset($infix)) ? $infix : $this->inFix;
- //check to make sure 'valid' equation
- $this->checkInfix($infix);
- $pf = array();
- $ops = new NF_EOS_Stack();
- //$vars = new Stack();
- // remove all white-space
- $infix = preg_replace("/\s/", "", $infix);
- // Create postfix array index
- $pfIndex = 0;
- //what was the last character? (useful for decerning between a sign for negation and subtraction)
- $lChar = '';
- //loop through all the characters and start doing stuff ^^
- for($i=0;$i<strlen($infix);$i++) {
- // pull out 1 character from the string
- $chr = substr($infix, $i, 1);
- // if the character is numerical
- if(preg_match('/[0-9.]/i', $chr)) {
- // if the previous character was not a '-' or a number
- if((!preg_match('/[0-9.]/i', $lChar) && ($lChar != "")) && (isset($pf[$pfIndex]) && ($pf[$pfIndex]!="-")))
- $pfIndex++; // increase the index so as not to overlap anything
- // Add the number character to the array
- if(isset($pf[$pfIndex])) {
- $pf[$pfIndex] .= $chr;
- } else {
- $pf[$pfIndex] = $chr;
- }
- }
- // If the character opens a set e.g. '(' or '['
- elseif(in_array($chr, $this->SEP['open'])) {
- // if the last character was a number, place an assumed '*' on the stack
- if(preg_match('/[0-9.]/i', $lChar))
- $ops->push('*');
- $ops->push($chr);
- }
- // if the character closes a set e.g. ')' or ']'
- elseif(in_array($chr, $this->SEP['close'])) {
- // find what set it was i.e. matches ')' with '(' or ']' with '['
- $key = array_search($chr, $this->SEP['close']);
- // while the operator on the stack isn't the matching pair...pop it off
- while($ops->peek() != $this->SEP['open'][$key]) {
- $nchr = $ops->pop();
- if($nchr)
- $pf[++$pfIndex] = $nchr;
- else {
- throw new Exception("Error while searching for '". $this->SEP['open'][$key] ."' in '{$infix}'.", NF_EOS_Parser::E_NO_SET);
- }
- }
- $ops->pop();
- }
- // If a special operator that has precedence over everything else
- elseif(in_array($chr, $this->ST)) {
- while(in_array($ops->peek(), $this->ST))
- $pf[++$pfIndex] = $ops->pop();
- $ops->push($chr);
- $pfIndex++;
- }
- // Any other operator other than '+' and '-'
- elseif(in_array($chr, $this->ST1)) {
- while(in_array($ops->peek(), $this->ST1) || in_array($ops->peek(), $this->ST))
- $pf[++$pfIndex] = $ops->pop();
- $ops->push($chr);
- $pfIndex++;
- }
- // if a '+' or '-'
- elseif(in_array($chr, $this->ST2)) {
- // if it is a '-' and the character before it was an operator or nothingness (e.g. it negates a number)
- if((in_array($lChar, array_merge($this->ST1, $this->ST2, $this->ST, $this->SEP['open'])) || $lChar=="") && $chr=="-") {
- // increase the index because there is no reason that it shouldn't..
- $pfIndex++;
- $pf[$pfIndex] = $chr;
- }
- // Otherwise it will function like a normal operator
- else {
- while(in_array($ops->peek(), array_merge($this->ST1, $this->ST2, $this->ST)))
- $pf[++$pfIndex] = $ops->pop();
- $ops->push($chr);
- $pfIndex++;
- }
- }
- // make sure we record this character to be referred to by the next one
- $lChar = $chr;
- }
- // if there is anything on the stack after we are done...add it to the back of the RPN array
- while(($tmp = $ops->pop()) !== false)
- $pf[++$pfIndex] = $tmp;
- // re-index the array at 0
- $pf = array_values($pf);
- // set the private variable for later use if needed
- $this->postFix = $pf;
- // return the RPN array in case developer wants to use it fro some insane reason (bug testing ;]
- return $pf;
- } //end function in2post
- /**
- * Solve Postfix (RPN)
- *
- * This function will solve a RPN array. Default action is to solve
- * the RPN array stored in the class from Parser::in2post(), can take
- * an array input to solve as well, though default action is preferred.
- *
- * @link http://en.wikipedia.org/wiki/Reverse_Polish_notation Postix Notation
- * @param Array $pfArray RPN formatted array. Optional.
- * @throws Exception On division by zero.
- * @return Float Result of the operation.
- */
- public function solvePF($pfArray = null) {
- // if no RPN array is passed - use the one stored in the private var
- $pf = (!is_array($pfArray)) ? $this->postFix : $pfArray;
- // create our temporary function variables
- $temp = array();
- //$tot = 0;
- $hold = 0;
- // Loop through each number/operator
- for($i=0;$i<count($pf); $i++) {
- // If the string isn't an operator, add it to the temp var as a holding place
- if(!in_array($pf[$i], array_merge($this->ST, $this->ST1, $this->ST2))) {
- $temp[$hold++] = $pf[$i];
- }
- // ...Otherwise perform the operator on the last two numbers
- else {
- switch ($pf[$i]) {
- case '+':
- $temp[$hold-2] = $temp[$hold-2] + $temp[$hold-1];
- break;
- case '-':
- $temp[$hold-2] = $temp[$hold-2] - $temp[$hold-1];
- break;
- case '*':
- $temp[$hold-2] = $temp[$hold-2] * $temp[$hold-1];
- break;
- case '/':
- if($temp[$hold-1] == 0) {
- throw new Exception("Division by 0 on: '{$temp[$hold-2]} / {$temp[$hold-1]}' in {$this->inFix}", NF_EOS_Parser::E_DIV_ZERO);
- }
- $temp[$hold-2] = $temp[$hold-2] / $temp[$hold-1];
- break;
- case '^':
- $temp[$hold-2] = pow($temp[$hold-2], $temp[$hold-1]);
- break;
- case '!':
- $temp[$hold-1] = $this->factorial($temp[$hold-1]);
- $hold++;
- break;
- case '%':
- if($temp[$hold-1] == 0) {
- throw new Exception("Division by 0 on: '{$temp[$hold-2]} % {$temp[$hold-1]}' in {$this->inFix}", NF_EOS_Parser::E_DIV_ZERO);
- }
- $temp[$hold-2] = bcmod($temp[$hold-2], $temp[$hold-1]);
- break;
- }
- // Decrease the hold var to one above where the last number is
- $hold = $hold-1;
- }
- }
- // return the last number in the array
- return $temp[$hold-1];
- } //end function solvePF
- public function solve($equation, $values = null) {
- if(is_array($equation)) {
- return $this->solvePF($equation);
- } else {
- return $this->solveIF($equation, $values);
- }
- }
- /**
- * Solve Infix (Standard) Notation Equation
- *
- * Will take a standard equation with optional variables and solve it. Variables
- * must begin with '&' or '$'
- * The variable array must be in the format of 'variable' => value. If
- * variable array is scalar (ie 5), all variables will be replaced with it.
- *
- * @param String $infix Standard Equation to solve
- * @param String|Array $vArray Variable replacement
- * @throws Exception On division by zero and on NaN and lack of variable replacement.
- * @return Float Solved equation
- */
- function solveIF($infix, $vArray = null) {
- $infix = ($infix != "") ? $infix : $this->inFix;
- //Check to make sure a 'valid' expression
- $this->checkInfix($infix);
- //$ops = new Stack();
- //$vars = new Stack();
- $hand = null;
- //remove all white-space
- $infix = preg_replace("/\s/", "", $infix);
- if(NF_EOS_Parser::$debug) {
- $hand=fopen("eq.txt","a");
- }
- //replace scientific notation with normal notation (2e-9 to 2*10^-9)
- $infix = preg_replace('/([\d])([eE])(-?\d)/', '$1*10^$3', $infix);
- if(NF_EOS_Parser::$debug) {
- fwrite($hand, "$infix\n");
- }
- // Finds all the 'functions' within the equation and calculates them
- // NOTE - when using function, only 1 set of parenthesis will be found, instead use brackets for sets within functions!!
- //while((preg_match("/(". implode("|", $this->FNC) . ")\(([^\)\(]*(\([^\)]*\)[^\(\)]*)*[^\)\(]*)\)/", $infix, $match)) != 0) {
- //Nested parenthesis are now a go!
- while((preg_match("/(". implode("|", $this->FNC) . ")\(((?:[^()]|\((?2)\))*+)\)/", $infix, $match)) != 0) {
- $func = $this->solveIF($match[2], $vArray);
- switch($match[1]) {
- case "cos":
- $ans = cos($func);
- break;
- case "sin":
- $ans = sin($func);
- break;
- case "tan":
- $ans = tan($func);
- break;
- case "sec":
- $tmp = cos($func);
- if($tmp == 0) {
- throw new Exception("Division by 0 on: 'sec({$func}) = 1/cos({$func})' in {$this->inFix}", NF_EOS_Parser::E_DIV_ZERO);
- }
- $ans = 1/$tmp;
- break;
- case "csc":
- $tmp = sin($func);
- if($tmp == 0) {
- throw new Exception("Division by 0 on: 'csc({$func}) = 1/sin({$func})' in {$this->inFix}", NF_EOS_Parser::E_DIV_ZERO);
- }
- $ans = 1/$tmp;
- break;
- case "cot":
- $tmp = tan($func);
- if($tmp == 0) {
- throw new Exception("Division by 0 on: 'cot({$func}) = 1/tan({$func})' in {$this->inFix}", NF_EOS_Parser::E_DIV_ZERO);
- }
- $ans = 1/$tmp;
- break;
- case "abs":
- $ans = abs($func);
- break;
- case "log":
- $ans = log($func);
- if(is_nan($ans) || is_infinite($ans)) {
- throw new Exception("Result of 'log({$func}) = {$ans}' is either infinite or a non-number in {$this->inFix}", NF_EOS_Parser::E_NAN);
- }
- break;
- case "log10":
- $ans = log10($func);
- if(is_nan($ans) || is_infinite($ans)) {
- throw new Exception("Result of 'log10({$func}) = {$ans}' is either infinite or a non-number in {$this->inFix}", NF_EOS_Parser::E_NAN);
- }
- break;
- case "sqrt":
- if($func < 0) {
- throw new Exception("Result of 'sqrt({$func}) = i. We can't handle imaginary numbers", NF_EOS_Parser::E_NAN);
- }
- $ans = sqrt($func);
- break;
- default:
- $ans = 0;
- break;
- }
- $infix = str_replace($match[0], "({$ans})", $infix);
- }
- $infix = preg_replace('/[$&]/', "", $infix);
- //Find all the variables that were passed and replaces them
- while((preg_match('/([^a-zA-Z]){0,1}([a-zA-Z]+)([^a-zA-Z]){0,1}/', $infix, $match)) != 0) {
- //remove notices by defining if undefined.
- if(!isset($match[3])) {
- $match[3] = "";
- }
- if(NF_EOS_Parser::$debug)
- fwrite($hand, "{$match[1]} || {$match[3]}\n");
- // Ensure that the variable has an operator or something of that sort in front and back - if it doesn't, add an implied '*'
- if((!in_array($match[1], array_merge($this->ST, $this->ST1, $this->ST2, $this->SEP['open'])) && $match[1] != "") || is_numeric($match[1])) //$this->SEP['close'] removed
- $front = "*";
- else
- $front = "";
- if((!in_array($match[3], array_merge($this->ST, $this->ST1, $this->ST2, $this->SEP['close'])) && $match[3] != "") || is_numeric($match[3])) //$this->SEP['open'] removed
- $back = "*";
- else
- $back = "";
- //Make sure that the variable does have a replacement
- //First check for pi and e variables that wll automagically be replaced
- if(in_array(strtolower($match[2]), array('pi', 'e'))) {
- $t = (strtolower($match[2])=='pi') ? pi() : exp(1);
- $infix = str_replace($match[0], $match[1] . $front. $t. $back . $match[3], $infix);
- } elseif(!isset($vArray[$match[2]]) && (!is_array($vArray != "") && !is_numeric($vArray) && 0 !== $vArray)) {
- throw new Exception("Variable replacement does not exist for '". substr($match[0], 1, 1). $match[2] ."' in {$this->inFix}", NF_EOS_Parser::E_NO_VAR);
- } elseif(!isset($vArray[$match[2]]) && (!is_array($vArray != "") && is_numeric($vArray))) {
- $infix = str_replace($match[0], $match[1] . $front. $vArray. $back . $match[3], $infix);
- } elseif(isset($vArray[$match[2]])) {
- $infix = str_replace($match[0], $match[1] . $front. $vArray[$match[2]]. $back . $match[3], $infix);
- }
- }
- if(NF_EOS_Parser::$debug)
- fclose($hand);
- return $this->solvePF($this->in2post($infix));
- } //end function solveIF
- /**
- * Solve factorial (!)
- *
- * Will take any real positive number and solve for it's factorial. Eg.
- * `5!` will become `1*2*3*4*5` = `120` For integers
- * and
- * 5.2! will become gamma(6.2) for non-integers
- * DONE:
- * Solve for non-integer factorials 2015/07/02
- *
- * @param Float $num Non-negative real number to get factorial of
- * @throws Exception if number is at or less than 0
- * @return Float Solved factorial
- */
- protected function factorial($num) {
- if($num < 0) {
- throw new Exception("Factorial Error: Factorials don't exist for numbers < 0", NF_EOS_Parser::E_NAN);
- }
- //A non-integer! Gamma that sucker up!
- if(intval($num) != $num) {
- return $this->gamma($num + 1);
- }
- $tot = 1;
- for($i=1;$i<=$num;$i++) {
- $tot *= $i;
- }
- return $tot;
- } //end function factorial
- /**
- * Gamma Function
- *
- * Because we can. This function exists as a catch-all for different
- * numerical approx. of gamma if I decide to add any past Lanczos'.
- * This method is public because a function doesn't currently exist
- * within this parser to use it. That will change in the future.
- *
- * @param $z Number to compute gamma from
- * @return Float The gamma (hopefully, I'll test it after writing the code)
- */
- public function gamma($z)
- {
- return $this->laGamma($z);
- }
- /**
- * Lanczos Approximation
- *
- * The Lanczos Approximation method of finding gamma values
- *
- * @link http://www.rskey.org/CMS/index.php/the-library/11
- * @link http://algolist.manual.ru/maths/count_fast/gamma_function.php
- * @link https://en.wikipedia.org/wiki/Lanczos_approximation
- * @param float $z Number to obtain the gamma of
- * @return float Gamma of inputted number
- * @throws Exception if Number is less than or equal to 0
- */
- protected function laGamma($z)
- {
- //check validity of $z, throw error if not a valid number to be used with gamma
- if($z <= 0) {
- throw new Exception("Gamma cannot be calculated on numbers less than or equal to 0", NF_EOS_Parser::E_NAN);
- }
- // Set up coefficients
- $p = array(
- 0 => 1.000000000190015,
- 1 => 76.18009172947146,
- 2 => -86.50532032941677,
- 3 => 24.01409824083091,
- 4 => -1.231739572450155,
- 5 => 1.208650973866179E-3,
- 6 => -5.395239384953E-6
- );
- //formula:
- // ((sqrt(2pi)/z)(p[0]+sum(p[n]/(z+n), 1, 6)))(z+5.5)^(z+0.5)*e^(-(z+5.5))
- // Break it down now...
- $g1 = sqrt(2*pi())/$z;
- //Next comes our summation
- $g2 =0;
- for($n=1;$n<=6;$n++) {
- $g2 += $p[$n]/($z+$n);
- }
- // Don't forget to add p[0] to it...
- $g2 += $p[0];
- $g3 = pow($z+5.5, $z + .5);
- $g4 = exp(-($z+5.5));
- //now just multiply them all together
- $gamma = $g1 * $g2 * $g3 * $g4;
- return $gamma;
- }
- } //end class 'Parser'
|