Merge branch 'MDL-28133' of git://github.com/timhunt/moodle

This commit is contained in:
Eloy Lafuente (stronk7) 2011-07-01 01:10:06 +02:00
commit d72129829f
2 changed files with 109 additions and 83 deletions

View file

@ -135,7 +135,7 @@ class EvalMath {
if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end
//=============== //===============
// is it a variable assignment? // is it a variable assignment?
if (preg_match('/^\s*([a-z][a-z0-9]*)\s*=\s*(.+)$/', $expr, $matches)) { if (preg_match('/^\s*('.self::$namepat.')\s*=\s*(.+)$/', $expr, $matches)) {
if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant
return $this->trigger(get_string('cannotassigntoconstant', 'mathslib', $matches[1])); return $this->trigger(get_string('cannotassigntoconstant', 'mathslib', $matches[1]));
} }
@ -144,7 +144,7 @@ class EvalMath {
return $this->v[$matches[1]]; // and return the resulting value return $this->v[$matches[1]]; // and return the resulting value
//=============== //===============
// is it a function assignment? // is it a function assignment?
} elseif (preg_match('/^\s*([a-z][a-z0-9]*)\s*\(\s*([a-z][a-z0-9]*(?:\s*,\s*[a-z][a-z0-9]*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) { } elseif (preg_match('/^\s*('.self::$namepat.')\s*\(\s*('.self::$namepat.'(?:\s*,\s*'.self::$namepat.')*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) {
$fnn = $matches[1]; // get the function name $fnn = $matches[1]; // get the function name
if (in_array($matches[1], $this->fb)) { // make sure it isn't built in if (in_array($matches[1], $this->fb)) { // make sure it isn't built in
return $this->trigger(get_string('cannotredefinebuiltinfunction', 'mathslib', $matches[1])); return $this->trigger(get_string('cannotredefinebuiltinfunction', 'mathslib', $matches[1]));
@ -153,7 +153,7 @@ class EvalMath {
if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix 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 for ($i = 0; $i<count($stack); $i++) { // freeze the state of the non-argument variables
$token = $stack[$i]; $token = $stack[$i];
if (preg_match('/^[a-z][a-z0-9]*$/', $token) and !in_array($token, $args)) { if (preg_match('/^'.self::$namepat.'$/', $token) and !in_array($token, $args)) {
if (array_key_exists($token, $this->v)) { if (array_key_exists($token, $this->v)) {
$stack[$i] = $this->v[$token]; $stack[$i] = $this->v[$token];
} else { } else {
@ -212,7 +212,7 @@ class EvalMath {
while(1) { // 1 Infinite Loop ;) while(1) { // 1 Infinite Loop ;)
$op = substr($expr, $index, 1); // get the first character at the current index $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 // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
$ex = preg_match('/^([a-z][a-z0-9]*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr($expr, $index), $match); $ex = preg_match('/^('.self::$namepat.'\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr($expr, $index), $match);
//=============== //===============
if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus? if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
$stack->push('_'); // put a negation on the stack $stack->push('_'); // put a negation on the stack
@ -243,7 +243,7 @@ class EvalMath {
if (is_null($o2)) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib')); if (is_null($o2)) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
else $output[] = $o2; else $output[] = $o2;
} }
if (preg_match("/^([a-z][a-z0-9]*)\($/", $stack->last(2), $matches)) { // did we just close a function? if (preg_match('/^('.self::$namepat.')\($/', $stack->last(2), $matches)) { // did we just close a function?
$fnn = $matches[1]; // get the function name $fnn = $matches[1]; // get the function name
$arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you) $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
$fn = $stack->pop(); $fn = $stack->pop();
@ -283,7 +283,7 @@ class EvalMath {
else $output[] = $o2; // pop the argument expression stuff and push onto the output else $output[] = $o2; // pop the argument expression stuff and push onto the output
} }
// make sure there was a function // make sure there was a function
if (!preg_match("/^([a-z][a-z0-9]*)\($/", $stack->last(2), $matches)) if (!preg_match('/^('.self::$namepat.')\($/', $stack->last(2), $matches))
return $this->trigger(get_string('unexpectedcomma', 'mathslib')); return $this->trigger(get_string('unexpectedcomma', 'mathslib'));
$stack->push($stack->pop()+1); // increment the argument count $stack->push($stack->pop()+1); // increment the argument count
$stack->push('('); // put the ( back on, we'll need to pop back to it again $stack->push('('); // put the ( back on, we'll need to pop back to it again
@ -298,7 +298,7 @@ class EvalMath {
} elseif ($ex and !$expecting_op) { // do we now have a function/variable/number? } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
$expecting_op = true; $expecting_op = true;
$val = $match[1]; $val = $match[1];
if (preg_match("/^([a-z][a-z0-9]*)\($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses... 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 array_key_exists($matches[1], $this->fc)) { // it's a func
$stack->push($val); $stack->push($val);
$stack->push(1); $stack->push(1);
@ -316,7 +316,7 @@ class EvalMath {
} elseif ($op == ')') { } elseif ($op == ')') {
//it could be only custom function with no params or general error //it could be only custom function with no params or general error
if ($stack->last() != '(' or $stack->last(2) != 1) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib')); if ($stack->last() != '(' or $stack->last(2) != 1) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
if (preg_match("/^([a-z][a-z0-9]*)\($/", $stack->last(3), $matches)) { // did we just close a function? if (preg_match('/^('.self::$namepat.')\($/', $stack->last(3), $matches)) { // did we just close a function?
$stack->pop();// ( $stack->pop();// (
$stack->pop();// 1 $stack->pop();// 1
$fn = $stack->pop(); $fn = $stack->pop();
@ -383,8 +383,7 @@ class EvalMath {
for ($i = $count-1; $i >= 0; $i--) { for ($i = $count-1; $i >= 0; $i--) {
if (is_null($args[] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib')); if (is_null($args[] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
} }
$classname = 'EvalMathCalcEmul_'.$fnn; $res = call_user_func_array(array('EvalMathFuncs', $fnn), array_reverse($args));
$res = call_user_func(array($classname, 'calculate'), $args);
if ($res === FALSE) { if ($res === FALSE) {
return $this->trigger(get_string('internalerror', 'mathslib')); return $this->trigger(get_string('internalerror', 'mathslib'));
} }
@ -473,16 +472,15 @@ class EvalMathStack {
// spreadsheet functions emulation // spreadsheet functions emulation
// watch out for reversed args!! class EvalMathFuncs {
class EvalMathCalcEmul_average {
static function calculate($args) { static function average() {
return (EvalMathCalcEmul_sum::calculate($args)/count($args)); $args = func_get_args();
return (call_user_func_array(array('self', 'sum'), $args) / count($args));
} }
}
class EvalMathCalcEmul_max { static function max() {
static function calculate($args) { $args = func_get_args();
$res = array_pop($args); $res = array_pop($args);
foreach($args as $a) { foreach($args as $a) {
if ($res < $a) { if ($res < $a) {
@ -491,10 +489,9 @@ class EvalMathCalcEmul_max {
} }
return $res; return $res;
} }
}
class EvalMathCalcEmul_min { static function min() {
static function calculate($args) { $args = func_get_args();
$res = array_pop($args); $res = array_pop($args);
foreach($args as $a) { foreach($args as $a) {
if ($res > $a) { if ($res > $a) {
@ -503,42 +500,32 @@ class EvalMathCalcEmul_min {
} }
return $res; return $res;
} }
}
class EvalMathCalcEmul_mod { static function mod($op1, $op2) {
static function calculate($args) { return $op1 % $op2;
return $args[1] % $args[0];
} }
}
class EvalMathCalcEmul_pi { static function pi() {
static function calculate($args) {
return pi(); return pi();
} }
}
class EvalMathCalcEmul_power {
static function calculate($args) {
return $args[1]^$args[0];
}
}
class EvalMathCalcEmul_round { static function power($op1, $op2) {
static function calculate($args) { return pow($op1, $op2);
if (count($args)==1) {
return round($args[0]);
} else {
return round($args[1], $args[0]);
}
} }
}
class EvalMathCalcEmul_sum { static function round($val, $precision = 0) {
static function calculate($args) { return round($val, $precision);
}
static function sum() {
$args = func_get_args();
$res = 0; $res = 0;
foreach($args as $a) { foreach($args as $a) {
$res += $a; $res += $a;
} }
return $res; return $res;
} }
}
class EvalMathCalcEmul_randomised {
protected static $randomseed = null; protected static $randomseed = null;
static function set_random_seed($randomseed) { static function set_random_seed($randomseed) {
@ -553,12 +540,7 @@ class EvalMathCalcEmul_randomised {
} }
} }
} static function rand_int($min, $max){
class EvalMathCalcEmul_rand_int extends EvalMathCalcEmul_randomised {
static function calculate($args){
$min = $args[1];
$max = $args[0];
if ($min >= $max) { if ($min >= $max) {
return false; //error return false; //error
} }
@ -574,9 +556,8 @@ class EvalMathCalcEmul_rand_int extends EvalMathCalcEmul_randomised {
} while (($min + $randomno) > $max); } while (($min + $randomno) > $max);
return $min + $randomno; return $min + $randomno;
} }
}
class EvalMathCalcEmul_rand_float extends EvalMathCalcEmul_randomised { static function rand_float(){
static function calculate(){
$randomvalue = array_shift(unpack('v', md5(self::get_random_seed(), true))); $randomvalue = array_shift(unpack('v', md5(self::get_random_seed(), true)));
return $randomvalue / 65536; return $randomvalue / 65536;
} }

View file

@ -1,7 +1,20 @@
<?php <?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
if (!defined('MOODLE_INTERNAL')) { if (!defined('MOODLE_INTERNAL')) {
die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page die('Direct access to this script is forbidden.');/// It must be included from a Moodle page
} }
require_once($CFG->libdir . '/mathslib.php'); require_once($CFG->libdir . '/mathslib.php');
@ -19,7 +32,7 @@ class mathsslib_test extends UnitTestCase {
/** /**
* Tests the basic formula evaluation * Tests the basic formula evaluation
*/ */
function test__basic() { public function test__basic() {
$formula = new calc_formula('=1+2'); $formula = new calc_formula('=1+2');
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, 3, '3+1 is: %s'); $this->assertEqual($res, 3, '3+1 is: %s');
@ -28,8 +41,8 @@ class mathsslib_test extends UnitTestCase {
/** /**
* Tests the formula params * Tests the formula params
*/ */
function test__params() { public function test__params() {
$formula = new calc_formula('=a+b+c', array('a'=>10,'b'=>20,'c'=>30)); $formula = new calc_formula('=a+b+c', array('a'=>10, 'b'=>20, 'c'=>30));
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, 60, '10+20+30 is: %s'); $this->assertEqual($res, 60, '10+20+30 is: %s');
} }
@ -37,11 +50,11 @@ class mathsslib_test extends UnitTestCase {
/** /**
* Tests the changed params * Tests the changed params
*/ */
function test__changing_params() { public function test__changing_params() {
$formula = new calc_formula('=a+b+c', array('a'=>10,'b'=>20,'c'=>30)); $formula = new calc_formula('=a+b+c', array('a'=>10, 'b'=>20, 'c'=>30));
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, 60, '10+20+30 is: %s'); $this->assertEqual($res, 60, '10+20+30 is: %s');
$formula->set_params(array('a'=>1,'b'=>2,'c'=>3)); $formula->set_params(array('a'=>1, 'b'=>2, 'c'=>3));
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, 6, 'changed params 1+2+3 is: %s'); $this->assertEqual($res, 6, 'changed params 1+2+3 is: %s');
} }
@ -49,20 +62,31 @@ class mathsslib_test extends UnitTestCase {
/** /**
* Tests the spreadsheet emulation function in formula * Tests the spreadsheet emulation function in formula
*/ */
function test__calc_function() { public function test__calc_function() {
$formula = new calc_formula('=sum(a,b,c)', array('a'=>10,'b'=>20,'c'=>30)); $formula = new calc_formula('=sum(a, b, c)', array('a'=>10, 'b'=>20, 'c'=>30));
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, 60, 'sum(a,b,c) is: %s'); $this->assertEqual($res, 60, 'sum(a, b, c) is: %s');
}
public function test_other_functions() {
$formula = new calc_formula('=average(1,2,3)');
$this->assertEqual($formula->evaluate(), 2);
$formula = new calc_formula('=mod(10,3)');
$this->assertEqual($formula->evaluate(), 1);
$formula = new calc_formula('=power(2,3)');
$this->assertEqual($formula->evaluate(), 8);
} }
/** /**
* Tests the min and max functions * Tests the min and max functions
*/ */
function test__minmax_function() { public function test__minmax_function() {
$formula = new calc_formula('=min(a,b,c)', array('a'=>10,'b'=>20,'c'=>30)); $formula = new calc_formula('=min(a, b, c)', array('a'=>10, 'b'=>20, 'c'=>30));
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, 10, 'minimum is: %s'); $this->assertEqual($res, 10, 'minimum is: %s');
$formula = new calc_formula('=max(a,b,c)', array('a'=>10,'b'=>20,'c'=>30)); $formula = new calc_formula('=max(a, b, c)', array('a'=>10, 'b'=>20, 'c'=>30));
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, 30, 'maximum is: %s'); $this->assertEqual($res, 30, 'maximum is: %s');
} }
@ -70,8 +94,8 @@ class mathsslib_test extends UnitTestCase {
/** /**
* Tests special chars * Tests special chars
*/ */
function test__specialchars() { public function test__specialchars() {
$formula = new calc_formula('=gi1 + gi2 + gi11', array('gi1'=>10,'gi2'=>20,'gi11'=>30)); $formula = new calc_formula('=gi1 + gi2 + gi11', array('gi1'=>10, 'gi2'=>20, 'gi11'=>30));
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, 60, 'sum is: %s'); $this->assertEqual($res, 60, 'sum is: %s');
} }
@ -79,49 +103,52 @@ class mathsslib_test extends UnitTestCase {
/** /**
* Tests some slightly more complex expressions * Tests some slightly more complex expressions
*/ */
function test__more_complex_expressions() { public function test__more_complex_expressions() {
$formula = new calc_formula('=pi() + a', array('a'=>10)); $formula = new calc_formula('=pi() + a', array('a'=>10));
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, pi()+10); $this->assertEqual($res, pi()+10);
$formula = new calc_formula('=pi()^a', array('a'=>10)); $formula = new calc_formula('=pi()^a', array('a'=>10));
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, pow(pi(),10)); $this->assertEqual($res, pow(pi(), 10));
$formula = new calc_formula('=-8*(5/2)^2*(1-sqrt(4))-8'); $formula = new calc_formula('=-8*(5/2)^2*(1-sqrt(4))-8');
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, -8*pow((5/2),2)*(1-sqrt(4))-8); $this->assertEqual($res, -8*pow((5/2), 2)*(1-sqrt(4))-8);
} }
/** /**
* Tests some slightly more complex expressions * Tests some slightly more complex expressions
*/ */
function test__error_handling() { public function test__error_handling() {
if (debugging('', DEBUG_DEVELOPER)){ if (debugging('', DEBUG_DEVELOPER)) {
$this->expectError(); $this->expectError();
} }
$formula = new calc_formula('=pi( + a', array('a'=>10)); $formula = new calc_formula('=pi( + a', array('a'=>10));
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, false); $this->assertEqual($res, false);
$this->assertEqual($formula->get_error(), get_string('unexpectedoperator', 'mathslib', '+')); $this->assertEqual($formula->get_error(),
get_string('unexpectedoperator', 'mathslib', '+'));
if (debugging('', DEBUG_DEVELOPER)){ if (debugging('', DEBUG_DEVELOPER)) {
$this->expectError(); $this->expectError();
} }
$formula = new calc_formula('=pi('); $formula = new calc_formula('=pi(');
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, false); $this->assertEqual($res, false);
$this->assertEqual($formula->get_error(), get_string('expectingaclosingbracket', 'mathslib')); $this->assertEqual($formula->get_error(),
get_string('expectingaclosingbracket', 'mathslib'));
if (debugging('', DEBUG_DEVELOPER)){ if (debugging('', DEBUG_DEVELOPER)) {
$this->expectError(); $this->expectError();
} }
$formula = new calc_formula('=pi()^'); $formula = new calc_formula('=pi()^');
$res = $formula->evaluate(); $res = $formula->evaluate();
$this->assertEqual($res, false); $this->assertEqual($res, false);
$this->assertEqual($formula->get_error(), get_string('operatorlacksoperand', 'mathslib', '^')); $this->assertEqual($formula->get_error(),
get_string('operatorlacksoperand', 'mathslib', '^'));
} }
function test_rounding_function() { public function test_rounding_function() {
$formula = new calc_formula('=round(2.5)'); $formula = new calc_formula('=round(2.5)');
$this->assertEqual($formula->evaluate(), 3); $this->assertEqual($formula->evaluate(), 3);
@ -140,7 +167,6 @@ class mathsslib_test extends UnitTestCase {
$formula = new calc_formula('=round(-2.5)'); $formula = new calc_formula('=round(-2.5)');
$this->assertEqual($formula->evaluate(), -3); $this->assertEqual($formula->evaluate(), -3);
$formula = new calc_formula('=ceil(2.5)'); $formula = new calc_formula('=ceil(2.5)');
$this->assertEqual($formula->evaluate(), 3); $this->assertEqual($formula->evaluate(), 3);
@ -159,7 +185,6 @@ class mathsslib_test extends UnitTestCase {
$formula = new calc_formula('=ceil(-2.5)'); $formula = new calc_formula('=ceil(-2.5)');
$this->assertEqual($formula->evaluate(), -2); $this->assertEqual($formula->evaluate(), -2);
$formula = new calc_formula('=floor(2.5)'); $formula = new calc_formula('=floor(2.5)');
$this->assertEqual($formula->evaluate(), 2); $this->assertEqual($formula->evaluate(), 2);
@ -180,6 +205,26 @@ class mathsslib_test extends UnitTestCase {
} }
public function test_scientific_notation() {
$formula = new calc_formula('=10e10');
$this->assertWithinMargin($formula->evaluate(), 1e11, 1e11*1e-15);
$formula = new calc_formula('=10e-10');
$this->assertWithinMargin($formula->evaluate(), 1e-9, 1e11*1e-15);
$formula = new calc_formula('=10e+10');
$this->assertWithinMargin($formula->evaluate(), 1e11, 1e11*1e-15);
$formula = new calc_formula('=10e10*5');
$this->assertWithinMargin($formula->evaluate(), 5e11, 1e11*1e-15);
$formula = new calc_formula('=10e10^2');
$this->assertWithinMargin($formula->evaluate(), 1e22, 1e22*1e-15);
}
} }