/**
* 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 {
'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;
$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);
//===============
}
// 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?
$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)) {
$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('(');
$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();
}
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()) {
$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--) {
$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) {
$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 == "_") {
// 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));
$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.
*/