mirror of
https://github.com/moodle/moodle.git
synced 2025-04-13 20:42:22 +02:00
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:
parent
decd90e2a3
commit
6aa8a3b6d0
@ -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));
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user