Parser

Equation Operating System (EOS) Parser.

Defined (1)

The class is defined in the following location(s).

/includes/Libraries/EOS/Parser.php  
  1. class Parser { 
  2.  
  3. /** 
  4. * No matching Open/Close pair 
  5. */ 
  6. const E_NO_SET = 5500; 
  7.  
  8. /** 
  9. * Division by 0 
  10. */ 
  11. const E_DIV_ZERO = 5501; 
  12.  
  13. /** 
  14. * No Equation 
  15. */ 
  16. const E_NO_EQ = 5502; 
  17.  
  18. /** 
  19. * No variable replacement available 
  20. */ 
  21. const E_NO_VAR = 5503; 
  22.  
  23. /** 
  24. * Not a number 
  25. */ 
  26. const E_NAN = 5504; 
  27.  
  28. /** 
  29. * @var bool Activate Debug output. 
  30. * @see __construct() 
  31. * @see solveIF() 
  32. */ 
  33. public static $debug = FALSE; 
  34.  
  35. /**#@+ 
  36. *Private variables 
  37. */ 
  38. private $postFix; 
  39. private $inFix; 
  40. /**#@-*/ 
  41. /**#@+ 
  42. * Protected variables 
  43. */ 
  44. //What are opening and closing selectors 
  45. protected $SEP = array('open' => array('(', '['), 'close' => array(')', ']')); 
  46. //Top presedence following operator - not in use 
  47. protected $SGL = array('!'); 
  48. //Order of operations arrays follow 
  49. protected $ST = array('^', '!'); 
  50. protected $ST1 = array('/', '*', '%'); 
  51. protected $ST2 = array('+', '-'); 
  52. //Allowed functions 
  53. protected $FNC = array('sin', 'cos', 'tan', 'csc', 'sec', 'cot', 'abs', 'log', 'log10', 'sqrt'); 
  54. /**#@-*/ 
  55. /** 
  56. * Construct method 
  57. * Will initiate the class. If variable given, will assign to 
  58. * internal variable to solve with this::solveIF() without needing 
  59. * additional input. Initializing with a variable is not suggested. 
  60. * @see Parser::solveIF() 
  61. * @param String $inFix Standard format equation 
  62. */ 
  63. public function __construct($inFix = null) { 
  64. if(defined('DEBUG') && DEBUG) { 
  65. self::$debug = true; 
  66. $this->inFix = (isset($inFix)) ? $inFix : null; 
  67. $this->postFix = array(); 
  68.  
  69. /** 
  70. * Check Infix for opening closing pair matches. 
  71. * This function is meant to solely check to make sure every opening 
  72. * statement has a matching closing one, and throws an exception if 
  73. * it doesn't. 
  74. * @param String $infix Equation to check 
  75. * @throws Exception if malformed. 
  76. * @return Bool true if passes - throws an exception if not. 
  77. */ 
  78. private function checkInfix($infix) { 
  79. if(trim($infix) == "") { 
  80. throw new Exception("No Equation given", Parser::E_NO_EQ); 
  81. //Make sure we have the same number of '(' as we do ')' 
  82. // and the same # of '[' as we do ']' 
  83. if(substr_count($infix, '(') != substr_count($infix, ')')) { 
  84. throw new Exception("Mismatched parenthesis in '{$infix}'", Parser::E_NO_SET); 
  85. } elseif(substr_count($infix, '[') != substr_count($infix, ']')) { 
  86. throw new Exception("Mismatched brackets in '{$infix}'", Parser::E_NO_SET); 
  87. $this->inFix = $infix; 
  88. return true; 
  89.  
  90. /** 
  91. * Infix to Postfix 
  92. * Converts an infix (standard) equation to postfix (RPN) notation. 
  93. * Sets the internal variable $this->postFix for the Parser::solvePF() 
  94. * function to use. 
  95. * @link http://en.wikipedia.org/wiki/Infix_notation Infix Notation 
  96. * @link http://en.wikipedia.org/wiki/Reverse_Polish_notation Reverse Polish Notation 
  97. * @param String $infix A standard notation equation 
  98. * @throws Exception When parenthesis are mismatched. 
  99. * @return Array Fully formed RPN Stack 
  100. */ 
  101. public function in2post($infix = null) { 
  102. // if an equation was not passed, use the one that was passed in the constructor 
  103. $infix = (isset($infix)) ? $infix : $this->inFix; 
  104.  
  105. //check to make sure 'valid' equation 
  106. $this->checkInfix($infix); 
  107. $pf = array(); 
  108. $ops = new Stack(); 
  109. //$vars = new Stack(); 
  110.  
  111. // remove all white-space 
  112. $infix = preg_replace("/\s/", "", $infix); 
  113.  
  114. // Create postfix array index 
  115. $pfIndex = 0; 
  116.  
  117. //what was the last character? (useful for decerning between a sign for negation and subtraction) 
  118. $lChar = ''; 
  119.  
  120. //loop through all the characters and start doing stuff ^^ 
  121. for($i=0;$i<strlen($infix);$i++) { 
  122. // pull out 1 character from the string 
  123. $chr = substr($infix, $i, 1); 
  124.  
  125. // if the character is numerical 
  126. if(preg_match('/[0-9.]/i', $chr)) { 
  127. // if the previous character was not a '-' or a number 
  128. if((!preg_match('/[0-9.]/i', $lChar) && ($lChar != "")) && (isset($pf[$pfIndex]) && ($pf[$pfIndex]!="-"))) 
  129. $pfIndex++; // increase the index so as not to overlap anything 
  130. // Add the number character to the array 
  131. if(isset($pf[$pfIndex])) { 
  132. $pf[$pfIndex] .= $chr; 
  133. } else { 
  134. $pf[$pfIndex] = $chr; 
  135.  
  136. // If the character opens a set e.g. '(' or '[' 
  137. elseif(in_array($chr, $this->SEP['open'])) { 
  138. // if the last character was a number, place an assumed '*' on the stack 
  139. if(preg_match('/[0-9.]/i', $lChar)) 
  140. $ops->push('*'); 
  141.  
  142. $ops->push($chr); 
  143. // if the character closes a set e.g. ')' or ']' 
  144. elseif(in_array($chr, $this->SEP['close'])) { 
  145. // find what set it was i.e. matches ')' with '(' or ']' with '[' 
  146. $key = array_search($chr, $this->SEP['close']); 
  147. // while the operator on the stack isn't the matching pair...pop it off 
  148. while($ops->peek() != $this->SEP['open'][$key]) { 
  149. $nchr = $ops->pop(); 
  150. if($nchr) 
  151. $pf[++$pfIndex] = $nchr; 
  152. else { 
  153. throw new Exception("Error while searching for '". $this->SEP['open'][$key] ."' in '{$infix}'.", Parser::E_NO_SET); 
  154. $ops->pop(); 
  155. // If a special operator that has precedence over everything else 
  156. elseif(in_array($chr, $this->ST)) { 
  157. while(in_array($ops->peek(), $this->ST)) 
  158. $pf[++$pfIndex] = $ops->pop(); 
  159. $ops->push($chr); 
  160. $pfIndex++; 
  161. // Any other operator other than '+' and '-' 
  162. elseif(in_array($chr, $this->ST1)) { 
  163. while(in_array($ops->peek(), $this->ST1) || in_array($ops->peek(), $this->ST)) 
  164. $pf[++$pfIndex] = $ops->pop(); 
  165.  
  166. $ops->push($chr); 
  167. $pfIndex++; 
  168. // if a '+' or '-' 
  169. elseif(in_array($chr, $this->ST2)) { 
  170. // if it is a '-' and the character before it was an operator or nothingness (e.g. it negates a number) 
  171. if((in_array($lChar, array_merge($this->ST1, $this->ST2, $this->ST, $this->SEP['open'])) || $lChar=="") && $chr=="-") { 
  172. // increase the index because there is no reason that it shouldn't.. 
  173. $pfIndex++; 
  174. $pf[$pfIndex] = $chr; 
  175. // Otherwise it will function like a normal operator 
  176. else { 
  177. while(in_array($ops->peek(), array_merge($this->ST1, $this->ST2, $this->ST))) 
  178. $pf[++$pfIndex] = $ops->pop(); 
  179. $ops->push($chr); 
  180. $pfIndex++; 
  181. // make sure we record this character to be referred to by the next one 
  182. $lChar = $chr; 
  183. // if there is anything on the stack after we are done...add it to the back of the RPN array 
  184. while(($tmp = $ops->pop()) !== false) 
  185. $pf[++$pfIndex] = $tmp; 
  186.  
  187. // re-index the array at 0 
  188. $pf = array_values($pf); 
  189.  
  190. // set the private variable for later use if needed 
  191. $this->postFix = $pf; 
  192.  
  193. // return the RPN array in case developer wants to use it fro some insane reason (bug testing ;] 
  194. return $pf; 
  195. } //end function in2post 
  196.  
  197. /** 
  198. * Solve Postfix (RPN) 
  199. * This function will solve a RPN array. Default action is to solve 
  200. * the RPN array stored in the class from Parser::in2post(), can take 
  201. * an array input to solve as well, though default action is preferred. 
  202. * @link http://en.wikipedia.org/wiki/Reverse_Polish_notation Postix Notation 
  203. * @param Array $pfArray RPN formatted array. Optional. 
  204. * @throws Exception On division by zero. 
  205. * @return Float Result of the operation. 
  206. */ 
  207. public function solvePF($pfArray = null) { 
  208. // if no RPN array is passed - use the one stored in the private var 
  209. $pf = (!is_array($pfArray)) ? $this->postFix : $pfArray; 
  210.  
  211. // create our temporary function variables 
  212. $temp = array(); 
  213. //$tot = 0; 
  214. $hold = 0; 
  215.  
  216. // Loop through each number/operator 
  217. for($i=0;$i<count($pf); $i++) { 
  218. // If the string isn't an operator, add it to the temp var as a holding place 
  219. if(!in_array($pf[$i], array_merge($this->ST, $this->ST1, $this->ST2))) { 
  220. $temp[$hold++] = $pf[$i]; 
  221. // ...Otherwise perform the operator on the last two numbers 
  222. else { 
  223. switch ($pf[$i]) { 
  224. case '+': 
  225. $temp[$hold-2] = $temp[$hold-2] + $temp[$hold-1]; 
  226. break; 
  227. case '-': 
  228. $temp[$hold-2] = $temp[$hold-2] - $temp[$hold-1]; 
  229. break; 
  230. case '*': 
  231. $temp[$hold-2] = $temp[$hold-2] * $temp[$hold-1]; 
  232. break; 
  233. case '/': 
  234. if($temp[$hold-1] == 0) { 
  235. throw new Exception("Division by 0 on: '{$temp[$hold-2]} / {$temp[$hold-1]}' in {$this->inFix}", Parser::E_DIV_ZERO); 
  236. $temp[$hold-2] = $temp[$hold-2] / $temp[$hold-1]; 
  237. break; 
  238. case '^': 
  239. $temp[$hold-2] = pow($temp[$hold-2], $temp[$hold-1]); 
  240. break; 
  241. case '!': 
  242. $temp[$hold-1] = $this->factorial($temp[$hold-1]); 
  243. $hold++; 
  244. break; 
  245. case '%': 
  246. if($temp[$hold-1] == 0) { 
  247. throw new Exception("Division by 0 on: '{$temp[$hold-2]} % {$temp[$hold-1]}' in {$this->inFix}", Parser::E_DIV_ZERO); 
  248. $temp[$hold-2] = bcmod($temp[$hold-2], $temp[$hold-1]); 
  249. break; 
  250. // Decrease the hold var to one above where the last number is 
  251. $hold = $hold-1; 
  252. // return the last number in the array 
  253. return $temp[$hold-1]; 
  254.  
  255. } //end function solvePF 
  256.  
  257. public function solve($equation, $values = null) { 
  258. if(is_array($equation)) { 
  259. return $this->solvePF($equation); 
  260. } else { 
  261. return $this->solveIF($equation, $values); 
  262.  
  263. /** 
  264. * Solve Infix (Standard) Notation Equation 
  265. * Will take a standard equation with optional variables and solve it. Variables 
  266. * must begin with '&' or '$' 
  267. * The variable array must be in the format of 'variable' => value. If 
  268. * variable array is scalar (ie 5), all variables will be replaced with it. 
  269. * @param String $infix Standard Equation to solve 
  270. * @param String|Array $vArray Variable replacement 
  271. * @throws Exception On division by zero and on NaN and lack of variable replacement. 
  272. * @return Float Solved equation 
  273. */ 
  274. function solveIF($infix, $vArray = null) { 
  275. $infix = ($infix != "") ? $infix : $this->inFix; 
  276. //Check to make sure a 'valid' expression 
  277. $this->checkInfix($infix); 
  278.  
  279. //$ops = new Stack(); 
  280. //$vars = new Stack(); 
  281. $hand = null; 
  282.  
  283. //remove all white-space 
  284. $infix = preg_replace("/\s/", "", $infix); 
  285. if(Parser::$debug) { 
  286. $hand=fopen("eq.txt", "a"); 
  287.  
  288. //replace scientific notation with normal notation (2e-9 to 2*10^-9) 
  289. $infix = preg_replace('/([\d])([eE])(-?\d)/', '$1*10^$3', $infix); 
  290.  
  291. if(Parser::$debug) { 
  292. fwrite($hand, "$infix\n"); 
  293.  
  294. // Finds all the 'functions' within the equation and calculates them 
  295. // NOTE - when using function, only 1 set of parenthesis will be found, instead use brackets for sets within functions!! 
  296. //while((preg_match("/(". implode("|", $this->FNC) . ")\(([^\)\(]*(\([^\)]*\)[^\(\)]*)*[^\)\(]*)\)/", $infix, $match)) != 0) { 
  297. //Nested parenthesis are now a go! 
  298. while((preg_match("/(". implode("|", $this->FNC) . ")\(((?:[^()]|\((?2)\))*+)\)/", $infix, $match)) != 0) { 
  299. $func = $this->solveIF($match[2], $vArray); 
  300. switch($match[1]) { 
  301. case "cos": 
  302. $ans = cos($func); 
  303. break; 
  304. case "sin": 
  305. $ans = sin($func); 
  306. break; 
  307. case "tan": 
  308. $ans = tan($func); 
  309. break; 
  310. case "sec": 
  311. $tmp = cos($func); 
  312. if($tmp == 0) { 
  313. throw new Exception("Division by 0 on: 'sec({$func}) = 1/cos({$func})' in {$this->inFix}", Parser::E_DIV_ZERO); 
  314. $ans = 1/$tmp; 
  315. break; 
  316. case "csc": 
  317. $tmp = sin($func); 
  318. if($tmp == 0) { 
  319. throw new Exception("Division by 0 on: 'csc({$func}) = 1/sin({$func})' in {$this->inFix}", Parser::E_DIV_ZERO); 
  320. $ans = 1/$tmp; 
  321. break; 
  322. case "cot": 
  323. $tmp = tan($func); 
  324. if($tmp == 0) { 
  325. throw new Exception("Division by 0 on: 'cot({$func}) = 1/tan({$func})' in {$this->inFix}", Parser::E_DIV_ZERO); 
  326. $ans = 1/$tmp; 
  327. break; 
  328. case "abs": 
  329. $ans = abs($func); 
  330. break; 
  331. case "log": 
  332. $ans = log($func); 
  333. if(is_nan($ans) || is_infinite($ans)) { 
  334. throw new Exception("Result of 'log({$func}) = {$ans}' is either infinite or a non-number in {$this->inFix}", Parser::E_NAN); 
  335. break; 
  336. case "log10": 
  337. $ans = log10($func); 
  338. if(is_nan($ans) || is_infinite($ans)) { 
  339. throw new Exception("Result of 'log10({$func}) = {$ans}' is either infinite or a non-number in {$this->inFix}", Parser::E_NAN); 
  340. break; 
  341. case "sqrt": 
  342. if($func < 0) { 
  343. throw new Exception("Result of 'sqrt({$func}) = i. We can't handle imaginary numbers", Parser::E_NAN); 
  344. $ans = sqrt($func); 
  345. break; 
  346. default: 
  347. $ans = 0; 
  348. break; 
  349. $infix = str_replace($match[0], "({$ans})", $infix); 
  350.  
  351. $infix = preg_replace('/[$&]/', "", $infix); 
  352. //Find all the variables that were passed and replaces them 
  353. while((preg_match('/([^a-zA-Z]) {0, 1}([a-zA-Z]+)([^a-zA-Z]) {0, 1}/', $infix, $match)) != 0) { 
  354.  
  355.  
  356. //remove notices by defining if undefined. 
  357. if(!isset($match[3])) { 
  358. $match[3] = ""; 
  359.  
  360. if(Parser::$debug) 
  361. fwrite($hand, "{$match[1]} || {$match[3]}\n"); 
  362. // Ensure that the variable has an operator or something of that sort in front and back - if it doesn't, add an implied '*' 
  363. 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 
  364. $front = "*"; 
  365. else 
  366. $front = ""; 
  367.  
  368. 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 
  369. $back = "*"; 
  370. else 
  371. $back = ""; 
  372.  
  373. //Make sure that the variable does have a replacement 
  374. //First check for pi and e variables that wll automagically be replaced 
  375. if(in_array(strtolower($match[2]), array('pi', 'e'))) { 
  376. $t = (strtolower($match[2])=='pi') ? pi() : exp(1); 
  377. $infix = str_replace($match[0], $match[1] . $front. $t. $back . $match[3], $infix); 
  378. } elseif(!isset($vArray[$match[2]]) && (!is_array($vArray != "") && !is_numeric($vArray) && 0 !== $vArray)) { 
  379. throw new Exception("Variable replacement does not exist for '". substr($match[0], 1, 1). $match[2] ."' in {$this->inFix}", Parser::E_NO_VAR); 
  380. } elseif(!isset($vArray[$match[2]]) && (!is_array($vArray != "") && is_numeric($vArray))) { 
  381. $infix = str_replace($match[0], $match[1] . $front. $vArray. $back . $match[3], $infix); 
  382. } elseif(isset($vArray[$match[2]])) { 
  383. $infix = str_replace($match[0], $match[1] . $front. $vArray[$match[2]]. $back . $match[3], $infix); 
  384.  
  385. if(Parser::$debug) 
  386. fclose($hand); 
  387. return $this->solvePF($this->in2post($infix)); 
  388.  
  389.  
  390. } //end function solveIF 
  391.  
  392. /** 
  393. * Solve factorial (!) 
  394. * Will take any real positive number and solve for it's factorial. Eg. 
  395. * `5!` will become `1*2*3*4*5` = `120` For integers 
  396. * and 
  397. * 5.2! will become gamma(6.2) for non-integers 
  398. * DONE: 
  399. * Solve for non-integer factorials 2015/07/02 
  400. * @param Float $num Non-negative real number to get factorial of 
  401. * @throws Exception if number is at or less than 0 
  402. * @return Float Solved factorial 
  403. */ 
  404. protected function factorial($num) { 
  405. if($num < 0) { 
  406. throw new Exception("Factorial Error: Factorials don't exist for numbers < 0", Parser::E_NAN); 
  407. //A non-integer! Gamma that sucker up! 
  408. if(intval($num) != $num) { 
  409. return $this->gamma($num + 1); 
  410.  
  411. $tot = 1; 
  412. for($i=1;$i<=$num;$i++) { 
  413. $tot *= $i; 
  414. return $tot; 
  415. } //end function factorial 
  416.  
  417. /** 
  418. * Gamma Function 
  419. * Because we can. This function exists as a catch-all for different 
  420. * numerical approx. of gamma if I decide to add any past Lanczos'. 
  421. * This method is public because a function doesn't currently exist 
  422. * within this parser to use it. That will change in the future. 
  423. * @param $z Number to compute gamma from 
  424. * @return Float The gamma (hopefully, I'll test it after writing the code) 
  425. */ 
  426. public function gamma($z) 
  427. return $this->laGamma($z); 
  428.  
  429. /** 
  430. * Lanczos Approximation 
  431. * The Lanczos Approximation method of finding gamma values 
  432. * @link http://www.rskey.org/CMS/index.php/the-library/11 
  433. * @link http://algolist.manual.ru/maths/count_fast/gamma_function.php 
  434. * @link https://en.wikipedia.org/wiki/Lanczos_approximation 
  435. * @param float $z Number to obtain the gamma of 
  436. * @return float Gamma of inputted number 
  437. * @throws Exception if Number is less than or equal to 0 
  438. */ 
  439. protected function laGamma($z) 
  440. //check validity of $z, throw error if not a valid number to be used with gamma 
  441. if($z <= 0) { 
  442. throw new Exception("Gamma cannot be calculated on numbers less than or equal to 0", Parser::E_NAN); 
  443. // Set up coefficients 
  444. $p = array( 
  445. 0 => 1.000000000190015,  
  446. 1 => 76.18009172947146,  
  447. 2 => -86.50532032941677,  
  448. 3 => 24.01409824083091,  
  449. 4 => -1.231739572450155,  
  450. 5 => 1.208650973866179E-3,  
  451. 6 => -5.395239384953E-6 
  452. ); 
  453. //formula: 
  454. // ((sqrt(2pi)/z)(p[0]+sum(p[n]/(z+n), 1, 6)))(z+5.5)^(z+0.5)*e^(-(z+5.5)) 
  455. // Break it down now... 
  456. $g1 = sqrt(2*pi())/$z; 
  457. //Next comes our summation 
  458. $g2 =0; 
  459. for($n=1;$n<=6;$n++) { 
  460. $g2 += $p[$n]/($z+$n); 
  461. // Don't forget to add p[0] to it... 
  462. $g2 += $p[0]; 
  463. $g3 = pow($z+5.5, $z + .5); 
  464. $g4 = exp(-($z+5.5)); 
  465. //now just multiply them all together 
  466. $gamma = $g1 * $g2 * $g3 * $g4; 
  467. return $gamma; 
  468.  
  469. } //end class 'Parser'