MDL-14274 lib/evalmath: support of conditional math in formulas.

- Comparison operators >, ==, <, <=, >=
- "if(condition, valueiftrue, valueif false)" function.

(amended to keep non-related lines unmodified - whitespace & indent)
This commit is contained in:
Juan Pablo de Castro 2018-02-08 19:57:35 +01:00 committed by Eloy Lafuente (stronk7)
parent decd90e2a3
commit 6aa8a3b6d0
4 changed files with 110 additions and 18 deletions

View File

@ -89,7 +89,8 @@ LICENSE
/**
* This class was heavily modified in order to get usefull spreadsheet emulation ;-)
* skodak
*
* This class was modified to allow comparison operators (<, <=, ==, >=, >)
* and synonyms functions (for the 'if' function). See MDL-14274 for more details.
*/
class EvalMath {
@ -113,7 +114,8 @@ class EvalMath {
'average'=>array(-1), 'max'=>array(-1), 'min'=>array(-1),
'mod'=>array(2), 'pi'=>array(0), 'power'=>array(2),
'round'=>array(1, 2), 'sum'=>array(-1), 'rand_int'=>array(2),
'rand_float'=>array(0));
'rand_float'=>array(0), 'ifthenelse'=>array(3));
var $fcsynonyms = array('if' => 'ifthenelse');
var $allowimplicitmultiplication;
@ -207,20 +209,25 @@ class EvalMath {
$stack = new EvalMathStack;
$output = array(); // postfix form of expression, to be passed to pfx()
$expr = trim(strtolower($expr));
$ops = array('+', '-', '*', '/', '^', '_');
// MDL-14274: new operators for comparison added.
$ops = array('+', '-', '*', '/', '^', '_', '>', '<', '<=', '>=', '==');
$ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?
$ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence
$ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2, '>'=>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
if (preg_match("/[^\w\s+*^\/()\.,-<>=]/", $expr, $matches)) { // make sure the characters are all good
return $this->trigger(get_string('illegalcharactergeneral', 'mathslib', $matches[0]));
}
while(1) { // 1 Infinite Loop ;)
$op = substr($expr, $index, 1); // get the first character at the current index
// MDL-14274 Test two character operators.
$op = substr($expr, $index, 2);
if (!in_array($op, $ops)) {
// MDL-14274 Get one character operator.
$op = substr($expr, $index, 1); // get the first character at the current index
}
// find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
$ex = preg_match('/^('.self::$namepat.'\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr($expr, $index), $match);
//===============
@ -245,7 +252,7 @@ class EvalMath {
}
// 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++;
$index += strlen($op);
$expecting_op = false;
//===============
} elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
@ -265,7 +272,9 @@ class EvalMath {
$a->given = $arg_count;
return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
}
} elseif (array_key_exists($fnn, $this->fc)) {
} elseif ($this->get_native_function_name($fnn)) {
$fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
$counts = $this->fc[$fnn];
if (in_array(-1, $counts) and $arg_count > 0) {}
elseif (!in_array($arg_count, $counts)) {
@ -309,7 +318,9 @@ class EvalMath {
$expecting_op = true;
$val = $match[1];
if (preg_match('/^('.self::$namepat.')\($/', $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 array_key_exists($matches[1], $this->fc)) { // it's a func
if (in_array($matches[1], $this->fb) or
array_key_exists($matches[1], $this->f) or
$this->get_native_function_name($matches[1])){ // it's a func
$stack->push($val);
$stack->push(1);
$stack->push('(');
@ -331,6 +342,7 @@ class EvalMath {
$stack->pop();// 1
$fn = $stack->pop();
$fnn = $matches[1]; // get the function name
$fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
$counts = $this->fc[$fnn];
if (!in_array(0, $counts)){
$a= new stdClass();
@ -368,7 +380,20 @@ class EvalMath {
}
return $output;
}
/**
*
* @param string $fnn
* @return string|boolean false if function name unknown.
*/
function get_native_function_name($fnn) {
if (array_key_exists($fnn, $this->fcsynonyms)) {
return $this->fcsynonyms[$fnn];
} else if (array_key_exists($fnn, $this->fc)) {
return $fnn;
} else {
return false;
}
}
// evaluate postfix notation
function pfx($tokens, $vars = array()) {
@ -387,7 +412,8 @@ class EvalMath {
$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 (array_key_exists($fnn, $this->fc)) { // calc emulation function
} elseif ($this->get_native_function_name($fnn)) { // calc emulation function
$fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
// get args
$args = array();
for ($i = $count-1; $i >= 0; $i--) {
@ -407,7 +433,7 @@ class EvalMath {
$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, array('+', '-', '*', '/', '^'), true)) {
} elseif (in_array($token, array('+', '-', '*', '/', '^', '>', '<', '==', '<=', '>='), true)) {
if (is_null($op2 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
switch ($token) {
@ -422,6 +448,16 @@ class EvalMath {
$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;
}
// if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
} elseif ($token == "_") {
@ -483,7 +519,21 @@ class EvalMathStack {
// spreadsheet functions emulation
class EvalMathFuncs {
/**
* MDL-14274 new conditional function.
* @param boolean $condition boolean for conditional.
* @param variant $then value if condition is true.
* @param unknown $else value if condition is false.
* @author Juan Pablo de Castro <juan.pablo.de.castro@gmail.com>
* @return unknown
*/
static function ifthenelse($condition, $then, $else) {
if ($condition == true) {
return $then;
} else {
return $else;
}
}
static function average() {
$args = func_get_args();
return (call_user_func_array(array('self', 'sum'), $args) / count($args));

View File

@ -18,3 +18,7 @@ To see all changes diff against version 1.1, available from:
http://www.phpclasses.org/browse/package/2695.html
skodak, Tim Hunt
Changes by Juan Pablo de Castro (MDL-14274):
* operators >,<,>=,<=,== added.
* function if[thenelse](condition, true_value, false_value)

View File

@ -56,10 +56,7 @@ class calc_formula {
return;
}
$formula = substr($formula, 1);
if (strpos($formula, '=') !== false) {
$this->_error = "too many '='";
return;
}
$this->_nfx = $this->_em->nfx($formula);
if ($this->_nfx == false) {
$this->_error = $this->_em->last_error;

View File

@ -81,6 +81,47 @@ class core_mathslib_testcase extends basic_testcase {
$this->assertSame(8, $formula->evaluate());
}
public function test_conditional_functions() {
$formula = new calc_formula('=ifthenelse(1,2,3)');
$this->assertSame(2, (int)$formula->evaluate());
$formula = new calc_formula('=ifthenelse(0,2,3)');
$this->assertSame(3, (int) $formula->evaluate());
$formula = new calc_formula('=ifthenelse(2<3,2,3)');
$this->assertSame(2, (int) $formula->evaluate());
// Test synonim if.
$formula = new calc_formula('=if(1,2,3)');
$this->assertSame(2, (int)$formula->evaluate());
$formula = new calc_formula('=if(0,2,3)');
$this->assertSame(3, (int) $formula->evaluate());
$formula = new calc_formula('=if(2<3,2,3)');
$this->assertSame(2, (int) $formula->evaluate());
}
public function test_conditional_operators() {
$formula = new calc_formula('=2==2');
$this->assertSame(1, $formula->evaluate());
$formula = new calc_formula('=2>3');
$this->assertSame(0, $formula->evaluate());
$formula = new calc_formula('=2<3');
$this->assertSame(1, $formula->evaluate());
$formula = new calc_formula('=(2<=3)');
$this->assertSame(1, $formula->evaluate());
$formula = new calc_formula('=(2<=3)*10');
$this->assertSame(10, $formula->evaluate());
$formula = new calc_formula('=(2>=3)*10');
$this->assertSame(0, $formula->evaluate());
$formula = new calc_formula('=2<3*10');
$this->assertSame(10, $formula->evaluate());
}
/**
* Tests the min and max functions.
*/