Files
com_sportsmanager/src/structure/components/com_sportsmanager/mathparser.php
T

2672 lines
91 KiB
PHP

<?php
/*
*
* PRODUCT: Bestcode Math Parser for PHP
* COPYRIGHT: (C) COPYRIGHT Suavi Ali Demir 2010-2011
*
* The source code for this program is not GNU or is not free.
* The user is granted rights to modify and/or use source code
* to use it with a PHP application. The user of the software
* is not granted rights to modify and use source code to create
* a competitive product. Resulting modifications cannot be re-sold
* as a competitive product but they can be used as part of the
* application.
*
* Copyright and all rights of this software, irrespective of what
* has been deposited with the U.S. Copyright Office belongs
* to Suavi Ali Demir.
*
* http://www.bestcode.com
*
* This notice shall remain in modified versions of this file.
*/
use Joomla\CMS\Log\Log;
/**
* MathParser is a mathematical expression parser class for PHP. MathParser parses and evaluates
* mathematical formulas given as strings at runtime. Math expressions can contain variables,
* functions, numeric literals combined with basic mathematical operators +, -, /, *, ^, % (modulus).
* MathParser can utilize user defined variables, user defined functions that are implemented in PHP
* language.
* <br /><br />
* Math parser library features at a glance include:<br /><br />
*
* Easy to use, simple class API.<br />
* Functions with 0 or more number of parameters in the form of: f(x,y,z, ...)<br />
* Functions with unknown number of parameters, e.g. SUM(a,b,c,...)<br />
* Comes with predefined functions.<br />
* You can create custom functions/variables with callbacks to the functions that you define in your source code, either through delegates or interfaces.<br />
* VariableResolver, a callback function to provide values for undefined variables.<br />
* Function/variable names start with letters and can contain letters, numbers and underscore (_).<br />
* Expression can contain string literals, variable values and function return values can be strings.<br />
* Arithmetic Operators: +, -, /, *, ^, %(mod)<br />
* Boolean Operators: <, >, =, &, |, ! ,<>, >=, <=<br />
* String concatenation with & or +<br />
* Optimization: Constant expression elimination for repeated tasks.<br />
* Paranthesis: (<br />
* List of predefined functions is available in the documentation.<br />
* Provides localization support which is a dictionary of message-keys to messages you can set.<br />
* Royalty free distribution.<br />
* Comes as source code.<br /><br />
*
* You may ask why not use PHP eval() function?
* The answer is security. eval() is a security nightmare if you ask for formula input from the user.
* PHP syntax for formulas with $ signs is not intuitive either. The aim of MathParser is to be a
* a secure, simple math evaluator alternative to eval(). Please keep in mind, MathParser only evaluates
* mathematical expressions given using a simple syntax that consists of numbers, variables, function calls
* and paranthesis. MathParser does not evaluate PHP expressions.<br /><br />
*
* Predefined functions are:
* <br /><br />
* SQR: Square function which can be used as SQR(X)
* <br /><br />
* SIN: Sinus function which can be used as SIN(X), X is a real-type expression.
* Sin returns the sine of the angle X in radians.
* <br /><br />
* COS: Cosinus function which can be used as COS(X), X is a real-type expression.
* COS returns the cosine of the angle X in radians.
* <br /><br />
* ATAN: ArcTangent function which can be used as ATAN(X)
* Returns the arctangent of a number as a numeric value between -PI/2 and PI/2 radians.
* <br /><br />
* SINH: Sinus Hyperbolic function which can be used as SINH(X)
* <br /><br />
* COSH: Cosinus Hyperbolic function which can be used as COSH(X)
* <br /><br />
* COTAN: which can be used as COTAN(X)
* <br /><br />
* TAN: which can be used as TAN(X)
* <br /><br />
* EXP: which can be used as EXP(X)
* <br /><br />
* LN: natural log, which can be used as LN(X)
* <br /><br />
* LOG: 10 based log, which can be used as LOG(X)
* <br /><br />
* SQRT: which can be used as SQRT(X)
* <br /><br />
* ABS: absolute value, which can be used as ABS(X)
* <br /><br />
* SIGN: SIGN(X) returns -1 if X&lt;0; +1 if X&gt;0, 0 if X=0; it can be used as SQR(X)
* <br /><br />
* TRUNC: Discards the fractional part of a number. e.g. TRUNC(-3.2) is -3, TRUNC(3.2) is 3.
* <br /><br />
* CEIL: CEIL(-3.2) = 3, CEIL(3.2) = 4
* <br /><br />
* FLOOR: FLOOR(-3.2) = -4, FLOOR(3.2) = 3
* <br /><br />
* VAL: VAL("3.1") = 3.1 Returns the floating point numeric value of the string argument.
* <br /><br />
* Predefined functions that take two parameters are:
* <br /><br />
* POW: The Power function raises Base to any power. For fractional exponents or
* exponents greater than MaxInt, Base must be greater than 0.
* <br /><br />
* LOGN: The LogN function returns the log base N of X. Example: LOGN(10, 100) = 2
* <br /><br />
* MIN: MIN(2, 3) is 2.
* <br /><br />
* MAX: MAX(2, 3) is 3.
* <br /><br />
* IF: The IF(b, case1, case2) function provides branching capability.
* If b is not 0, then it returns case1, else it returns case2.
* Behavior is similar to PHP's: <b>return b ? case1 : case2;</b><br />
* If b==0 then case1 will not be Evaluated, and vice versa.
* Example: IF(HEIGHT, 3/HEIGHT, 3) will make sure 3/HEIGHT does not cause division by zero.
* <br /><br />
* Predefined functions that take no parameters are:
* RND: RND() function generates a random number (double value) between 0 and 1.
* <br /><br />
* STR: STR(123) function returns the string representation of the passed value: "123".
* <br /><br />
* SUBSTR: SUBSTR("Hello", 1,3) function returns the substring just like PHP substr function.
* The first parameter is the string, the second parameter is which index (0 based) to start copying,
* and the last parameter is the number of characters to copy. For example, SUBSTR("Hello", 1,3) returns "ell".
* <br /><br />
* STRLEN: STRLEN("abc") function returns the length of the string parameter. For example, for "abc" it returns 3.
* <br /><br />
* CONCAT: CONCAT("abc","def",...) function returns the concatanated strings: "abcdef".
* There is no preset limit on the number of parameters.
* <br /><br />
* TRIM: TRIM(" abc ") function returns the trimmed version of the string parameter: " abc " -> "abc".
* <br /><br />
* SUM: SUM(2,3,5,...) functions returns the sum of it's arguments. There is no preset limit on the number of parameters.
* <br /><br />
* @since 1.0.0
*/
class MathParser
{
/**
* protected variable that holds the expression to parse.
* @var string
* @since 1.0.0
*/
protected string $Expression;
/**
* non-public flag that shows if parsing is needed or not. if false, existing
* parse tree will be re-used.
* @var boolean
* @since 1.0.0
*/
protected bool $Dirty;
/**
* when optimization on, after the parse tree is compiled, it will be further optimized
* by removing sub branches that evaluate to a constant. This will make parse() operation
* slower and getValue() operation faster.
* @var boolean
* @since 1.0.0
*/
protected bool $OptimizationOn;
/**
* Starting node for the internal compiled representation of the
* expression in parsed form.
* @var $Node
* @since 1.0.0
*/
protected $Node;
/**
* internal list of variables that are available for use in an expression.
* @var array
* @since 1.0.0
*/
protected array $Variables;
/**
* internal list of functions that take more than 2 parameters that are available for use in an expression.
* @var array
* @since 1.0.0
*/
protected array $Functions;
/**
* A dictionary that holds translated messages.
* @var array
* @since 1.0.0
*/
static protected array $translator;
/**
* Does the expression allows string literals or not?
* For example, is an expression like this allowed: "Hello"+"World"
* @var boolean
* @since 1.0.0
*/
protected bool $StringLiteralsAllowed = true;
/**
* The parser can use ampersand(&) or the plus(+) operator for string concatenations.
* You can set which one you want using StrConcatOperator property.
* The default is ampersand(&). Valid values are STR_CONCAT_OP.PLUS or
* STR_CONCAT_OP.AMPERSAND.
* @var string
* @since 1.0.0
*/
protected string $StrConcatOperator;
/**
* Public for performance reasons.
* @see getVariableResolver function for more info.
* @var string|null
* @since 1.0.0
*/
public ?string $VariableResolver = null;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $add__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $subtract__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $mult__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $div__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $power__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $mod__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $unaryadd__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $negate__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $not__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $notequals__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $equals__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $lt__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $gt__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $ltequals__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $gtequals__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $and__;
/**
* @ignore
* @since 1.0.0
*/
static protected Mathparser_ParserFunction $or__;
/**
* Implementation of plus (+) operator.
* @ignore
* @since 1.0.0
*/
protected Mathparser_ParserFunction $AddOp;
/**
* Implementation of ampersand (&) operator for strings.
* @ignore
* @since 1.0.0
*/
protected Mathparser_ParserFunction $AndOp;
/**
* VariableResolver is used as a callback function that returns the values of variables when asked.
* This is useful when a variable cannot be defined before the parse operation.
* If VariableResolver property is set, it will be used to resolve variables that were not defined
* at the time parse operation has started. This means that syntax errors regarding undefined variables
* will not be caught at parse time. This VariableResolver (user implementation) will decide whether
* a variable name is valid or not.
* This is useful when for example the value comes from a database lookup. In some cases,
* the problem domain is too big to define the variables ahead of time. In such cases, it makes sense
* to parse with the assumption that it is a valid variable and resolve it's value when needed on demand during
* evaluation.
* @return string|null
* @since 1.0.0
*/
public function getVariableResolver(): ?string
{
return $this->VariableResolver;
}
/**
* Constructor for the MathParser.
* @throws Exception
* @since 1.0.0
*/
public function __construct()
{
$this->Expression = '';
$this->Node = null;
$this->Dirty = true; //means it is not Parsed yet.
$this->OptimizationOn = false;
$this->Variables = array();
$this->Functions = array();
//Comment out if you don't want these functions and variables:
$this->createDefaultFuncs();
$this->createDefaultVars();
if (!isset(self::$add__)) {
self::$add__ = new Mathparser_ParserFunction('+', 'mp_add_', 2);
self::$subtract__ = new Mathparser_ParserFunction('-', 'mp_subtract_', 2);
self::$mult__ = new Mathparser_ParserFunction('*', 'mp_mult_', 2);
self::$div__ = new Mathparser_ParserFunction('/', 'mp_div_', 2);
self::$power__ = new Mathparser_ParserFunction('/', 'mp_power_', 2);
self::$mod__ = new Mathparser_ParserFunction('/', 'mp_mod_', 2);
self::$notequals__ = new Mathparser_ParserFunction('!=', 'mp_notequals_', 2);
self::$equals__ = new Mathparser_ParserFunction('=', 'mp_equals_', 2);
self::$lt__ = new Mathparser_ParserFunction('<', 'mp_ltequals_', 2);
self::$gt__ = new Mathparser_ParserFunction('>', 'mp_gtequals_', 2);
self::$ltequals__ = new Mathparser_ParserFunction('<=', 'mp_ltequals_', 2);
self::$gtequals__ = new Mathparser_ParserFunction('>=', 'mp_gtequals_', 2);
self::$unaryadd__ = new Mathparser_ParserFunction('+', 'mp_unaryadd_', 1);
self::$negate__ = new Mathparser_ParserFunction('-', 'mp_negate_', 1);
self::$not__ = new Mathparser_ParserFunction('!', 'mp_not_', 1);
self::$and__ = new Mathparser_ParserFunction('&', 'mp_and_', 2);
self::$or__ = new Mathparser_ParserFunction('|', 'mp_or_', 2);
}
$this->AddOp = self::$add__;
$this->AndOp = self::$and__;
$this->StrConcatOperator = '&'; //default.
}
/**
* Returns the existing translatable strings used to report error messages.
* To use this component in a localized application, set the translated versions of
* strings for each value that exists in the hashtable. By default, same hashtable is shared
* between all instances of the parser. To change translation strings, set a new hashtable
* of your own for the parser instance instead of editing the values contained in this hashtable if
* you do not wish other parser instances not to be effected.
* Returns dictionary (array) of key/value pairs where value is a Locale dependent, typically translated String, that
* may contain placeholder parameters using %s.
* @return array
* @since 1.0.0
*/
public static function &getTranslationStrings(): array
{
if (!isset(self::$translator)) {
self::$translator = array();
//#Strings used by the math Parser.
self::$translator['ExpEmpty'] = 'Expression is empty.';
//#parameter is the name of a variable:
self::$translator['VarNtExst'] = 'Variable %s does not exist.';
//variable is not numeric:
self::$translator['VarNtDbl'] = 'Variable %s is not numeric.';
//variable is not a string:
self::$translator['VarNtStr'] = 'Variable %s is not a string.';
//variable is not a string or a double:
self::$translator['VarNtDblStr'] = 'Variable %s should be a Double or a String. It is {1}.';
//variable already exists:
self::$translator['VarExt'] = 'Variable %s already exists.';
//#parameter is the name of a variable:
self::$translator['NtVarNm'] = '%s is not a valid variable name.';
//#parameter is the math expression that failed to Parse:
self::$translator['SntxErr'] = 'Syntax error in expression %s';
//#parameter is a function name:
self::$translator['NtFncNm'] = '%s is not a valid function name.';
//#parameter is a function name:
self::$translator['FncExst'] = 'Function %s already exists.';
//#Parameters are function name and number of parameters:
self::$translator['WrngNPrms'] = 'Function %s must accept at least 0 parameters not %s.';
self::$translator['WrngNPrms2'] = 'Function %s requires at least %s parameters.';
//#sub expression is not valid:
self::$translator['ExpNtVld'] = 'Sub expression "%s" in "%s" is not valid.';
self::$translator['InvNPrm'] = 'Invalid number of parameters in "%s".';
//#bracket mismatch.
self::$translator['BrcktMis'] = 'Bracket mismatch in expression "%s" at index %s.';
//#missing brackets.
self::$translator['MisBrckt'] = 'Missing bracket ")" in expression "%s".';
//applying logical and & to string values:
self::$translator['InvConcatOper'] = 'Parameter #2 %s is not a numeric value. Cannot apply logical and (&) operator. If you want to concatenate 2 strings, use STR() function to convert first parameter to string.';
//applying logical and + to string values:
self::$translator['InvConcatOper2'] = 'Parameter #2 %s is not a numeric value. Cannot apply plus (+) operator. If you want to concatenate 2 strings, use STR() function to convert first parameter to string.';
//converting str to number:
self::$translator['InvNum'] = 'Cannot convert to a number. %s is not a numeric value.';
}
return self::$translator;
}
/**
* Variable property is a way to set and get variable values.
* setVariable function creates the variable if the variable does not exist.
* Variable name is not case sensitive. Throws exception if the variable needs
* to be created and the name is not a valid variable name. CreateVar is just an
* alias for this method.
* @param $varName - Name of the variable whose value is being set.
* @param $newVal - New value of the variable. It should be either a Double or a String.
* @param $fn_valueProvider - The delegate that returns the value for a variable when asked by name.
* When a $fn_valueProvider is set for a Variable, it's assigned value will not be used and instead the $fn_valueProvider of type
* <br /><br /> function VariableDelegate($parser, $varName);<br /><br />
* will be called. This is useful in cases when the value of a variable comes from a database and it is not easy to set it's value
* (or it is costly) ahead of time. So, when you want to compute the variable value only when it is needed, then you can register a $fn_valueProvider
* to be called. You can use the same $fn_valueProvider for all or some of your variables or you can have a different valueProvider for each variable.
* @return void
* @throws Exception
* @since 1.0.0
*/
public function setVariable($varName, $newVal, $fn_valueProvider = null): void
{
if ($varName == null) {
throw new Exception("Variable name cannot be null.");
}
if (is_numeric($newVal)) {
$newVal = (float)$newVal;
} else
if (!is_string($newVal)) {
throw new Exception("Variable should be floating point or string value: " . $newVal);
}
$upcName = strtoupper($varName);
$existing = $this->Variables[$upcName];
if ($existing != null) {
$existing->Value = $newVal;
} else {
if (!$this->isValidName($upcName)) {
throw new Exception($this->getMessage1("NtVarNm", $varName));
}
//create the variable:
$var = new Mathparser_Variable($this, $upcName, $newVal, $fn_valueProvider);
$this->Variables[$upcName] = $var;
$this->Dirty = true;
}
}
/**
* Evaluates the expression and returns the result of it. If it cannot be parsed or
* evaluated then this method throws Exception.
* <br /><br />
* Calling this method is identical to calling getValue()
* @return double|string - Returns the value of the parsed expression.
* The return type is an object that can be interpreted as either Double or String.
* @throws Mathparser_ParserException
* @since 1.0.0
*/
public function evaluate(): float|string
{
if ($this->Dirty) { //if the expression has been changed, we need to Parse it again
$this->parse();
}
return $this->Node->getValue(); //this will start the chain reaction to get the
//value of all nodes
}
/**
* Parses the expression and forms a parse tree. Throws Exception if it cannot parse.
* Upon successful completion of parsing, it will set the Dirty flag to false, so that
* unless the expression is changed or variables and functions added or removed,
* expression does not need to be re-parsed. Users may want to call the parse method
* directly to check the validity of an input expression using a try-except block.
* <br /><br />
* If OptimizationOn property is true, Parse method will optimize the parse tree by
* evaluating constant branches of the parse tree at that moment, so that Evaluate
* function will run faster.
* @return void
* @throws Mathparser_ParserException
* @throws Exception
* @since 1.0.0
*/
public function parse(): void
{
if (!isset($this->Expression) || !(strlen($this->Expression) > 0)) {
$this->Node = null;
throw new Exception($this->getMessage("ExpEmpty"));
}
$formula = $this->Expression;
$this->upperCase($formula);
//echo $formula;
$len = strlen($formula);
$brackets = self::checkBrackets($formula);
if ($brackets > -1 && $brackets < $len) {
throw new Mathparser_ParserException($this->getMessage2("BrcktMis", $formula, $brackets), substr($formula, $brackets), $formula);
} else
if ($brackets == 'len') {
throw new Mathparser_ParserException($this->getMessage1("MisBrckt", $formula), $formula, $formula);
}
if (($this->Node = $this->createParseTree($formula)) == null) {
throw new Mathparser_ParserException($this->getMessage2("ExpNtVld", $formula, $formula), $formula, $formula);
}
if ($this->OptimizationOn) {
$this->optimize(); //will make sure FNode tree is lean and mean
}
$this->Dirty = false; //note that we Parsed it once. Unless the expression is changed we do not need to reParse it.
}
/**
* Uppercases the expression in char array form without touching the string literals.
* @param $c - to convert to uppercase.
* @return void
* @since 1.0.0
*/
private function upperCase(&$c): void
{
$len = strlen($c);
$insideStringConst = false;
for ($i = 0; $i < $len; $i++) {
$ch = $c[$i];
if ($ch == '"')//support string literals in the expression.
{
$insideStringConst = !$insideStringConst;
continue;
}
if (!$insideStringConst) {
$c[$i] = strtoupper($ch);
}
}
}
/**
* Creates a variable with given name and initial value.
* If the variable already exists, sets it's value to the specified value.
* Throws Exception if $varName is not a valid variable name.
* @param $varName - Variable name (case insensitive).
* @param $varValue - Initial value for the variable.
* @param $valueProvider - The callback delegate (a plain old PHP function) that decides the value of the variable at runtime.
* <br /><br /> function VariableDelegate($parser, $varName);<br /><br />
* @throws Exception
* @since 1.0.0
*/
public function createVar($varName, $varValue, $valueProvider = null): void
{
$this->setVariable($varName, $varValue, $valueProvider);
}
/**
* CreateFunc method creates a new function that takes n number of parameters (could be 0) in
* the parser's list of functions. If the function name already exists then this method throws Exception.
* Function name is not case sensitive.
* <br /><br />
* The second parameter is the name of the user defined function that is globally available:
* <br /><br />
* function funcName($parameter1 [, $parameter2,...]);<br />
* <br /><br />
* During evaluation of the expression, when the registered function name is
* encountered, the user supplied funcName will be called and variables will be passed.
* <br /><br />
* This user defined function
* will need to return a result (representing the value of the function)
* based on the parameters passed as an array of double values.
* If the number of parameters that a function takes is not known ahead of time,
* then the $paramCount parameter should be -1. Otherwise, it will be the
* number of parameters that will be passed to the function at runtime. Knowing the number of
* parameters ahead of time helps detect syntax errors during parsing. For example, if SIN(x) function
* descriptor did not tell us that it was taking only 1 parameter, then an expression like:
* SIN(x,y) would not cause syntax error during parsing, and if the implementation of SIN(x) did not check
* actual runtime number of parameters passed, then the problem could go undetected
* and the second y parameter could be ignored silently.
* @param $newFuncName - Name of the new function to create.
* @param $funcAddr - name of the PHP function that will be invoked when parser needs to execute this user defined function.
* @param $paramCount - number of parameters that this function takes. -1 means any number of parameters.
* @throws Exception
* @since 1.0.0
*/
public function createFunc($newFuncName, $funcAddr, $paramCount): void
{
if ($newFuncName == null) {
throw new Exception("Function name cannot be null.");
}
if ($funcAddr == null) {
throw new Exception("Function implementation cannot be null.");
}
$upcName = strtoupper($newFuncName);
if (!$this->isValidName($upcName)) { //must contain uppercase letters only
throw new Exception($this->getMessage1("NtFncNm", $newFuncName));
}
if ($this->isFunction($upcName)) {
throw new Exception($this->getMessage1("FncExst", $newFuncName));
} else if ($paramCount < -1)//-1 means num params unknown.
{
throw new Exception($this->getMessage2("WrngNPrms", $newFuncName, (string)$paramCount));
} else {
$func = new Mathparser_ParserFunction($upcName, $funcAddr, $paramCount);
//if newFuncName doesn't exist it is inserted:
$this->Functions[$upcName] = $func;
}
$this->Dirty = true; //previously bad expression may now be ok, we should reParse it
}
/**
* CreateDefaultFuncs method creates some predefined functions in the parser's list of functions.
* <br /><br />
* Predefined functions are:
* <br /><br />
* SQR: Square function which can be used as SQR(X)
* <br /><br />
* SIN: Sinus function which can be used as SIN(X), X is a real-type expression.
* Sin returns the sine of the angle X in radians.
* <br /><br />
* COS: Cosinus function which can be used as COS(X), X is a real-type expression.
* COS returns the cosine of the angle X in radians.
* <br /><br />
* ATAN: ArcTangent function which can be used as ATAN(X)
* Returns the arctangent of a number as a numeric value between -PI/2 and PI/2 radians.
* <br /><br />
* SINH: Sinus Hyperbolic function which can be used as SINH(X)
* <br /><br />
* COSH: Cosinus Hyperbolic function which can be used as COSH(X)
* <br /><br />
* COTAN: which can be used as COTAN(X)
* <br /><br />
* TAN: which can be used as TAN(X)
* <br /><br />
* EXP: which can be used as EXP(X)
* <br /><br />
* LN: natural log, which can be used as LN(X)
* <br /><br />
* LOG: 10 based log, which can be used as LOG(X)
* <br /><br />
* SQRT: which can be used as SQRT(X)
* <br /><br />
* ABS: absolute value, which can be used as ABS(X)
* <br /><br />
* SIGN: SIGN(X) returns -1 if X&lt;0; +1 if X&gt;0, 0 if X=0; it can be used as SQR(X)
* <br /><br />
* TRUNC: Discards the fractional part of a number. e.g. TRUNC(-3.2) is -3, TRUNC(3.2) is 3.
* <br /><br />
* CEIL: CEIL(-3.2) = 3, CEIL(3.2) = 4
* <br /><br />
* FLOOR: FLOOR(-3.2) = -4, FLOOR(3.2) = 3
* <br /><br />
* VAL: VAL("3.1") = 3.1 Returns the floating point numeric value of the string argument.
* <br /><br />
* Predefined functions that take two parameters are:
* <br /><br />
* POW: The Power function raises Base to any power. For fractional exponents or
* exponents greater than MaxInt, Base must be greater than 0.
* <br /><br />
* LOGN: The LogN function returns the log base N of X. Example: LOGN(10, 100) = 2
* <br /><br />
* MIN: MIN(2, 3) is 2.
* <br /><br />
* MAX: MAX(2, 3) is 3.
* <br /><br />
* IF: The IF(b, case1, case2) function provides branching capability.
* If b is not 0, then it returns case1, else it returns case2.
* Behavior is similar to PHP's: <b>return b ? case1 : case2;</b><br />
* If b==0 then case1 will not be Evaluated, and vice versa.
* Example: IF(HEIGHT, 3/HEIGHT, 3) will make sure 3/HEIGHT does not cause division by zero.
* <br /><br />
* Predefined functions that take no parameters are:
* RND: RND() function generates a random number (double value) between 0 and 1.
* <br /><br />
* STR: STR(123) function returns the string representation of the passed value: "123".
* <br /><br />
* SUBSTR: SUBSTR("Hello", 1,3) function returns the substring just like PHP substring function.
* The first parameter is the string, the second parameter is which index (0 based) to start copying,
* and the last parameter is the number of characters to copy. For example, SUBSTR("Hello", 1,3) returns "ell".
* <br /><br />
* STRLEN: STRLEN("abc") function returns the length of the string parameter. For example, for "abc" it returns 3.
* <br /><br />
* CONCAT: CONCAT("abc","def",...) function returns the concatanated strings: "abcdef".
* There is no preset limit on the number of parameters.
* <br /><br />
* TRIM: TRIM(" abc ") function returns the trimmed version of the string parameter: " abc " -> "abc".
* <br /><br />
* SUM: SUM(2,3,5,...) functions returns the sum of it's arguments. There is no preset limit on the number of parameters.
* <br /><br />
* User functions can be added using CreateFunc method.
* Functions and Variables can be deleted using DeleteVar, DeleteFunc,
* DeleteAllVars, DeleteAllFuncs methods.
* @throws Exception
* @since 1.0.0
*/
public function createDefaultFuncs(): void
{
$this->createFunc("SQR", 'mp_square_', 1);
$this->createFunc("SIN", 'sin', 1);
$this->createFunc("COS", 'cos', 1);
$this->createFunc("ATAN", 'atan', 1);
$this->createFunc("SINH", 'sinh', 1);
$this->createFunc("COSH", 'cosh', 1);
$this->createFunc("COTAN", 'mp_cotan_', 1);
$this->createFunc("TAN", 'tan', 1);
$this->createFunc("EXP", 'exp', 1);
$this->createFunc("LN", 'log', 1);
$this->createFunc("LOG", 'log10', 1);
$this->createFunc("SQRT", 'sqrt', 1);
$this->createFunc("ABS", 'abs', 1);
$this->createFunc("SIGN", 'mp_sign_', 1);
$this->createFunc("TRUNC", 'mp_trunc_', 1);
$this->createFunc("CEIL", 'ceil', 1);
$this->createFunc("FLOOR", 'floor', 1);
$this->createFunc("RND", 'mp_rand_', 0);
$this->createFunc("VAL", 'mp_float_', 1);
$this->createFunc("POW", 'pow', 2);
$this->createFunc("LOGN", 'mp_logn_', 2);
$this->createFunc("MIN", 'mp_min_', -1);
$this->createFunc("MAX", 'mp_max_', -1);
$this->createFunc("MOD", 'modulo', 2);
$this->createFunc("IF", 'if_', 3);
$this->createFunc("STRLEN", 'strlen', 1);
$this->createFunc("STR", 'strval', 1);
$this->createFunc("SUBSTR", 'substr', 3);
$this->createFunc("CONCAT", 'mp_concat_', -1);
$this->createFunc("TRIM", 'trim', 1);
$this->createFunc("RTRIM", 'rtrim', 1);
$this->createFunc("LTRIM", 'ltrim', 1);
$this->createFunc("CHR", 'chr', 1);
$this->createFunc("NUM", 'mp_num_', 1);
$this->createFunc("SUM", 'sum_', -1);
}
/**
* X, Y and PI variables can be predefined and can be immediately used in the expression.
* Initial values of X and Y are 0. PI is 3.14159265358979
* @since 1.0.0
*/
public function createDefaultVars(): void
{
try {
$this->createVar("PI", 3.14159265358979);
$this->createVar("X", 0.0);
$this->createVar("Y", 0.0);
} catch (Exception $e) {
//we know that these are valid names, so there won't be an exception.
Log::add("Pi, X or Y could not be created: " . $e->getMessage(), Log::WARNING,"com_sportsmanager");
}
}
/**
* Returns true if a function with the name 'funcName' is present in any of
* the current functions lists. Returns true if a function with the given name is defined. False otherwise.
* @param string $funcName - Name of the function in question.
* @return boolean
* @throws Exception
* @since 1.0.0
*/
public function isFunction(string $funcName): bool
{
if ($funcName == null) {
throw new Exception("Function name cannot be null.");
}
$funcName = strtoupper($funcName);
return isset($this->Functions[$funcName]);
}
/**
* Optimizes the parse tree by finding branches that evaluate to a constant and
* replacing them with a leaf representing the constant.
* Until the expression is changed and reparsed, further evaluation requests will be
* quicker.
* <br /><br />
* If the same expression will not be evaluated repeatedly with varying
* values of parameters used in it, then optimization will not bring any gain,
* but will slow performance.
* <br /><br />
* If OptimizationOn property is set to true, this method is called automatically when an
* evaluation is requested by calling Evaluate method or getValue() method.
* @ignore
* @since 1.0.0
*/
public function optimize(): void
{
$this->Node = self::optimizeNode($this->Node);
}
/**
* Value property is a read only property which returns the result of the expression.
* throws Exception if expression cannot be parsed. Return value is either Double or String.
* @return double|string
* @throws Mathparser_ParserException
* @since 1.0.0
*/
public function getValue(): float|string
{
return $this->evaluate();
}
//------------------------------------------------------------------------------
//END OF PUBLIC METHODS.
//------------------------------------------------------------------------------
/**
* @param $index
* @param $c
* @return bool
* @ignore
* Valid char definition for function and variable names.
* @since 1.0.0
*/
private function isValidChar($index, $c): bool
{
if ($index == 0) {
if (($c >= 'A' && $c <= 'Z')) {
return true;
}
if ($c == '_') {
return true;
}
return false;
}
if ((($c >= '0' && $c <= '9') || ($c >= 'A' && $c <= 'Z'))) {
return true;
}
if ($c == '_') {
return true;
}
return false;
}
/**
* Valid name definition for function and variable names.
* @param $name
* @return bool
* @ignore
* @since 1.0.0
*/
private function isValidName($name): bool
{
$len = strlen($name);
for ($i = 0; $i < $len; $i++) {
if (!$this->isValidChar($i, $name[$i])) {
return false;
}
}
return true;
}
//------------------------------------------------------------------------------
/**
* Makes sure number of opening brackets "(" are equal to
* the number of closing brackets ")" and they are consistent.
* @param $formula
* @return int
* @ignore
* @since 1.0.0
*/
protected static function checkBrackets($formula): int
{
//this function checks to see if the order and double of brackets are correct
//it will say ok if it sees something like 3+()()
$n = 0;
$len = strlen($formula);
$insideStringConst = false;
for ($i = 0; $i < $len; $i++) { //if length<1 loop won't execute
$ch = $formula[$i];
if ($ch == '\"')//support string literals in the expression.
{
$insideStringConst = !$insideStringConst;
continue;
}
if (!$insideStringConst) {
if ($ch == '(')
++$n;
else if ($ch == ')')
--$n;
if ($n < 0) return $i; //at any moment if expression is valid we cannot have more ) then (
}
}
return ($n == 0) ? -1 : $len; //true if double of brackets matches
}
//------------------------------------------------------------------------------
/**
* Removes unncessary outer brackets in an expression.
* @param $formula
* @return mixed|string
* @ignore
* @since 1.0.0
*/
protected static function removeOuterBrackets($formula): mixed
{
//has to be careful about (X+1)-(Y-1)
//should not remove the outer brackets here thinking that they are unnecessary
//but should remove when ((X+1)-(Y-1))
$temp = $formula;
$Len = strlen($temp);
while (($Len > 2) && ($temp[0] == '(') && ($temp[$Len - 1] == ')')) {
$temp = trim(substr($temp, 1, $Len - 2));
if (self::checkBrackets($temp) == -1) { //if we did not screw up then assign to the return value
$formula = $temp;
}
$Len = strlen($temp);
}
return $formula;
}
//------------------------------------------------------------------------------
/**
* Determines if a given string is a valid double number or not.
* If it is valid, the actual double value is returned out in the out parameter dblVal.
* @param $formula - Expression to parse.
* @param $dblVal - Output double value
* @ignore
* @since 1.0.0
*/
protected static function isValidDouble($formula, &$dblVal): bool
{
if (is_numeric($formula)) {
$dblVal = (float)$formula;
return true;
}
return false;
}
/**
* Returns the string literal if this formula is a string literal such as "abc".
* Escape character is " inside the string. Meaning, to have quotes inside the
* string it would be like this:
* "abc""defg".<br />
* Returns null if this is not a valid string literal.
* @param $formula - The sub expression in question.
* @return string|null the string that is inside the "" after "" escapes inside the string are processed. Null if the string is not valid.
* @ignore
* @since 1.0.0
*/
private static function getStringLiteral($formula): ?string
{
$len = strlen($formula);
if (!($len > 1 && $formula[0] == '"' && $formula[$len - 1] == '"')) {
return null;
}
$temp = substr($formula, 1, $len - 2);
if (self::checkEscapes($temp)) {
return str_replace('""', '"', $temp);
}
return null;
}
/**
* @param $formula
* @return boolean
* @ignore
* @since 1.0.0
*/
private static function checkEscapes($formula): bool
{
//this function checks to see if the escaped quotes match.
$len = strlen($formula);
$insideStringConst = false;
for ($i = 0; $i < $len; $i++) { //if length<1 loop won't execute
$ch = $formula[$i];
if ($ch == '"') //support string literals in the expression.
{
if ($insideStringConst) {
return false; //too many quotes.
}
++$i;
if ($i < $len && $formula[$i] == '"') {
$insideStringConst = true;
} else {
return false;
}
continue;
}
$insideStringConst = false;
}
return true;
}
//------------------------------------------------------------------------------
/**
* Compiles the parse tree of the given expression.
* @param $expToParse - Mathematical expression.
* @return mixed node of the parse tree.
* @throws Mathparser_ParserException
* @since 1.0.0
* @ignore
*/
protected function createParseTree($expToParse): mixed
{
$expToParse = trim($expToParse);
if (($len = strlen($expToParse)) == 0) {
return null;
}
$formula = $this->removeOuterBrackets($expToParse); //remove unnecessary brackets
if (strlen($formula) != $len) {
$formula = trim($formula);
if (strlen($formula) == 0) {
return null;
}
}
//is this text a simple double value?
$dblVal = null;
if ($this->isValidDouble($formula, $dblVal)) { //attach a double node in the structure
return new Mathparser_BasicNode($dblVal); //we create a double node and attach it to the *Node reference.
}
if ($this->StringLiteralsAllowed) {
$tempFormula = $this->getStringLiteral($formula);
if ($tempFormula != null) {
return new Mathparser_BasicNode($tempFormula); //we create a double node and attach it to the *Node reference.
}
}
//if it is not a simple double or a string, maybe it is a variable?
$varNode = $this->createVarNode($formula);
if ($varNode != null) {
return $varNode;
}
//if it is not a variable
$LastOper = $this->findLastOper($formula);
$funcAddr = null;
if (!($LastOper > 0)) //if it is 0 then it is a unary operation which is a one param function
{
$param = null;
if ($this->isOneParamFunc($formula, $funcAddr, $param, $LastOper)) {
if (($leftNode = $this->createParseTree($param)) == null) {
throw new Mathparser_ParserException($this->getMessage2("ExpNtVld", $param, $formula), $param, $formula);
}
if ($funcAddr != null) {
return new Mathparser_NParamNode(array($leftNode), $funcAddr);
}
}
}
$paramLeft = null;
$paramRight = null;
if ($this->isTwoParamFunc($formula, $funcAddr, $paramLeft, $paramRight, $LastOper)) {
if (($leftNode = $this->createParseTree($paramLeft)) == null) {
throw new Mathparser_ParserException($this->getMessage2("ExpNtVld", $paramLeft, $formula), $paramLeft, $formula);
}
if (($rightNode = $this->createParseTree($paramRight)) == null) {
throw new Mathparser_ParserException($this->getMessage2("ExpNtVld", $paramRight, $formula), $paramRight, $formula);
}
//if there is a function address returned then we use it, otherwise we use function name
if ($funcAddr != null) {
return new Mathparser_NParamNode(array($leftNode, $rightNode), $funcAddr);
}
}
$parms = null;
//if it is none of the above:
if ($this->isNParamFunc($formula, /*out*/ $funcAddr, /*out*/ $parms)) {
if (!isset($parms)) {
//We now allow functions with no parameters.
throw new Mathparser_ParserException($this->getMessage1("InvNPrm", $formula), $formula, $formula);
}
$nParam = count($parms);
$nodes = array();
for ($i = 0; $i < $nParam; $i++) {
if (($leftNode = $this->createParseTree($parms[$i])) == null) {
throw new Mathparser_ParserException($this->getMessage2("ExpNtVld", $parms[$i], $formula), $parms[$i], $formula);
}
$nodes[$i] = $leftNode;
}
//if there is a function address returned then we use it, otherwise we use function name
if ($funcAddr != null) {
return new Mathparser_NParamNode($nodes, $funcAddr);
}
}
//when code reaches here it means we did not return true so after compiling the expression.
return null;
}
//------------------------------------------------------------------------------
/**
* Finds the index of the last math operation in an expression.
* @param $formula - Math expression.
* @return int index of the last operand.
* @ignore
* @since 1.0.0
*/
private static function findLastOper($formula): int
{ //returns -1 if it cannot find anything
$Precedence = 13; //There are 12 operands and 13 is higher then all
$BracketLevel = 0; //shows the level of brackets we moved through
$Result = -1;
$Len = strlen($formula);
$lastWasOperator = 0;
$insideStringLiteral = false;
for ($i = 0; $i < $Len; $i++) //from left to right scan...
{
$current_ch = $formula[$i];
if ($current_ch == '"') {
$insideStringLiteral = !$insideStringLiteral;
$lastWasOperator = 0;
continue;
}
if ($insideStringLiteral) {
continue; //skip string literals.
}
if ($lastWasOperator > 2) {
return -1;
}
switch ($current_ch) {
case ')' :
--$BracketLevel; //counting bracket levels
$lastWasOperator = 0;
break;
case '(' :
++$BracketLevel;
$lastWasOperator = 0;
break;
case '|' :
if (!($BracketLevel > 0 || $lastWasOperator > 0))
if ($Precedence >= 1) {
$Precedence = 1;
$Result = $i;
}
++$lastWasOperator;
break;
case '&' :
if (!($BracketLevel > 0 || $lastWasOperator > 0))
if ($Precedence >= 2) {
$Precedence = 2;
$Result = $i;
}
++$lastWasOperator;
break;
case '!' :
if (!($BracketLevel > 0 || $lastWasOperator > 0))
if ($Precedence >= 3) {
$Precedence = 3;
$Result = $i;
}
++$lastWasOperator;
break;
case '=' :
if (!($BracketLevel > 0 || $lastWasOperator > 0))
if ($Precedence >= 4) {
$Precedence = 4;
$Result = $i;
}
if ($lastWasOperator > 0) {
$prevOperIndex = $i - $lastWasOperator;
if ($formula[$prevOperIndex] == '<' || $formula[$prevOperIndex] == '>') {
break; //skip incrementing lastWasOperator variable.
}
}
++$lastWasOperator;
break;
case '>' :
if (!($BracketLevel > 0 || $lastWasOperator > 0))
if ($Precedence >= 5) {
$Precedence = 5;
$Result = $i;
}
if ($lastWasOperator > 0) {
if ($formula[$i - $lastWasOperator] == '<') {
break;
}
}
++$lastWasOperator;
break;
case '<' :
if (!($BracketLevel > 0 || $lastWasOperator > 0))
if ($Precedence >= 5) {
$Precedence = 5;
$Result = $i;
}
++$lastWasOperator;
break;
case '-' :
if (!($BracketLevel > 0 || $lastWasOperator > 0)) //a main operation has to be outside the brackets
if ($Precedence >= 7) //seeking for lowest precedence
{
$Precedence = 7;
$Result = $i; //record the current index.
}
++$lastWasOperator;
break;
case '+' :
if (!($BracketLevel > 0 || $lastWasOperator > 0))
if ($Precedence >= 7) {
$Precedence = 7;
$Result = $i;
}
++$lastWasOperator;
break;
case '/':
case '*':
case '%' :
if (!($BracketLevel > 0 || $lastWasOperator > 0))
if ($Precedence >= 9) {
$Precedence = 9;
$Result = $i;
}
++$lastWasOperator;
break;
case '^' :
if (!($BracketLevel > 0 || $lastWasOperator > 0))
if ($Precedence >= 12) {
$Precedence = 12;
$Result = $i;
}
++$lastWasOperator;
break;
case 'E' :
if ($i > 0 && $lastWasOperator == 0) {
$ch = $formula[$i - 1];
if ($ch >= '0' && $ch <= '9') {//this E may be part of a number in scientific notation.
$j = $i;
while ($j > 0) { //trace back.
--$j;
$ch = $formula[$j];
if ($ch == '.' || ($ch >= '0' && $ch <= '9')) { //if it is not a function or variable name.
continue;
}
if ($ch == '_' || ($ch >= 'A' && $ch <= 'Z')) {//is it a func or var name?
$lastWasOperator = 0;
break; //break the while loop.
}
++$lastWasOperator; //it must be an operator or a paranthesis.
break; //break the while loop.
}
if ($j == 0 && ($ch >= '0' && $ch <= '9')) {
++$lastWasOperator;
}
} else {
$lastWasOperator = 0;
}
} else {
$lastWasOperator = 0;
}
break;
case ' ': //space:
break;
default :
$lastWasOperator = 0;
break;
}
}
return $Result;
}
//------------------------------------------------------------------------------
/**
* Determines if a given expression by itself is a function that takes 2 parameters.
* For example: and input of "MAX(x,y+1)" would return true.
* @param $formula - expression
* @param $funcAddr - The IFunction that this expression represents (if it is a function call)
* @param $paramLeft - Left parameter whose values will be sent out in a wrapper utility object.
* @param $paramRight - Right parameter group whose values will be sent out in a wrapper utility object.
* @param $CurrChar - current operand index in the expression
* @return true if formula is a valid two parameter function.
* @ignore
* @since 1.0.0
*/
private function isTwoParamFunc($formula, &$funcAddr, &$paramLeft, &$paramRight,
$CurrChar //gives the last operation index in the string
): bool
{
$funcAddr = null;
$paramLeft = null;
$paramRight = null;
$Len = strlen($formula);
if ($CurrChar > 0) //if function in question is an operand
{
if ($CurrChar > $Len - 2) {
return false;
}
$CurrCh = $formula[$CurrChar];
//was it an operand also? we want to find <>, >=, <=
if ($CurrCh == '<') {
$nextCh = $formula[$CurrChar + 1]; //look ahead.
if ($nextCh == '>') {
$funcAddr = self::$notequals__;
$paramLeft = substr($formula, 0, $CurrChar);
$paramRight = substr($formula, $CurrChar + 2);
} else if ($nextCh == '=') {
$funcAddr = self::$ltequals__;
$paramLeft = substr($formula, 0, $CurrChar);
$paramRight = substr($formula, $CurrChar + 2);
} else {
$funcAddr = self::$lt__; //default case.
$paramLeft = substr($formula, 0, $CurrChar);
$paramRight = substr($formula, $CurrChar + 1);
}
if (!(strlen($paramLeft) > 0)) {
return false;
}
if (!(strlen($paramRight) > 0)) {
return false;
}
return true; //all output is assigned, now we return true.
} else if ($CurrCh == '>') {
$nextCh = $formula[$CurrChar + 1];
if ($nextCh == '=') {
$funcAddr = self::$gtequals__;
$paramLeft = substr($formula, 0, $CurrChar);
$paramRight = substr($formula, $CurrChar + 2);
} else {
$funcAddr = self::$gt__; //default case.
$paramLeft = substr($formula, 0, $CurrChar);
$paramRight = substr($formula, $CurrChar + 1);
}
if (!(strlen($paramLeft) > 0)) {
return false;
}
if (!(strlen($paramRight) > 0)) {
return false;
}
return true; //all output is assigned, now we return true.
} else {
$paramLeft = substr($formula, 0, $CurrChar);
if (!(strlen($paramLeft) > 0)) {
return false;
}
$paramRight = substr($formula, $CurrChar + 1);
if (!(strlen($paramRight) > 0)) {
return false;
}
switch ($formula[$CurrChar]) {
//analytical operators:
case '+':
$funcAddr = $this->AddOp;
break;
case '-':
$funcAddr = self::$subtract__;
break;
case '*':
$funcAddr = self::$mult__;
break;
case '/':
$funcAddr = self::$div__;
break;
case '^':
$funcAddr = self::$power__;
break;
case '%':
$funcAddr = self::$mod__;
break;
//logical operators:
case '<':
$funcAddr = self::$lt__;
break;
case '>':
$funcAddr = self::$gt__;
break;
case '=':
$funcAddr = self::$equals__;
break;
case '&':
$funcAddr = $this->AndOp;
break;
case '|':
$funcAddr = self::$or__;
break;
}
}
return true; //all output is assigned, now we return true.
}
//if we reach here, result is false
//if main operation is not an operand but a function
$temp = '';
if ($formula[$Len - 1] == ')') //last character must be brackets closing function param list
{
$i = 0;
while ($this->isValidChar($i, $formula[$i])) {
$temp .= ($formula[$i]);
++$i;
}
while ($formula[$i] == ' ') {
++$i;
}
if (($formula[$i] == '(') && ($i < $Len - 1)) {
$func = $this->Functions[$temp];
if ($func != null && $func->paramCount == 2) {
$funcAddr = $func;
$paramStart = $i + 1;
$BracketLevel = 1;
$insideStringLiteral = false;
while (!($i > $Len - 1 - 1)) //last character is a ')', that's why we use i>Len-1
{
++$i;
switch ($formula[$i]) {
case '"':
$insideStringLiteral = !$insideStringLiteral;
break;
case '(':
if (!$insideStringLiteral) ++$BracketLevel;
break;
case ')':
if (!$insideStringLiteral) --$BracketLevel;
break;
case ',':
if ((!$insideStringLiteral) && (1 == $BracketLevel) && ($i < $Len - 2)) //last character is a ')', that's why we use i>Len-2
{
$paramLeft = substr($formula, $paramStart, $i - $paramStart);
$paramRight = substr($formula, $i + 1, $Len - 1 - ($i + 1)); //last character is a ')', that's why we use Len-1-i
return true; //we are sure that it is a two parameter function
}
break;
}
}
}
}
}
return false; //means we could not find it
}
//------------------------------------------------------------------------------
/**
* Determines if a given expression by itself is a function that takes 1 parameter.
* For example: and input of "SIN(x)" would return true.
* @param $formula - expression
* @param $funcAddr - The IFunction that this node represents.
* @param $param - the parameter that will be passed into this function.
* @param $CurrChar - current operand index in the expression
* @return true if formula is a valid one parameter function.
* @ignore
* @since 1.0.0
*/
private function isOneParamFunc($formula, &$funcAddr, &$param, $CurrChar): bool
{
$funcAddr = null;
$param = null;
$Len = strlen($formula);
if ($CurrChar == 0) //if function in question is an unary operand
{
$param = substr($formula, 1);
if (!(strlen($param) > 0)) {
return false;
}
switch ($formula[$CurrChar]) {
case '+':
$funcAddr = self::$unaryadd__;
break;
case '-':
$funcAddr = self::$negate__;
break;
case '!':
$funcAddr = self::$not__;
break;
default:
return false; //only + and - can be unary operators
}
return true; //all output is assigned, now we exit.
}
//if we reach here, result is false
//if main operation is not an operand but a function
if ($formula[$Len - 1] == ')') //last character must be brackets closing function param list
{
$i = 0;
$temp = '';
while ($this->isValidChar($i, $formula[$i])) {
$temp .= ($formula[$i]);
++$i;
}
while ($formula[$i] == ' ') {
++$i;
}
if (($formula[$i] == '(') && ($i < $Len - 2)) {
$func = $this->Functions[$temp];
if ($func != null && $func->paramCount == 1) {
$funcAddr = $func;
$paramStart = $i + 1;
$param = substr($formula, $paramStart, $Len - 1 - $paramStart); //check example: SIN(30)
return true; //we are sure that it is a two parameter function
}
}
}
return false;
}
//------------------------------------------------------------------------------
/**
* Determines if a given expression by itself is a function that takes N parameters.
* For example: and input of "IF(x, 3/x, 3)" would return true.
* @param $formula - expression
* @param $funcAddr - the IFunction that this node represents.
* @param $parms - the parameters that this function takes in this expression.
* @return true if formula is a valid N parameter function.
* @ignore
* @since 1.0.0
*/
private function isNParamFunc($formula, &$funcAddr, &$parms): bool
{
$funcAddr = null;
$parms = null;
//if main operation is not an operand but a function
$Len = strlen($formula);
$temp = '';
if ($formula[$Len - 1] == ')') //last character must be brackets closing function param list
{
$i = 0;
while ($this->isValidChar($i, $formula[$i])) {
$temp .= ($formula[$i]);
++$i;
}
while ($formula[$i] == ' ') {
++$i;
}
if (($formula[$i] == '(') && ($i < $Len - 1)) {
//we first check if it is one param func or two param func,
//so here if it is a func, we know it is N param func.
$funcAddr = $this->Functions[$temp];
if ($funcAddr != null) {
$nParams = $funcAddr->paramCount;
if ($nParams > -1) {
$parms = array();
if ($nParams == 0) //a function that takes no param.
{
if ($formula[$i + 1] == ')') {
return true;
}
}
$paramStart = $i + 1;
$BracketLevel = 1;
$pIndex = 0;
$insideStringLiteral = false;
while (!($i > $Len - 1 - 1)) //last character is a ')', that's why we use i>Len-1
{
++$i;
switch ($formula[$i]) {
case '"':
$insideStringLiteral = !$insideStringLiteral;
break;
case '(':
if (!$insideStringLiteral) ++$BracketLevel;
break;
case ')':
if (!$insideStringLiteral) --$BracketLevel;
break;
case ',':
if ((!$insideStringLiteral) && (1 == $BracketLevel) && ($i < $Len - 2)) //last character is a ')', that's why we use i>Len-2
{
//must have at least 2 params for this part to work:
if ($nParams > -1 && !($pIndex < $nParams)) {
return false; //wrong number of parameters.
}
$parms[$pIndex++] = substr($formula, $paramStart, $i - $paramStart);
if ($pIndex == $nParams - 1) {
//assign the last one:
$parms[$pIndex] = substr($formula, $i + 1, $Len - 1 - ($i + 1));
return true;
}
$paramStart = $i + 1;
}
break;
}
}
} else {
$list = array();
$paramStart = $i + 1;
$BracketLevel = 1;
$insideStringLiteral = false;
while (!($i > $Len - 1 - 1)) //last character is a ')', that's why we use i>Len-1
{
++$i;
switch ($formula[$i]) {
case '"':
$insideStringLiteral = !$insideStringLiteral;
break;
case '(':
if (!$insideStringLiteral) ++$BracketLevel;
break;
case ')':
if (!$insideStringLiteral) --$BracketLevel;
break;
case ',':
if (!$insideStringLiteral && (1 == $BracketLevel) && ($i < $Len - 2)) //last character is a ')', that's why we use i>Len-2
{
$list[] = substr($formula, $paramStart, $i - $paramStart);
$paramStart = $i + 1;
}
break;
}
}
//add the remaining:
$remaining = trim(substr($formula, $paramStart, $Len - 1 - $paramStart));
if (strlen($remaining) > 0) {
$list[] = $remaining;
}
$parms = $list;
return true;
}
}
}
}
return false; //means we could not find it
}
//------------------------------------------------------------------------------
/**
* Optimizes a compiled tree where root is the given Node.
* @param $aNode - Root node.
* @return mixed - Starting root node of the optimized tree.
* @ignore
* @since 1.0.0
*/
static function optimizeNode($aNode): mixed
{
$aNode->optimize();
//Below code changes the value of the Node pointer,
//thus it cannot be done inside a polymorphic method of the
//class instance pointed to by that pointer.
//Therefore, "instanceof" is required.
if ($aNode instanceof Mathparser_NParamNode) {
$count = count($aNode->nodes);
for ($i = 0; $i < $count; $i++) {
//if any node is not constant, just return as is:
if (!($aNode->nodes[$i] instanceof Mathparser_BasicNode)) {
return $aNode;
}
}
//if all parameters of the function are constants (basic nodes):
return new Mathparser_BasicNode($aNode->getValue());
}
return $aNode;
}
//------------------------------------------------------------------------------
/**
* Expression property represents the mathematical expression which is input to be Evaluated by the user.
*
* The expression can contain variables such as X, Y, T, HEIGHT, WEIGHT and so on.
* The values of these constants are set before the expression is evaluated.
* The values of the variables can be set before the expression is evaluated or the values can be provided
* during evaluation using a PHP function registered as $VariableResolver.
* @return string
* @since 1.0.0
*/
public function getExpression(): string
{
return $this->Expression;
}
/**
* @param $value
* @return void
* @see getExpression
* @since 1.0.0
*/
public function setExpression($value): void
{
if ($value != $this->Expression) {
$this->Expression = $value;
$this->Dirty = true;
}
}
/**
* This method can be overridden to return a Node that is fancy enough to lookup
* values from a database etc to come up with variable values at evaluation time.
* @param string $varName
* @return Mathparser_UnknownVarNode|Mathparser_VarNode|null
* @since 1.0.0
*/
protected function createVarNode(string $varName): Mathparser_UnknownVarNode|null|Mathparser_VarNode
{
//if it is not a simple double, maybe it is a variable?
$variable = $this->Variables[$varName];
if ($variable != null) {
return new Mathparser_VarNode($variable); //recursion will end on these points when we get to the basics
} else if ($this->VariableResolver != null) {
$Len = strlen($varName);
for ($i = 0; $i < $Len; $i++) {
$ch = $varName[$i];
if (!$this->isValidChar($i, $ch)) {
return null;
}
}
return new Mathparser_UnknownVarNode($this, $varName);
}
return null;
}
/**
* Translation support.
* @ignore
* @since 1.0.0
*/
static function getMessage($key)
{
try {
if (self::$translator == null) {
self::getTranslationStrings();
}
$s = self::$translator[$key];
if ($s == null) {
$s = $key;
}
return $s;
} catch (Exception $e) {
Log::add("Could not translate: " . $e->getMessage(), Log::WARNING,"com_sportsmanager");
return $key;
}
}
/**
* Translation support.
* @ignore
* @since 1.0.0
*/
static function getMessage1($key, $param): string
{
$temp = self::getMessage($key);
//assuming {0} is not used in the real string,
//and it is only a placeholder for the first parameter.
return sprintf($temp, $param);
}
/**
* Translation support.
* @ignore
* @since 1.0.0
*/
static function getMessage2($key, $param0, $param1 = ''): string
{
$temp = self::getMessage($key);
return sprintf($temp, $param0, $param1);
}
} //End of Math Parser Implementation.
/**
* Internal support class to help internal representation of a variable and a constant.
* @ignore
* @since 1.0.0
*/
class Mathparser_Variable
{
/**
* The parent parser instance, used to call the VariableResolver if needed.
* @since 1.0.0
*/
private $Parser;
/**
* Name of this variable.
* @since 1.0.0
*/
public string $Name;
/**
* The preset value of this variable.
* @since 1.0.0
*/
public float|string $Value;
/**
* The delegate function name (string value) that supplies variable values.
* If this is not null, then it is called to get
* the value for this variable.
* <br /><br /> function VariableResolver($parser, $varName);<br /><br />
* @since 1.0.0
*/
public $ValueProvider;
/**
* If the $this->ValueProvider is set, then this method uses it to get the runtime value.
* Otherwise, this method returns the value of $this->Value;
* @since 1.0.0
* @noinspection PhpUnused
*/
public function getRuntimeValue(){
return !is_callable($this->ValueProvider) ? $this->Value : ($this->ValueProvider)($this->Parser, $this->Name);
}
public function __construct($parser, $aName, $newVal, $valueProvider)
{
$this->Parser = $parser;
$this->Name = $aName;
$this->Value = $newVal;
$this->ValueProvider = $valueProvider;
}
}
/**
* Internal support class to represent a user defined function.
* User defined functions can be used in expressions.
* @ignore
* @since 1.0.0
*/
class Mathparser_ParserFunction
{
/**
* $EventHandler that is the name of the actual PHP function to call.
* @since 1.0.0
*/
public $EventHandler;
/**
* Name of the user defined function as used in an expression.
* @since 1.0.0
*/
public string $Name;
/**
* Number of parameters that the user defined function accepts.
* -1 means any number of parameters.
* 0 means no parameters, 1 means 1 parameter like SIN(X) etc.
* @since 1.0.0
*/
public int $paramCount;
/**
* Constructor.
* @param $name - Name of the user defined function as used in an expression.
* @param $eventHandler - is the name of the actual PHP function to call.
* @param $paramCount - Number of parameters that the user defined function accepts.
* @since 1.0.0
*/
public function __construct($name, $eventHandler, $paramCount)
{
$this->EventHandler = $eventHandler;
$this->Name = $name;
$this->paramCount = $paramCount;
}
}
/**
* Internal support class that represents constants such as 3, 5, 7 in the formula.
* @ignore
* @since 1.0.0
*/
class Mathparser_BasicNode extends Mathparser_Node
{
public $Value;
public function __construct($Val)
{
$this->Value = $Val;
}
public function getValue()
{
return $this->Value;
}
/**
* @throws Exception
* @since 1.0.0
*/
public function getValueAsDouble(): float
{
if (is_float($this->Value)) {
return $this->Value;
} else if (is_numeric($this->Value)) {
return (float)$this->Value;
}
throw new Exception("Value is not numeric: " . $this->Value);
}
public function getValueAsString(): string
{
return (string)$this->Value;
}
//------------------------------------------------------------------------------
public function isVariableUsed($Name): bool
{
return false; //a basic node does not store any variable or function info
}
//------------------------------------------------------------------------------
public function isFunctionUsed($Name): bool
{
return false; //a basic node does not store any variable or function info
}
//------------------------------------------------------------------------------
//since basic node cannot be Optimized further, this function does nothing
public function optimize()
{
//Optimize Evaluates constant values at compile time.
//do nothing.
}
}
/**
* Internal support class that represents N parameter functions such as IF(X, Y, Z) etc.
* @ignore
* @since 1.0.0
*/
class Mathparser_NParamNode extends Mathparser_Node
{
public array $nodes; //array of nodes (these are the parameters).
public $fPtr; //function to execute
public function __construct($n, $FuncAddr)
{
$this->nodes = &$n;
$this->fPtr = $FuncAddr;
}
public function getValue()
{
if ($this->fPtr->Name == 'IF') {
//special case for short circuit boolean logic.
//helps to avoid div by zero etc.
//assume there are 3 parameters:
$cond = $this->nodes[0]->getValue();
if ($cond == 0) { //false:
return $this->nodes[2]->getValue();
} else {
return $this->nodes[1]->getValue();
}
} else {
$p = array();
$count = count($this->nodes);
for ($i = 0; $i < $count; $i++) {
$p[] = $this->nodes[$i]->getValue();
}
return call_user_func_array($this->fPtr->EventHandler, $p);
}
}
/**
* @throws Exception
* @since 1.0.0
*/
public function getValueAsDouble(): float
{
$Value = $this->getValue();
if (is_float($Value)) {
return $Value;
} else if (is_numeric($Value)) {
return (float)$Value;
}
throw new Exception("Value is not numeric: " . $Value);
}
public function getValueAsString(): string
{
return (string)$this->getValue();
}
public function isFunctionUsed($Name): bool
{
if ($this->fPtr->Name == $Name) {
return true;
}
foreach ($this->nodes as $n) {
if ($n->isFunctionUsed($Name)) {
return true;
}
}
return false;
}
public function isVariableUsed($Name): bool
{
foreach ($this->nodes as $n) {
if ($n->isVariableUsed($Name)) {
return true;
}
}
return false;
}
public function optimize(): void
{
$i = 0;
foreach ($this->nodes as $n) {
$this->nodes[$i] = MathParser::optimizeNode($n);
$i++;
}
}
}
/**
* Internal support class that represents a variable Node.
* @ignore
* @since 1.0.0
*/
class Mathparser_VarNode extends Mathparser_Node
{
private $pVar; //address of the variable in the variable list
public function __construct($variable)
{
$this->pVar = $variable;
}
public function getValue()
{
return $this->pVar->getRuntimeValue();
}
/**
* @throws Exception
* @since 1.0.0
*/
public function getValueAsDouble(): float
{
$Value = $this->pVar->getRuntimeValue();
if (is_float($Value)) {
return $Value;
} else if (is_numeric($Value)) {
return (float)$Value;
}
throw new Exception("Value is not numeric: " . $Value);
}
public function getValueAsString(): string
{
return (string)$this->pVar->getRuntimeValue();
}
public function isVariableUsed($Name): bool
{
return $this->pVar->Name == $Name;
}
public function isFunctionUsed($Name): bool
{
return false;
}
//since there is no parameter used to get the value of a variable, no further
//optimization can be made.
public function optimize()
{ //Optimize Evaluates constant values at compile time.
//do nothing
}
}
/**
* Internal support class that represents a variable Node that was not defined before the parse.
* We will resolve it's value during evaluation.
* @ignore
* @since 1.0.0
*/
class Mathparser_UnknownVarNode extends Mathparser_Node
{
private string $VarName; //name of the variable that was defined on the fly.
private $MathParser;
public function __construct($parser, $varName)
{
$this->MathParser = $parser;
$this->VarName = $varName;
}
public function getValue()
{
return call_user_func($this->MathParser->VariableResolver, $this->MathParser, $this->VarName);
}
/**
* @throws Exception
* @since 1.0.0
*/
public function getValueAsDouble(): float
{
$Value = $this->getValue();
if (is_float($Value)) {
return $Value;
} else if (is_numeric($Value)) {
return (float)$Value;
}
throw new Exception("Value is not numeric: " . $Value);
}
public function getValueAsString(): string
{
return (string)$this->getValue();
}
public function isVariableUsed($Name): bool
{
return $this->VarName == $Name;
}
public function isFunctionUsed($Name): bool
{
return false;
}
//since there is no parameter used to get the value of a variable, no further
//optimization can be made.
public function optimize()
{ //Optimize Evaluates constant values at compile time.
//do nothing
}
}
/**
* Base class for expression tree nodes.
* @ignore
* @since 1.0.0
*/
abstract class Mathparser_Node
{
/**
* Returns the computed value of this Node.
* @since 1.0.0
*/
abstract public function getValue();
/**
* Returns the computed value of this Node as a double precision value.
* @since 1.0.0
*/
abstract public function getValueAsDouble();
/**
* Returns the value as a String.
* @since 1.0.0
*/
abstract public function getValueAsString();
/**
* Is this variable used in the expression under this node?
* @since 1.0.0
*/
abstract public function isVariableUsed($Name);
/**
* Is this function used in the expression under this node?
* @since 1.0.0
*/
abstract public function isFunctionUsed($Name);
/**
* Optimize Evaluates constant values at compile time (When parse() is called).
* Once this is done, Evaluate will run faster.
* @since 1.0.0
*/
abstract public function optimize();
}
/**
* ParserException is thrown by some methods of MathParser implementation if an
* expression cannot be parsed. These methods are:
* parse(), evaluate(), getValue()
* User defined functions can choose to throw this exception too.
* @since 1.0.0
*/
class Mathparser_ParserException extends Exception
{
/**
* Member variable that holds the error portion of the expression.
* @since 1.0.0
*/
public $err;
/**
* Member variable that holds the parsed expression itself.
* @since 1.0.0
*/
public $exp;
/**
* Constructor.
* @since 1.0.0
*/
public function __construct($msg, $errPart, $expression)
{
parent::__construct($msg);
$this->err = $errPart;
$this->exp = $expression;
}
}
//Globals
/**
* @param $p1
* @param $p2
* @return number or string
* @ignore
* @since 1.0.0
*/
function mp_add_($p1, $p2)
{
return $p1 + $p2;
}
/**
* @param $p1
* @param $p2
* @return float|int|string
* @throws Exception
* @since 1.0.0
* @ignore
*/
function mp_add_str_($p1, $p2): float|int|string
{
if (is_string($p1)) {
return $p1 . $p2;
} else if (!is_numeric($p2)) {
throw new Exception(MathParser::getMessage1('InvConcatOper2', $p2));
}
return $p1 + $p2;
}
/**
* @param $p1
* @param $p2
* @return number
* @ignore
* @since 1.0.0
*/
function mp_subtract_($p1, $p2)
{
return $p1 - $p2;
}
/**
* @param $p1
* @param $p2
* @return float|int
* @ignore
* @since 1.0.0
*/
function mp_mult_($p1, $p2): float|int
{
return $p1 * $p2;
}
/**
* @param $p1
* @param $p2
* @return float|int
* @ignore
* @since 1.0.0
*/
function mp_div_($p1, $p2): float|int
{
if ($p2 == 0) {
Log::add("Division by zero detected in mp_div_: divisor was 0. Returning 0 as fallback.", Log::WARNING, "com_sportsmanager");
return 0;
}
return $p1 / $p2;
}
/**
* @param $p1
* @param $p2
* @return number
* @ignore
* @since 1.0.0
*/
function mp_power_($p1, $p2)
{
return pow($p1, $p2);
}
/**
* @param $p1
* @param $p2
* @return int
* @ignore
* @since 1.0.0
*/
function mp_mod_($p1, $p2): int
{
return $p1 % $p2;
}
/**
* @param $p1
* @param $p2
* @return int
* @ignore
* @since 1.0.0
*/
function mp_notequals_($p1, $p2): int
{
return ($p1 != $p2) ? 1 : 0;
}
/**
* @param $p1
* @param $p2
* @return int
* @ignore
* @since 1.0.0
*/
function mp_equals_($p1, $p2): int
{
return ($p1 == $p2) ? 1 : 0;
}
/**
* @param $p1
* @param $p2
* @return int
* @ignore
* @since 1.0.0
*/
function mp_lt_($p1, $p2): int
{
return ($p1 < $p2) ? 1 : 0;
}
/**
* @param $p1
* @param $p2
* @return int
* @ignore
* @since 1.0.0
*/
function mp_gt_($p1, $p2): int
{
return ($p1 > $p2) ? 1 : 0;
}
/**
* @param $p1
* @param $p2
* @return int
* @ignore
* @since 1.0.0
*/
function mp_ltequals_($p1, $p2): int
{
return ($p1 <= $p2) ? 1 : 0;
}
/**
* @param $p1
* @param $p2
* @return int
* @ignore
* @since 1.0.0
*/
function mp_gtequals_($p1, $p2): int
{
return ($p1 >= $p2) ? 1 : 0;
}
/**
* @param $p1
* @return number
* @ignore
* @since 1.0.0
*/
function mp_unaryadd_($p1)
{
return $p1;
}
/**
* @param $p1
* @return float|int
* @ignore
* @since 1.0.0
*/
function mp_negate_($p1): float|int
{
return -$p1;
}
/**
* @param $p1
* @return int
* @ignore
* @since 1.0.0
*/
function mp_not_($p1): int
{
return ((!$p1) == 0) ? 0 : 1;
}
/**
* @param $p1
* @param $p2
* @return int
* @ignore
* @since 1.0.0
*/
function mp_and_($p1, $p2): int
{
return (!($p1 == 0) && !($p2 == 0)) ? 1 : 0;
}
/**
* @param $p1
* @param $p2
* @return bool|string
* @throws Exception
* @ignore
* @since 1.0.0
*/
function mp_and_str_($p1, $p2): bool|string
{
if (is_string($p1)) {
return $p1 . $p2;
} else if (!is_numeric($p2)) {
throw new Exception(MathParser::getMessage1('InvConcatOper', $p2));
}
return $p1 && $p2;
}
/**
* @param $p1
* @param $p2
* @return int
* @ignore
* @since 1.0.0
*/
function mp_or_($p1, $p2): int
{
return (!($p1 == 0) || !($p2 == 0)) ? 1 : 0;
}
/**
* @return number
* @ignore
* @since 1.0.0
*/
function sum_(): int
{
$p = func_get_args();
$count = func_num_args();
$tot = 0;
for ($i = 0; $i < $count; $i++) {
$tot += $p[$i];
}
return $tot;
}
/**
* @return number
* @throws Mathparser_ParserException
* @since 1.0.0
* @ignore
*/
function mp_max_()
{
$p = func_get_args();
$count = func_num_args();
if ($count < 1) {
throw new Mathparser_ParserException(MathParser::getMessage2('WrngNPrms2', 'MAX', '1'), 'max', 'max');
}
return max($p);
}
/**
* @return number
* @throws Mathparser_ParserException
* @since 1.0.0
* @ignore
*/
function mp_min_()
{
$p = func_get_args();
$count = func_num_args();
if ($count < 1) {
throw new Mathparser_ParserException(MathParser::getMessage2('WrngNPrms2', 'MIN', '1'), 'min', 'min');
}
return min($p);
}
/**
* @param $cond
* @param $trueCase
* @param $falseCase
* @return number
* @ignore
* @since 1.0.0
*/
function if_($cond, $trueCase, $falseCase)
{
return $cond != 0 ? $trueCase : $falseCase;
}
/**
* @param $val
* @return float
* @throws Mathparser_ParserException
* @since 1.0.0
* @ignore
*/
function mp_num_($val): float
{
if (is_numeric($val)) {
return (float)$val;
}
throw new Mathparser_ParserException(MathParser::getMessage2('InvNum', $val), 'NUM', 'NUM');
}
/**
* @param $val
* @return float|int
* @ignore
* @since 1.0.0
*/
function mp_square_($val): float|int
{
return $val * $val;
}
/**
* @param $val
* @return float
* @ignore
* @since 1.0.0
*/
function mp_cotan_($val): float
{
return 1.0 / tan($val);
}
/**
* @param $val
* @return int
* @ignore
* @since 1.0.0
*/
function mp_sign_($val): int
{
if ($val > 0) return 1;
if ($val < 0) return -1;
return 0;
}
/**
* @param $val
* @return float|int
* @ignore
* @since 1.0.0
*/
function mp_trunc_($val): float|int
{
if ($val > 0) return floor($val);
if ($val < 0) return floor($val) + 1;
return 0;
}
/**
* @param $val
* @return float
* @ignore
* @since 1.0.0
*/
function mp_float_($val): float
{
return (float)$val;
}
/**
* @param $base
* @param $val
* @return float|int
* @ignore
* @since 1.0.0
*/
function mp_logn_($base, $val): float|int
{
return log($val) / log($base);
}
/**
* @return float|int
* @ignore
* @since 1.0.0
*/
function mp_rand_(): float|int
{
$max = getrandmax();
return rand(0, $max) / $max;
}
/**
* @return string
* @ignore
* @since 1.0.0
*/
function mp_concat_(): string
{
$p = func_get_args();
return implode('', $p);
}