| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- <?php if ( ! defined( 'ABSPATH' ) ) exit;
- /**
- * 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.
- *
- * 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
- *
- * TODO:
- * - Add factorial support. (ie 5! = 120)
- *
- * @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 EOS
- * @version 2.0
- */
- //The following are defines for thrown exceptions
- /**
- * No matching Open/Close pair
- */
- define('EQEOS_E_NO_SET', 5500);
- /**
- * Division by 0
- */
- define('EQEOS_E_DIV_ZERO', 5501);
- /**
- * No Equation
- */
- define('EQEOS_E_NO_EQ', 5502);
- /**
- * No variable replacement available
- */
- define('EQEOS_E_NO_VAR', 5503);
- if(!defined('DEBUG'))
- define('DEBUG', false);
- //We use a stack class so we don't have to keep track of indices for an array
- // May eventually update to use `array_pop()` `end()` and `array_push()` instead
- // of this class.
- require_once "stack.class.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.0
- */
- class eqEOS {
- /**#@+
- *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');
- /**#@-*/
- /**
- * 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 eqEOS::solveIF()
- * @param String $inFix Standard format equation
- */
- public function __construct($inFix = null) {
- $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", EQEOS_E_NO_EQ);
- return false;
- }
- //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}'", EQEOS_E_NO_SET);
- return false;
- } elseif(substr_count($infix, '[') != substr_count($infix, ']')) {
- throw new Exception("Mismatched brackets in '{$infix}'", EQEOS_E_NO_SET);
- return false;
- }
- $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 eqEOS::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
- * @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 phpStack();
- $vars = new phpStack();
- // remove all white-space
- 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 != "")) && (@$pf[$pfIndex]!="-"))
- $pfIndex++; // increase the index so as not to overlap anything
- // Add the number character to the array
- @$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}'.", EQEOS_E_NO_SET);
- return false;
- }
- }
- $ops->pop();
- }
- // If a special operator that has precedence over everything else
- elseif(in_array($chr, $this->ST)) {
- $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 refered 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 eqEOS::in2post(), can take
- * an array input to solve as well, though default action is prefered.
- *
- * @link http://en.wikipedia.org/wiki/Reverse_Polish_notation Postix Notation
- * @param Array $pfArray RPN formatted array. Optional.
- * @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}", EQEOS_E_DIV_ZERO);
- return false;
- }
- @$temp[$hold-2] = $temp[$hold-2] / $temp[$hold-1];
- break;
- case '^':
- @$temp[$hold-2] = pow($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}", EQEOS_E_DIV_ZERO);
- return false;
- }
- @$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
- /**
- * Solve Infix (Standard) Notation Equation
- *
- * Will take a standard equation with optional variables and solve it. Variables
- * must begin with '&' will expand to allow variables to begin with '$' (TODO)
- * 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
- * @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 phpStack();
- $vars = new phpStack();
- //remove all white-space
- preg_replace("/\s/", "", $infix);
- if(DEBUG)
- $hand=fopen("eq.txt","a");
- //Find all the variables that were passed and replaces them
- while((preg_match('/(.){0,1}[&$]([a-zA-Z]+)(.){0,1}/', $infix, $match)) != 0) {
- //remove notices by defining if undefined.
- if(!isset($match[3])) {
- $match[3] = "";
- }
- if(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
- if(!isset($vArray[$match[2]]) && (!is_array($vArray != "") && !is_numeric($vArray))) {
- throw new Exception("Variable replacement does not exist for '". substr($match[0], 1, -1) ."' in {$this->inFix}", EQEOS_E_NO_VAR);
- return false;
- } 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(DEBUG)
- fwrite($hand, "$infix\n");
- // Finds all the 'functions' within the equation and calculates them
- // NOTE - when using function, only 1 set of paranthesis will be found, instead use brackets for sets within functions!!
- while((preg_match("/(". implode("|", $this->FNC) . ")\(([^\)\(]*(\([^\)]*\)[^\(\)]*)*[^\)\(]*)\)/", $infix, $match)) != 0) {
- $func = $this->solveIF($match[2]);
- 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}", EQEOS_E_DIV_ZERO);
- return false;
- }
- $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}", EQEOS_E_DIV_ZERO);
- return false;
- }
- $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}", EQEOS_E_DIV_ZERO);
- return false;
- }
- $ans = 1/$tmp;
- break;
- default:
- break;
- }
- $infix = str_replace($match[0], $ans, $infix);
- }
- if(DEBUG)
- fclose($hand);
- return $this->solvePF($this->in2post($infix));
- } //end function solveIF
- } //end class 'eqEOS'
- // fun class that requires the GD libraries to give visual output to the user
- /* extends the eqEOS class so that it doesn't need to create it as a private var
- - and it extends the functionality of that class */
- /**
- * Equation Graph
- *
- * Fun class that requires the GD libraries to give visual output of an
- * equation to the user. Extends the eqEOS class.
- *
- * @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.0
- */
- class eqGraph extends eqEOS {
- private $width;
- private $height;
- //GD Image reference
- private $image;
- /**
- * Constructor
- *
- * Sets up the Graph class with an image width and height defaults to
- * 640x480
- *
- * @param Integer $width Image width
- * @param Integer $height Image height
- */
- public function __construct($width=640, $height=480) {
- // default width and height equal to that of a poor monitor (in early 2000s)
- $this->width = $width;
- $this->height = $height;
- //initialize main class variables
- parent::__construct();
- } //end function eqGraph
- /**
- * Create GD Graph Image
- *
- * Creates a GD image based on the equation given with the parameters that are set
- *
- * @param String $eq Equation to use. Needs variable in equation to create graph, all variables are interpreted as 'x'
- * @param Integer $xLow Lower x-bound for graph
- * @param Integer $xHigh Upper x-bound for graph
- * @param Float $xStep Stepping points while solving, the lower, the better precision. Slow if lower than .01
- * @param Boolean $xyGrid Draw gridlines?
- * @param Boolean $yGuess Guess the upper/lower yBounds?
- * @param Integer $yLow Lower y-bound
- * @param Integer $yHigh Upper y-bound
- * @return Null
- */
- public function graph($eq, $xLow, $xHigh, $xStep, $xyGrid = false, $yGuess = true, $yLow=false, $yHigh=false) {
- //create our image and allocate the two colors
- $img = ImageCreate($this->width, $this->height);
- $white = ImageColorAllocate($img, 255, 255, 255);
- $black = ImageColorAllocate($img, 0, 0, 0);
- $grey = ImageColorAllocate($img, 220, 220, 220);
- $xStep = abs($xStep);
- //DEVELOPER, UNCOMMENT NEXT LINE IF WANTING TO PREVENT SLOW GRAPHS
- //$xStep = ($xStep > .01) ? $xStep : 0.01;
- if($xLow > $xHigh)
- list($xLow, $xHigh) = array($xHigh, $xLow); //swap function
-
- $xScale = $this->width/($xHigh-$xLow);
- $counter = 0;
- if(DEBUG) {
- $hand=fopen("eqgraph.txt","w");
- fwrite($hand, "$eq\n");
- }
- for($i=$xLow;$i<=$xHigh;$i+=$xStep) {
- $tester = sprintf("%10.3f",$i);
- if($tester == "-0.000") $i = 0;
- $y = $this->solveIF($eq, $i);
- //eval('$y='. str_replace('&x', $i, $eq).";"); /* used to debug my eqEOS class results */
- if(DEBUG) {
- $tmp1 = sprintf("y(%5.3f) = %10.3f\n", $i, $y);
- fwrite($hand, $tmp1);
- }
- // If developer asked us to find the upper and lower bounds for y...
- if($yGuess==true) {
- $yLow = ($yLow===false || ($y<$yLow)) ? $y : $yLow;
- $yHigh = ($yHigh===false || $y>$yHigh) ? $y : $yHigh;
- }
- $xVars[$counter] = $y;
- $counter++;
- }
- if(DEBUG)
- fclose($hand);
- // add 0.01 to each side so that if y is from 1 to 5, the lines at 1 and 5 are seen
- $yLow-=0.01;$yHigh+=0.01;
- //Now that we have all the variables stored...find the yScale
- $yScale = $this->height/(($yHigh)-($yLow));
- // if developer wanted a grid on the graph, add it now
- if($xyGrid==true) {
- for($i=ceil($yLow);$i<=floor($yHigh);$i++) {
- $i0 = abs($yHigh-$i);
- ImageLine($img, 0, $i0*$yScale, $this->width, $i0*$yScale, $grey);
- }
- for($i=ceil($xLow);$i<=floor($xHigh);$i++) {
- $i0 = abs($xLow-$i);
- ImageLine($img, $i0*$xScale, 0, $i0*$xScale, $this->height, $grey);
- }
- }
-
- //Now that we have the scales, let's see if we can draw an x/y-axis
- if($xLow <= 0 && $xHigh >= 0) {
- //the y-axis is within our range - draw it.
- $x0 = abs($xLow)*$xScale;
- ImageLine($img, $x0, 0, $x0, $this->height, $black);
- for($i=ceil($yLow);$i<=floor($yHigh);$i++) {
- $i0 = abs($yHigh-$i);
- ImageLine($img, $x0-3, $i0*$yScale, $x0+3, $i0*$yScale, $black);
- }
- }
- if($yLow <= 0 && $yHigh >= 0) {
- //the x-axis is within our range - draw it.
- $y0 = abs($yHigh)*$yScale;
- ImageLine($img, 0, $y0, $this->width, $y0, $black);
- for($i=ceil($xLow);$i<=floor($xHigh);$i++) {
- $i0 = abs($xLow-$i);
- ImageLine($img, $i0*$xScale, $y0-3, $i0*$xScale, $y0+3, $black);
- }
- }
- $counter=2;
- //now graph it all ;]
- for($i=$xLow+$xStep;$i<=$xHigh;$i+=$xStep) {
- $x1 = (abs($xLow - ($i - $xStep)))*$xScale;
- $y1 = (($xVars[$counter-1]<$yLow) || ($xVars[$counter-1] > $yHigh)) ? -1 : (abs($yHigh - $xVars[$counter-1]))*$yScale;
- $x2 = (abs($xLow - $i))*$xScale;
- $y2 = (($xVars[$counter]<$yLow) || ($xVars[$counter] > $yHigh)) ? -1 : (abs($yHigh - $xVars[$counter]))*$yScale;
-
- // if any of the y values were found to be off of the y-bounds, don't graph those connecting lines
- if($y1!=-1 && $y2!=-1)
- ImageLine($img, $x1, $y1, $x2, $y2, $black);
- $counter++;
- }
- $this->image = $img;
- } //end function 'graph'
- /**
- * Sends JPG to browser
- *
- * Sends a JPG image with proper header to output
- */
- public function outJPG() {
- header("Content-type: image/jpeg");
- ImageJpeg($this->image);
- }
- /**
- * Sends PNG to browser
- *
- * Sends a PNG image with proper header to output
- */
- function outPNG() {
- header("Content-type: image/png");
- ImagePng($this->image);
- }
-
- /**
- * Output GD Image
- *
- * Will give the developer the GD resource for the graph that
- * can be used to store the graph to the FS or other media
- *
- * @return Resource GD Image Resource
- */
- public function getImage() {
- return $this->image;
- }
-
- /**
- * Output GD Image
- *
- * Alias for eqGraph::getImage()
- *
- * @return Resource GD Image resource
- */
- public function outGD() {
- return $this->getImage();
- }
- } //end class 'eqGraph'
- ?>
|