diff --git a/lib/evalmath/evalmath.class.php b/lib/evalmath/evalmath.class.php index af8a77d30a3..d8875b56866 100644 --- a/lib/evalmath/evalmath.class.php +++ b/lib/evalmath/evalmath.class.php @@ -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 + * @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)); diff --git a/lib/evalmath/readme_moodle.txt b/lib/evalmath/readme_moodle.txt index e9b1a80a4b8..c9935c53634 100644 --- a/lib/evalmath/readme_moodle.txt +++ b/lib/evalmath/readme_moodle.txt @@ -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) diff --git a/lib/mathslib.php b/lib/mathslib.php index aa2e8666280..8753695cc16 100644 --- a/lib/mathslib.php +++ b/lib/mathslib.php @@ -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; diff --git a/lib/tests/mathslib_test.php b/lib/tests/mathslib_test.php index fe3b1d27573..6d37bcfdc41 100644 --- a/lib/tests/mathslib_test.php +++ b/lib/tests/mathslib_test.php @@ -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. */