added simple math and charts modules

This commit is contained in:
2024-12-10 23:53:23 +01:00
parent 62a5f3c97c
commit 688ff42752
196 changed files with 20285 additions and 8 deletions

7
vendor/andileco/eval-math/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
/.idea
/vendor
/composer.lock
.project
.settings
.buildpath
/build

21
vendor/andileco/eval-math/.travis.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
language: php
before_script:
- composer self-update
- composer config platform.php $PHP_VERSION
- composer install --prefer-source --no-interaction --dev
matrix:
fast_finish: true
include:
- php: '5.6'
env: PHP_VERSION=5.6
- php: '7.0'
env: PHP_VERSION=7.0
- php: '7.1'
env: PHP_VERSION=7.1
- php: '7.2'
env: PHP_VERSION=7.2
after_success:
- php vendor/bin/php-coveralls -v

25
vendor/andileco/eval-math/LICENSE vendored Normal file
View File

@@ -0,0 +1,25 @@
LICENSE
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1 Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

78
vendor/andileco/eval-math/README.md vendored Normal file
View File

@@ -0,0 +1,78 @@
Composer/Packagist version of EvalMath by Miles Kaufman
Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
NAME
----
EvalMath - safely evaluate math expressions
DESCRIPTION
-----------
Use the EvalMath class when you want to evaluate mathematical expressions
from untrusted sources. You can define your own variables and functions,
which are stored in the object. Try it, it's fun!
SYNOPSIS
--------
`$m = new EvalMath;`
`// basic evaluation:`
`$result = $m->evaluate('2+2');`
`// supports: order of operation; parentheses; negation; built-in functions`
`$result = $m->evaluate('-8(5/2)^2*(1-sqrt(4))-8');`
`// create your own variables`
`$m->evaluate('a = e^(ln(pi))');`
`// or functions`
`$m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');`
`// and then use them`
`$result = $m->evaluate('3*f(42,a)');`
`// use methods (calc functions)
`$m->evaluate('1+max(2,3)') // => 4`
`$m->evaluate('if(1=2, 2+2, 5+5') // => 10
METHODS
-------
`$m->evaluate($expr)`
Evaluates the expression and returns the result. If an error occurs,
prints a warning and returns false. If $expr is a function assignment,
returns true on success.
`$m->e($expr)`
A synonym for $m->evaluate().
`$m->vars()`
Returns an associative array of all user-defined variables and values.
`$m->funcs()`
Returns an array of all user-defined functions.
CALC METHODS (CALC FUNCTIONS)
-----------------------------
- `max(n...,m)` returns one of given arguments with maximal value
- `min(n...,m)` returns one of given arguments with minimal value
- `if(expr, true_value, false_value)` (has a `iif` synonym) returns `true_value` of `false_value` depends of `expr` evaluation result
- `round(n,m)` returns rounded n value with m precision
CREATE YOUR OWN CUSTOM CALC METHODS (CALC FUNCTIONS)
-----------------------------------------------
You can create custom classes, that implements custom calc methods.
TODO
----
- Improve documentation
- Ability to add custom operators
- More tests
CREDITS AND COPYRIGHTS
----------------------
This software integrates several libraries and patches by:
- Original EvalMath library, (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
- Modifications for 'calc functions' by Moodle, <https://github.com/moodle/moodle>
- Composer/Packagist version, (C) Daniel Bojdo, <https://github.com/dbojdo>

26
vendor/andileco/eval-math/composer.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"name" : "andileco/eval-math",
"description" : "EvalMath",
"license": "BSD-3-Clause",
"type" : "library",
"keywords" : [
"EvalMath"
],
"require": {
"php": "^7 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^5",
"php-coveralls/php-coveralls": "^2"
},
"autoload" : {
"psr-4" : {
"Andileco\\Util\\EvalMath\\" : "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./vendor/autoload.php">
<testsuites>
<testsuite name="EvalMath Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src/</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-clover" target="build/logs/clover.xml" />
</logging>
</phpunit>

View File

@@ -0,0 +1,511 @@
<?php
/**
*
*/
namespace Andileco\Util\EvalMath;
use Andileco\Util\EvalMath\Exception\AbstractEvalMathException;
use Andileco\Util\EvalMath\Exception\BuiltInFunctionRedefinitionException;
use Andileco\Util\EvalMath\Exception\ConstantAssignmentException;
use Andileco\Util\EvalMath\Exception\DivisionByZeroException;
use Andileco\Util\EvalMath\Exception\ExpectingTokenException;
use Andileco\Util\EvalMath\Exception\IllegalCharacterException;
use Andileco\Util\EvalMath\Exception\InternalErrorException;
use Andileco\Util\EvalMath\Exception\InvalidArgumentCountException;
use Andileco\Util\EvalMath\Exception\OperatorLacksOperandException;
use Andileco\Util\EvalMath\Exception\OperatorRequiredException;
use Andileco\Util\EvalMath\Exception\UndefinedVariableException;
use Andileco\Util\EvalMath\Exception\UndefinedVariableInFunctionDefinitionException;
use Andileco\Util\EvalMath\Exception\UnexpectedOperatorException;
use Andileco\Util\EvalMath\Exception\UnexpectedTokenException;
use Andileco\Util\EvalMath\Methods\Conditional;
use Andileco\Util\EvalMath\Methods\Maximum;
use Andileco\Util\EvalMath\Methods\Minimum;
use Andileco\Util\EvalMath\Methods\Round;
/**
* Class EvalMath
*/
class EvalMath
{
const NAME_PATTERN = '[a-z][a-z0-9_]*';
/**
* @deprecated
* @var bool
*/
public $suppress_errors = false;
/**
* @var string
*/
public $last_error = null;
/**
* @var array
*/
public $v = ['e'=>2.71,'pi'=>3.14]; // variables (and constants)
/**
* @var array
*/
public $f = []; // user-defined functions
/**
* @var array
*/
public $vb = ['e', 'pi']; // constants
/**
* @var array
*/
public $fb = [ // built-in functions
'sin','sinh','arcsin','asin','arcsinh','asinh',
'cos','cosh','arccos','acos','arccosh','acosh',
'tan','tanh','arctan','atan','arctanh','atanh',
'sqrt','abs','ln','log'
];
// Calc functions
// public $fc =['average'=>[-1], 'max'=>[-1], 'min'=>[-1], 'iif'=>[3]];
/** @var MethodsRegistry */
protected $fc;
public function __construct()
{
// make the variables a little more accurate
$this->v['pi'] = pi();
$this->v['e'] = exp(1);
$this->fc = (new MethodsRegistry)
->set(new Conditional)
->set((new Conditional)->setName('iif'))
->set(new Maximum)
->set(new Minimum)
->set(new Round);
}
/**
* @return MethodsRegistry
*/
public function getMethodsRegistry()
{
return $this->fc;
}
/**
* @param string $expr
* @return mixed
* @throws BuiltInFunctionRedefinitionException
* @throws ConstantAssignmentException
* @throws DivisionByZeroException
* @throws ExpectingTokenException
* @throws IllegalCharacterException
* @throws InternalErrorException
* @throws InvalidArgumentCountException
* @throws OperatorLacksOperandException
* @throws OperatorRequiredException
* @throws UndefinedVariableException
* @throws UndefinedVariableInFunctionDefinitionException
* @throws UnexpectedOperatorException
* @throws UnexpectedTokenException
*/
public function e($expr)
{
return $this->evaluate($expr);
}
/**
* @param string $expr
* @return mixed
* @throws BuiltInFunctionRedefinitionException
* @throws ConstantAssignmentException
* @throws DivisionByZeroException
* @throws ExpectingTokenException
* @throws IllegalCharacterException
* @throws InternalErrorException
* @throws InvalidArgumentCountException
* @throws OperatorLacksOperandException
* @throws OperatorRequiredException
* @throws UndefinedVariableException
* @throws UndefinedVariableInFunctionDefinitionException
* @throws UnexpectedOperatorException
* @throws UnexpectedTokenException
*/
public function evaluate($expr)
{
$this->last_error = null;
$expr = rtrim(trim($expr), ';'); // strip semicolons at the end
//===============
// is it a variable assignment?
if (preg_match('/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches)) {
if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant
throw new ConstantAssignmentException([':constant' => $matches[1]]);
}
if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) return false; // get the result and make sure it's good
$this->v[$matches[1]] = $tmp; // if so, stick it in the variable array
return $this->v[$matches[1]]; // and return the resulting value
//===============
// is it a function assignment?
} elseif (preg_match('/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) {
$fnn = $matches[1]; // get the function name
if (in_array($matches[1], $this->fb)) { // make sure it isn't built in
throw new BuiltInFunctionRedefinitionException([':function' =>$matches[1]]);
}
$args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments
if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix
for ($i = 0; $i<count($stack); $i++) { // freeze the state of the non-argument variables
$token = $stack[$i];
if (preg_match('/^[a-z]\w*$/', $token) and !in_array($token, $args)) {
if (array_key_exists($token, $this->v)) {
$stack[$i] = $this->v[$token];
} else {
throw new UndefinedVariableInFunctionDefinitionException([':token'=>$token]);
}
}
}
$this->f[$fnn] = ['args'=>$args, 'func'=>$stack];
return true;
//===============
} else {
return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
}
}
/**
* @return array
*/
public function vars()
{
$output = $this->v;
unset($output['pi'], $output['e']);
return $output;
}
/**
* @return array
*/
public function funcs()
{
$output = [];
foreach ($this->f as $fnn=>$dat)
$output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
return $output;
}
//===================== HERE BE INTERNAL METHODS ====================\\
/**
* Convert infix to postfix notation
*
* @param $expr
* @return array|bool
* @throws ExpectingTokenException
* @throws IllegalCharacterException
* @throws InternalErrorException
* @throws InvalidArgumentCountException
* @throws OperatorLacksOperandException
* @throws OperatorRequiredException
* @throws UnexpectedOperatorException
* @throws UnexpectedTokenException
*/
public function nfx($expr)
{
$index = 0;
$stack = new Stack();
$output = []; // postfix form of expression, to be passed to pfx()
// $expr = trim(strtolower($expr));
$expr = trim($expr);
$ops = ['+', '-', '*', '/', '^', '_', '%', '>', '<', '<=', '>=', '=='];
$ops_r = ['+' => 0, '-' => 0, '*' => 0, '/' => 0, '^' => 1, '%' => 0]; // right-associative operator?
$ops_p = ['+' => 0, '-' => 0, '*' => 1, '/' => 1, '_' => 1, '^' => 2, '%' => 1, '>' => 3, '<' => 3, '<=' => 3, '>=' => 3, '==' => 3]; // operator precedence
$expecting_op = false; // we use this in syntax-checking the expression
// and determining when a - is a negation
if (preg_match('/[^\%\w\s+*^\/()\.,-<>=]/', $expr, $matches)) { // make sure the characters are all good
throw new IllegalCharacterException([':character'=>$matches[0]]);
}
while(1) { // 1 Infinite Loop ;)
$op = substr($expr, $index, 2);
if (!in_array($op, $ops)) {
$op = substr($expr, $index, 1);
}
// find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
// $ex = preg_match('/^([A-Za-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr($expr, $index), $match);
$ex = preg_match('/^('.static::NAME_PATTERN.'\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr($expr, $index), $match);
//===============
if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
$stack->push('_'); // put a negation on the stack
$index++;
} elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
throw new IllegalCharacterException([':character'=>'_']);
//===============
} elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack?
if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
throw new OperatorRequiredException();
/*
* Implicit multiplication currently is not supported
*
$op = '*';
$index--; // it's an implicit multiplication
*/
}
// heart of the algorithm:
while($stack->count > 0 and ($o2 = $stack->last()) and in_array($o2, $ops) and ($ops_r[$op] ? $ops_p[$op] < $ops_p[$o2] : $ops_p[$op] <= $ops_p[$o2])) {
$output[] = $stack->pop(); // pop stuff off the stack into the output
}
// many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
$stack->push($op); // finally put OUR operator onto the stack
$index += strlen($op);
$expecting_op = false;
//===============
} elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last (
if (null === $o2) {
throw new UnexpectedTokenException(['token'=>')']);
} else {
$output[] = $o2;
}
}
if (preg_match("/^([A-Za-z]\w*)\($/", $stack->last(2) ?? '', $matches)) { // did we just close a function?
$fnn = $matches[1]; // get the function name
$arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
$fn = $stack->pop();
$output[] = ['fn' => $fn, 'fnn' => $fnn, 'argcount' => $arg_count]; // send function to output
if (in_array($fnn, $this->fb)) { // check the argument count
if ($arg_count > 1) {
throw new InvalidArgumentCountException();
}
} elseif (isset($this->fc[$fnn])) {
$counts = $this->fc[$fnn];
if (in_array(-1, $counts) and $arg_count > 0) {
} elseif (!in_array($arg_count, $counts)) {
throw new InvalidArgumentCountException();
}
} elseif (array_key_exists($fnn, $this->f)) {
if ($arg_count != count($this->f[$fnn]['args'])) {
throw new InvalidArgumentCountException();
}
} else { // did we somehow push a non-function on the stack? this should never happen
throw new InternalErrorException();
}
}
$index++;
//===============
} elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
while (($o2 = $stack->pop()) != '(') {
if (null === $o2) {
throw new UnexpectedTokenException([':token'=>',']);
} // oops, never had a (
else {
$output[] = $o2;
} // pop the argument expression stuff and push onto the output
}
// make sure there was a function
// if (!preg_match("/^([A-Za-z]\w*)\($/", $stack->last(2), $matches)) {
if (!preg_match('/^('.self::NAME_PATTERN.')\($/', $stack->last(2) ?? '', $matches)) {
throw new UnexpectedTokenException([':token'=>',']);
}
$stack->push($stack->pop()+1); // increment the argument count
$stack->push('('); // put the ( back on, we'll need to pop back to it again
$index++;
$expecting_op = false;
//===============
} elseif ($op == '(' and !$expecting_op) {
$stack->push('('); // that was easy
$index++;
$allow_neg = true;
//===============
} elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
$expecting_op = true;
$val = $match[1];
// if (preg_match("/^([A-Za-z]\w*)\($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
if (preg_match('/^('.static::NAME_PATTERN.')\($/', $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f) or isset($this->fc[$matches[1]])) { // it's a func
$stack->push($val);
$stack->push(1);
$stack->push('(');
$expecting_op = false;
} else { // it's a var w/ implicit multiplication
$val = $matches[1];
$output[] = $val;
}
} else { // it's a plain old var or num
$output[] = $val;
}
$index += strlen($val);
//===============
} elseif ($op == ')') { // miscellaneous error checking
if ($stack->last() != '(' or $stack->last(2) != 1) {
throw new UnexpectedTokenException([':token' => ')']);
}
// did we just close a function?
if (preg_match('/^(' . static::NAME_PATTERN . ')\($/', $stack->last(3) ?? '', $matches)) {
$stack->pop();// (
$stack->pop();// 1
$fn = $stack->pop();
$fnn = $matches[1]; // get the function name
$counts = $this->fc[$fnn];
if (!in_array(0, $counts)) {
throw new InvalidArgumentCountException([':given' => 0, ':accept' => $counts]);
}
$output[] = ['fn' => $fn, 'fnn' => $fnn, 'argcount' => 0]; // send function to output
$index++;
$expecting_op = true;
}
throw new UnexpectedTokenException([':token' => ')']);
} elseif (in_array($op, $ops) and !$expecting_op) {
throw new UnexpectedOperatorException([':operator'=>$op]);
} else { // I don't even want to know what you did to get here
throw new InternalErrorException();
}
if ($index == strlen($expr)) {
if (in_array($op, $ops)) { // did we end with an operator? bad.
throw new OperatorLacksOperandException([':operator' => $op]);
}
break;
}
while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace
$index++; // into implicit multiplication if no operator is there)
}
}
while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output
if ($op == '(') { // if there are (s on the stack, ()s were unbalanced
throw new ExpectingTokenException([':token' => ')']);
}
$output[] = $op;
}
return $output;
}
/**
* evaluate postfix notation
*
* @param $tokens
* @param array $vars
* @return bool|mixed|null
* @throws DivisionByZeroException
* @throws InternalErrorException
* @throws UndefinedVariableException
*/
public function pfx($tokens, $vars = [])
{
if ($tokens == false) {
return false;
}
$stack = new Stack();
foreach ($tokens as $token) { // nice and easy
if(is_array($token)) { // it's a function
$fnn = $token['fnn'];
$argcount = $token['argcount'];
if (in_array($fnn, $this->fb)) { // built-in function
if (is_null($op1 = $stack->pop())) {
throw new InternalErrorException();
}
$fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
if ($fnn == 'ln') {
$fnn = 'log';
}
eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval()
} elseif (($m = $this->fc->findByName($fnn))) {
$args = [];
for ($i = $argcount-1; $i >= 0; $i--) {
if (null === ($args[] = $stack->pop())) {
throw new InternalErrorException();
}
}
$res = $m->evaluate(...array_reverse($args));
if ($res === FALSE) {
throw new InternalErrorException();
}
$stack->push($res);
}elseif (array_key_exists($fnn, $this->f)) { // user function
// get args
$args = [];
for ($i = count($this->f[$fnn]['args'])-1; $i >= 0; $i--) {
if ( null === ($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) {
throw new InternalErrorException();
}
}
$stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
}
// if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
} elseif (in_array($token, ['+', '-', '*', '/', '^', '>', '<', '==', '<=', '>=', '%'], true)) {
if (is_null($op2 = $stack->pop())) {
throw new InternalErrorException();
}
if (is_null($op1 = $stack->pop())) {
throw new InternalErrorException();
}
switch ($token) {
case '+':
$stack->push($op1 + $op2);
break;
case '-':
$stack->push($op1 - $op2);
break;
case '*':
$stack->push($op1 * $op2);
break;
case '/':
if ($op2 == 0) {
throw new DivisionByZeroException();
}
$stack->push($op1 / $op2);
break;
case '^':
$stack->push(pow($op1, $op2));
break;
case '>':
$stack->push((int)($op1 > $op2));
break;
case '<':
$stack->push((int)($op1 < $op2));
break;
case '==':
$stack->push((int)($op1 == $op2));
break;
case '<=':
$stack->push((int)($op1 <= $op2));
break;
case '>=':
$stack->push((int)($op1 >= $op2));
break;
case '%':
$stack->push($op1 % $op2);
break;
}
// if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
} elseif ($token == "_") {
$stack->push(-1*$stack->pop());
// if the token is a number or variable, push it on the stack
} else {
if (is_numeric($token)) {
$stack->push($token);
} elseif (array_key_exists($token, $this->v)) {
$stack->push($this->v[$token]);
} elseif (array_key_exists($token, $vars)) {
$stack->push($vars[$token]);
} else {
throw new UndefinedVariableException();
}
}
} // when we're out of tokens, the stack should have a single element, the final result
if ($stack->count != 1) {
throw new InternalErrorException();
}
return $stack->pop();
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
use Exception;
class AbstractEvalMathException extends Exception implements EvalMathException
{
protected $error = '';
public function __construct(array $parameters=[])
{
$message = strtr($this->error, $parameters);
parent::__construct($message);
}
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class BuiltInFunctionRedefinitionException extends AbstractEvalMathException
{
protected $message = 'Cannot redefine built-in function :function()';
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class ConstantAssignmentException extends AbstractEvalMathException
{
protected $message = 'Cannot assign to constant :constant';
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class DivisionByZeroException extends AbstractEvalMathException
{
protected $message = 'Division by zero';
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
interface EvalMathException
{
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class ExpectingTokenException extends AbstractEvalMathException
{
protected $message = "Expecting ':token'";
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class IllegalCharacterException extends AbstractEvalMathException
{
protected $message = "illegal character ':character'";
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class InternalErrorException extends AbstractEvalMathException
{
protected $message = 'Internal error';
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class InvalidArgumentCountException extends AbstractEvalMathException
{
protected $message = 'Wrong number of arguments';
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class OperatorLacksOperandException extends AbstractEvalMathException
{
protected $message = "operator ':operator' lacks operand";
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class OperatorRequiredException extends AbstractEvalMathException
{
protected $message = 'Operator required';
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class UndefinedVariableException extends AbstractEvalMathException
{
protected $message = "Undefined variable";
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class UndefinedVariableInFunctionDefinitionException extends AbstractEvalMathException
{
protected $message = 'Undefined variable :token in function definition';
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class UnexpectedOperatorException extends AbstractEvalMathException
{
protected $message = 'Unexpected operator \':operator\'';
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Exception;
class UnexpectedTokenException extends AbstractEvalMathException
{
protected $message = 'Unexpected \':token\'';
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Methods;
abstract class AbstractMethod
{
protected $name = '';
protected $argument_count = [-1];
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
return clone $this;
}
public function getArgumentCount()
{
return $this->argument_count;
}
abstract public function evaluate(...$args);
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Methods;
class Conditional extends AbstractMethod
{
protected $name='if';
protected $argument_count = [3];
public function evaluate(...$args)
{
return $args[0] ? $args[1] : $args[2];
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Methods;
class Maximum extends AbstractMethod
{
protected $name='max';
public function evaluate(...$args)
{
return max($args);
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Methods;
class Minimum extends AbstractMethod
{
protected $name = 'min';
public function evaluate(...$args)
{
return min($args);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Andileco\Util\EvalMath\Methods;
class Round extends AbstractMethod
{
protected $name = 'round';
public function evaluate(...$args)
{
if (!isset($args[1])) {
return round($args[0]);
}
return round($args[0], $args[1]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath;
use ArrayAccess;
use Countable;
use RuntimeException;
use Andileco\Util\EvalMath\Methods\AbstractMethod;
class MethodsRegistry implements ArrayAccess, Countable
{
protected $methods = [];
public function set(AbstractMethod $method)
{
$this->methods[$method->getName()] = clone $method;
return $this;
}
public function unsetByName($name)
{
unset($this->methods[$name]);
return $this;
}
/**
* @param $name
* @return AbstractMethod|null
*/
public function findByName($name)
{
return (isset($this->methods[$name]) and $this->methods[$name] instanceof AbstractMethod) ? $this->methods[$name] : null;
}
public function offsetExists($offset): bool
{
return isset($this->methods[$offset]);
}
public function offsetGet($offset): mixed
{
$m = $this->findByName($offset);
return $m ? $m->getArgumentCount() : null;
}
public function offsetSet($offset, $value): void
{
throw new RuntimeException('Use set() method instead');
}
public function offsetUnset($offset): void
{
$this->unsetByName($offset);
}
public function count(): int
{
return count($this->methods);
}
}

45
vendor/andileco/eval-math/src/Stack.php vendored Normal file
View File

@@ -0,0 +1,45 @@
<?php
/**
*
*/
namespace Andileco\Util\EvalMath;
/**
* Class Stack
*/
class Stack
{
/**
* @var array
*/
public $stack = array();
/**
* @var int
*/
public $count = 0;
public function push($val)
{
$this->stack[$this->count] = $val;
$this->count++;
}
public function pop()
{
if ($this->count > 0) {
$this->count--;
return $this->stack[$this->count];
}
return null;
}
public function last($n=1)
{
$key = $this->count - $n;
return array_key_exists($key,$this->stack) ? $this->stack[$key] : null;
}
}

View File

@@ -0,0 +1,216 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Tests;
use PHPUnit\Framework\TestCase;
use Andileco\Util\EvalMath\EvalMath;
use Andileco\Util\EvalMath\Exception\AbstractEvalMathException;
use Andileco\Util\EvalMath\Exception\BuiltInFunctionRedefinitionException;
use Andileco\Util\EvalMath\Exception\ConstantAssignmentException;
use Andileco\Util\EvalMath\Exception\DivisionByZeroException;
use Andileco\Util\EvalMath\Exception\ExpectingTokenException;
use Andileco\Util\EvalMath\Exception\IllegalCharacterException;
use Andileco\Util\EvalMath\Exception\InternalErrorException;
use Andileco\Util\EvalMath\Exception\InvalidArgumentCountException;
use Andileco\Util\EvalMath\Exception\OperatorLacksOperandException;
use Andileco\Util\EvalMath\Exception\OperatorRequiredException;
use Andileco\Util\EvalMath\Exception\UndefinedVariableException;
use Andileco\Util\EvalMath\Exception\UndefinedVariableInFunctionDefinitionException;
use Andileco\Util\EvalMath\Exception\UnexpectedOperatorException;
use Andileco\Util\EvalMath\Exception\UnexpectedTokenException;
class EvalMathExceptionsTest extends TestCase
{
private $EvalMath;
protected function setUp()
{
$this->EvalMath = new EvalMath();
}
/**
* @throws AbstractEvalMathException
*/
public function testBuiltInFunctionRedefinition()
{
$this->expectException(BuiltInFunctionRedefinitionException::class);
$this->EvalMath->evaluate('cos(x) = x*2');
}
/**
* @throws AbstractEvalMathException
*/
public function testConstantAssignmentException()
{
$this->expectException(ConstantAssignmentException::class);
$this->EvalMath->evaluate('pi = 123');
}
/**
* @throws AbstractEvalMathException
*/
public function testDivisionByZero()
{
$this->expectException(DivisionByZeroException::class);
$this->EvalMath->evaluate('5/0');
}
/**
* @throws AbstractEvalMathException
*/
public function testExpectingTokenException()
{
$this->expectException(ExpectingTokenException::class);
$this->EvalMath->evaluate('p = 6*(3+1');
}
/**
* @throws AbstractEvalMathException
*/
public function testIllegalCharacterException()
{
$this->expectException(IllegalCharacterException::class);
$this->EvalMath->evaluate('$v = 5');
}
/**
* @throws AbstractEvalMathException
*/
public function testInternalErrorException()
{
$this->expectException(InternalErrorException::class);
$this->EvalMath->e('k=');
}
/**
* @throws AbstractEvalMathException
*/
public function testInvalidArgumentCountException()
{
$this->expectException(InvalidArgumentCountException::class);
$this->EvalMath->e('a=sin(5,7,6)');
}
/**
* @throws AbstractEvalMathException
*/
public function testInvalidArgumentFuncCountException()
{
$this->expectException(InvalidArgumentCountException::class);
$this->EvalMath->e('iif(1)');
}
/**
* @throws AbstractEvalMathException
*/
public function testInvalidArgumentFuncCountException2()
{
$this->expectException(InvalidArgumentCountException::class);
$this->EvalMath->e('iif()');
}
/**
* @throws AbstractEvalMathException
*/
public function testInvalidArgumentUserFuncCountException()
{
$this->expectException(InvalidArgumentCountException::class);
$this->EvalMath->e('f(a,b) = a+b');
$this->EvalMath->e('f(1)');
}
/**
* @throws AbstractEvalMathException
*/
public function testOperatorLacksOperandException()
{
$this->expectException(OperatorLacksOperandException::class);
$this->EvalMath->e('k=5+');
}
/**
* @throws AbstractEvalMathException
*/
public function testOperatorRequiredException()
{
$this->expectException(OperatorRequiredException::class);
$this->EvalMath->e('a=5(7+1)');
}
/**
* @throws AbstractEvalMathException
*/
public function testUndefinedVariableException()
{
$this->expectException(UndefinedVariableException::class);
$this->EvalMath->e('b=a+1');
}
/**
* @throws AbstractEvalMathException
*/
public function testUndefinedVariableInFunctionDefinitionException()
{
$this->expectException(UndefinedVariableInFunctionDefinitionException::class);
$this->EvalMath->e('f(a) = b*3');
}
/**
* @throws AbstractEvalMathException
*/
public function testUnexpectedOperatorException()
{
$this->expectException(UnexpectedOperatorException::class);
$this->EvalMath->e('1 + * 2');
}
/**
* @throws AbstractEvalMathException
*/
public function testUnexpectedTokenException()
{
$this->expectException(UnexpectedTokenException::class);
$this->EvalMath->e('a=(3+)');
}
/**
* @throws AbstractEvalMathException
*/
public function testUnexpectedClosingPrenticeException()
{
$this->expectException(UnexpectedTokenException::class);
$this->EvalMath->e('a=5)');
}
/**
* @throws AbstractEvalMathException
*/
public function testUnexpectedComma()
{
$this->expectException(UnexpectedTokenException::class);
$this->EvalMath->e('a=5,3)');
}
/**
* @throws AbstractEvalMathException
*/
public function testUnexpectedComma2()
{
$this->expectException(UnexpectedTokenException::class);
$this->EvalMath->e('a=sin(0.3),3)');
}
/**
* @throws AbstractEvalMathException
*/
public function testUnexpectedComma3()
{
$this->expectException(UnexpectedTokenException::class);
$this->EvalMath->e('a=sin(0.1,)');
}
}

View File

@@ -0,0 +1,140 @@
<?php
/**
* EvalMathTest.php
*
* @author dbojdo - Daniel Bojdo <daniel.bojdo@8x8.com>
* Created on 02 12, 2016, 17:17
* Copyright (C) 8x8
*/
namespace Andileco\Util\EvalMath\Tests;
use PHPUnit\Framework\TestCase;
use Andileco\Util\EvalMath\EvalMath;
class EvalMathTest extends TestCase
{
/**
* @var EvalMath
*/
private $evalMath;
protected function setUp()
{
$this->evalMath = new EvalMath();
}
/**
* @test
* @dataProvider moduloOperatorData
*/
public function shouldSupportModuloOperator($formula, $values, $expectedResult)
{
foreach ($values as $k => $v) {
$this->evalMath->v[$k] = $v;
}
$this->assertEquals($expectedResult, $this->evalMath->evaluate($formula));
}
public function moduloOperatorData()
{
return array(
array(
'a%b', // 9%3 => 0
array('a' => 9, 'b' => 3),
0
),
array(
'a%b', // 10%3 => 1
array('a' => 10, 'b' => 3),
1
),
array(
'10-a%(b+c*d)', // 10-10%(7-2*2) => 9
array('a' => '10', 'b' => 7, 'c'=> -2, 'd' => 2),
9
)
);
}
/**
* @test
* @dataProvider doubleMinusData
*/
public function shouldConsiderDoubleMinusAsPlus($formula, $values, $expectedResult)
{
foreach ($values as $k => $v) {
$this->evalMath->v[$k] = $v;
}
$this->assertEquals(
$expectedResult,
$this->evalMath->evaluate($formula)
);
}
public function doubleMinusData()
{
return array(
array(
'a+b*c--d', // 1+2*3--4 => 1+6+4 => 11
array(
'a' => 1,
'b' => 2,
'c' => 3,
'd' => 4
),
11
),
array(
'a+b*c--d', // 1+2*3---4 => 1+6-4 => 3
array(
'a' => 1,
'b' => 2,
'c' => 3,
'd' => -4
),
3
)
);
}
/**
* @dataProvider maximumsData
*/
public function testMaxCalcFunction($formula, $expected)
{
$this->assertEquals($expected, $this->evalMath->e($formula));
}
public function maximumsData()
{
return [
['max(1,2,3,4,5)', 5],
['max(5,2,3,1,4)', 5],
['max(5)', 5],
['max(5,5)', 5]
];
}
/**
* @dataProvider comparisonsData
*/
public function testComparisonOperators($formula, $expected)
{
$this->assertEquals($expected, $this->evalMath->e($formula));
}
public function comparisonsData()
{
return [
['1==1', 1],
['1==2', 0],
['1>2', 0],
['1<2', 1],
['1>=1', 1],
['1<=2', 1]
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Tests;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Andileco\Util\EvalMath\Methods\Maximum;
use Andileco\Util\EvalMath\MethodsRegistry;
class MethodRegistryTest extends TestCase
{
private $registry;
protected function setUp()
{
$this->registry = new MethodsRegistry();
}
public function testSetFindUnset()
{
$r = $this->registry->set(new Maximum);
$this->assertInstanceOf(MethodsRegistry::class, $r);
$c = $this->registry->findByName('max');
$this->assertInstanceOf(Maximum::class, $c);
$this->assertTrue(isset($this->registry['max']));
$c = $this->registry['max'];
$this->assertTrue(is_array($c));
// $this->assertInstanceOf(Maximum::class, $c);
$this->assertEquals(1, $this->registry->count());
unset($this->registry['max']);
$this->assertEquals(0, $this->registry->count());
$r = $this->registry->unsetByName('max');
$this->assertInstanceOf(MethodsRegistry::class, $r);
}
/**
* @expectedException RuntimeException
*/
public function testOffsetSetException()
{
$this->registry['max'] = new Maximum();
}
public function testAliases()
{
$this->registry = new MethodsRegistry();
$this->registry->set(new Maximum())->set((new Maximum)->setName('mmaaxx'));
$this->assertEquals(2, $this->registry->count());
$this->assertTrue(isset($this->registry['max']));
$this->assertTrue(isset($this->registry['mmaaxx']));
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Tests;
use PHPUnit\Framework\TestCase;
use Andileco\Util\EvalMath\Methods\Conditional;
use Andileco\Util\EvalMath\Methods\Maximum;
use Andileco\Util\EvalMath\Methods\Minimum;
use Andileco\Util\EvalMath\Methods\Round;
class MethodsTest extends TestCase
{
public function testConditional()
{
$m = new Conditional();
$this->assertEquals('if', $m->getName());
$this->assertEquals([3], $m->getArgumentCount());
$this->assertEquals(1, $m->evaluate(true, 1, 0));
$this->assertEquals(0, $m->evaluate(false, 1, 0));
}
public function testMaximum()
{
$m = new Maximum();
$this->assertEquals('max', $m->getName());
$this->assertEquals([-1], $m->getArgumentCount());
$this->assertEquals(1, $m->evaluate(1, 0));
$this->assertEquals(10, $m->evaluate(false, 1, 0, 10));
$this->assertEquals(1, $m->evaluate(1));
}
public function testMinimum()
{
$m = new Minimum();
$this->assertEquals('min', $m->getName());
$this->assertEquals([-1], $m->getArgumentCount());
$this->assertEquals(0, $m->evaluate(1, 0));
$this->assertEquals(0, $m->evaluate(false, 1, 0, 10));
$this->assertEquals(1, $m->evaluate(1));
}
public function testRound()
{
$m = new Round();
$this->assertEquals('round', $m->getName());
$this->assertEquals([-1], $m->getArgumentCount());
$this->assertEquals(2, $m->evaluate(1.5321));
$this->assertEquals(1, $m->evaluate(1.4321));
$this->assertEquals(1, $m->evaluate(1.4921));
$this->assertEquals(git add .1.57, $m->evaluate(1.5678, 2));
$this->assertEquals(1.568, $m->evaluate(1.5678, 3));
$this->assertEquals(1.5678, $m->evaluate(1.5678, 4));
$this->assertEquals(1.54, $m->evaluate(1.5378, 2));
$this->assertEquals(1.538, $m->evaluate(1.5378, 3));
$this->assertEquals(1.5378, $m->evaluate(1.5378, 4));
$this->assertEquals(1.53, $m->evaluate(1.5321, 2));
$this->assertEquals(1.532, $m->evaluate(1.5321, 3));
$this->assertEquals(1.5321, $m->evaluate(1.5321, 4));
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Tests;
use PHPUnit\Framework\TestCase;
use Andileco\Util\EvalMath\EvalMath;
class UserFunctionsTest extends TestCase
{
public function testFunctionsDump()
{
$e = new EvalMath();
$this->assertEmpty($e->funcs());
$e->e('f(x)=2*x');
$this->assertEquals(1, count($e->funcs()));
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @author Serge Rodovnichenko <serge@syrnik.com>
* @copyright Serge Rodovnichenko, 2019
* @license BSD 2.0
*/
namespace Andileco\Util\EvalMath\Tests;
use PHPUnit\Framework\TestCase;
use Andileco\Util\EvalMath\EvalMath;
class UserVariablesTest extends TestCase
{
public function testVariablesDump()
{
$e = new EvalMath();
$this->assertEmpty($e->vars());
$e->evaluate('a=1');
$this->assertArrayHasKey('a',$e->vars());
$this->assertEquals('1', $e->vars()['a']);
}
}