Moodle  2.2.1
http://www.collinsharper.com
C:/xampp/htdocs/moodle/question/type/calculated/question.php
Go to the documentation of this file.
00001 <?php
00002 // This file is part of Moodle - http://moodle.org/
00003 //
00004 // Moodle is free software: you can redistribute it and/or modify
00005 // it under the terms of the GNU General Public License as published by
00006 // the Free Software Foundation, either version 3 of the License, or
00007 // (at your option) any later version.
00008 //
00009 // Moodle is distributed in the hope that it will be useful,
00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of
00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00012 // GNU General Public License for more details.
00013 //
00014 // You should have received a copy of the GNU General Public License
00015 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
00016 
00027 defined('MOODLE_INTERNAL') || die();
00028 
00029 require_once($CFG->dirroot . '/question/type/numerical/question.php');
00030 
00031 
00038 class qtype_calculated_question extends qtype_numerical_question
00039         implements qtype_calculated_question_with_expressions {
00040 
00042     public $datasetloader;
00043 
00045     public $vs;
00046 
00051     public $synchronised;
00052 
00053     public function start_attempt(question_attempt_step $step, $variant) {
00054         qtype_calculated_question_helper::start_attempt($this, $step, $variant);
00055         parent::start_attempt($step, $variant);
00056     }
00057 
00058     public function apply_attempt_state(question_attempt_step $step) {
00059         qtype_calculated_question_helper::apply_attempt_state($this, $step);
00060         parent::apply_attempt_state($step);
00061     }
00062 
00063     public function calculate_all_expressions() {
00064         $this->questiontext = $this->vs->replace_expressions_in_text($this->questiontext);
00065         $this->generalfeedback = $this->vs->replace_expressions_in_text($this->generalfeedback);
00066 
00067         foreach ($this->answers as $ans) {
00068             if ($ans->answer && $ans->answer !== '*') {
00069                 $ans->answer = $this->vs->calculate($ans->answer,
00070                         $ans->correctanswerlength, $ans->correctanswerformat);
00071             }
00072             $ans->feedback = $this->vs->replace_expressions_in_text($ans->feedback,
00073                         $ans->correctanswerlength, $ans->correctanswerformat);
00074         }
00075     }
00076 
00077     public function get_num_variants() {
00078         return $this->datasetloader->get_number_of_items();
00079     }
00080 
00081     public function get_variants_selection_seed() {
00082         if (!empty($this->synchronised) &&
00083                 $this->datasetloader->datasets_are_synchronised($this->category)) {
00084             return 'category' . $this->category;
00085         } else {
00086             return parent::get_variants_selection_seed();
00087         }
00088     }
00089 }
00090 
00091 
00104 interface qtype_calculated_question_with_expressions {
00111     public function calculate_all_expressions();
00112 }
00113 
00114 
00124 abstract class qtype_calculated_question_helper {
00125     public static function start_attempt(
00126             qtype_calculated_question_with_expressions $question,
00127             question_attempt_step $step, $variant) {
00128 
00129         $question->vs = new qtype_calculated_variable_substituter(
00130                 $question->datasetloader->get_values($variant),
00131                 get_string('decsep', 'langconfig'));
00132         $question->calculate_all_expressions();
00133 
00134         foreach ($question->vs->get_values() as $name => $value) {
00135             $step->set_qt_var('_var_' . $name, $value);
00136         }
00137     }
00138 
00139     public static function apply_attempt_state(
00140             qtype_calculated_question_with_expressions $question, question_attempt_step $step) {
00141         $values = array();
00142         foreach ($step->get_qt_data() as $name => $value) {
00143             if (substr($name, 0, 5) === '_var_') {
00144                 $values[substr($name, 5)] = $value;
00145             }
00146         }
00147 
00148         $question->vs = new qtype_calculated_variable_substituter(
00149                 $values, get_string('decsep', 'langconfig'));
00150         $question->calculate_all_expressions();
00151     }
00152 }
00153 
00154 
00162 class qtype_calculated_dataset_loader {
00164     protected $questionid;
00165 
00167     protected $itemsavailable = null;
00168 
00173     public function __construct($questionid) {
00174         $this->questionid = $questionid;
00175     }
00176 
00183     public function get_number_of_items() {
00184         global $DB;
00185 
00186         if (is_null($this->itemsavailable)) {
00187             $this->itemsavailable = $DB->get_field_sql('
00188                     SELECT MIN(qdd.itemcount)
00189                       FROM {question_dataset_definitions} qdd
00190                       JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition
00191                      WHERE qd.question = ?
00192                     ', array($this->questionid), MUST_EXIST);
00193         }
00194 
00195         return $this->itemsavailable;
00196     }
00197 
00203     protected function load_values($itemnumber) {
00204         global $DB;
00205 
00206         return $DB->get_records_sql_menu('
00207                 SELECT qdd.name, qdi.value
00208                   FROM {question_dataset_items} qdi
00209                   JOIN {question_dataset_definitions} qdd ON qdd.id = qdi.definition
00210                   JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition
00211                  WHERE qd.question = ?
00212                    AND qdi.itemnumber = ?
00213                 ', array($this->questionid, $itemnumber));
00214     }
00215 
00222     public function get_values($itemnumber) {
00223         if ($itemnumber <= 0 || $itemnumber > $this->get_number_of_items()) {
00224             $a = new stdClass();
00225             $a->id = $this->questionid;
00226             $a->item = $itemnumber;
00227             throw new moodle_exception('cannotgetdsfordependent', 'question', '', $a);
00228         }
00229 
00230         return $this->load_values($itemnumber);
00231     }
00232 
00233     public function datasets_are_synchronised($category) {
00234         global $DB;
00235         // We need to ensure that there are synchronised datasets, and that they
00236         // all use the right category.
00237         $categories = $DB->get_record_sql('
00238                 SELECT MAX(qdd.category) AS max,
00239                        MIN(qdd.category) AS min
00240                   FROM {question_dataset_definitions} qdd
00241                   JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition
00242                  WHERE qd.question = ?
00243                    AND qdd.category <> 0
00244             ', array($this->questionid));
00245 
00246         return $categories && $categories->max == $category && $categories->min == $category;
00247     }
00248 }
00249 
00250 
00261 class qtype_calculated_variable_substituter {
00263     protected $values;
00264 
00266     protected $decimalpoint;
00267 
00269     protected $search;
00270 
00275     protected $safevalue;
00276 
00281     protected $prettyvalue;
00282 
00287     public function __construct(array $values, $decimalpoint) {
00288         $this->values = $values;
00289         $this->decimalpoint = $decimalpoint;
00290 
00291         // Prepare an array for {@link substitute_values()}.
00292         $this->search = array();
00293         $this->replace = array();
00294         foreach ($values as $name => $value) {
00295             if (!is_numeric($value)) {
00296                 $a = new stdClass();
00297                 $a->name = '{' . $name . '}';
00298                 $a->value = $value;
00299                 throw new moodle_exception('notvalidnumber', 'qtype_calculated', '', $a);
00300             }
00301 
00302             $this->search[] = '{' . $name . '}';
00303             $this->safevalue[] = '(' . $value . ')';
00304             $this->prettyvalue[] = $this->format_float($value);
00305         }
00306     }
00307 
00316     public function format_float($x, $length = null, $format = null) {
00317         if (!is_null($length) && !is_null($format)) {
00318             if ($format == 1) {
00319                 // Decimal places.
00320                 $x = sprintf('%.' . $length . 'F', $x);
00321             } else if ($format == 2) {
00322                 // Significant figures.
00323                 $x = sprintf('%.' . $length . 'g', $x);
00324             }
00325         }
00326         return str_replace('.', $this->decimalpoint, $x);
00327     }
00328 
00333     public function get_values() {
00334         return $this->values;
00335     }
00336 
00343     public function calculate($expression) {
00344         return $this->calculate_raw($this->substitute_values_for_eval($expression));
00345     }
00346 
00353     protected function calculate_raw($expression) {
00354         // This validation trick from http://php.net/manual/en/function.eval.php
00355         if (!@eval('return true; $result = ' . $expression . ';')) {
00356             throw new moodle_exception('illegalformulasyntax', 'qtype_calculated', '', $expression);
00357         }
00358         return eval('return ' . $expression . ';');
00359     }
00360 
00368     protected function substitute_values_for_eval($expression) {
00369         return str_replace($this->search, $this->safevalue, $expression);
00370     }
00371 
00380     protected function substitute_values_pretty($text) {
00381         return str_replace($this->search, $this->prettyvalue, $text);
00382     }
00383 
00390     public function replace_expressions_in_text($text, $length = null, $format = null) {
00391         $vs = $this; // Can't see to use $this in a PHP closure.
00392         $text = preg_replace_callback('~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)}~',
00393                 function ($matches) use ($vs, $format, $length) {
00394                     return $vs->format_float($vs->calculate($matches[1]), $length, $format);
00395                 }, $text);
00396         return $this->substitute_values_pretty($text);
00397     }
00398 
00405     public function get_formula_errors($formula) {
00406         // Validates the formula submitted from the question edit page.
00407         // Returns false if everything is alright.
00408         // Otherwise it constructs an error message
00409         // Strip away dataset names
00410         while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
00411             $formula = str_replace($regs[0], '1', $formula);
00412         }
00413 
00414         // Strip away empty space and lowercase it
00415         $formula = strtolower(str_replace(' ', '', $formula));
00416 
00417         $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */
00418         $operatorornumber = "[$safeoperatorchar.0-9eE]";
00419 
00420         while (preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)" .
00421                 "\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~",
00422             $formula, $regs)) {
00423             switch ($regs[2]) {
00424                 // Simple parenthesis
00425                 case '':
00426                     if ((isset($regs[4]) && $regs[4]) || strlen($regs[3]) == 0) {
00427                         return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
00428                     }
00429                     break;
00430 
00431                     // Zero argument functions
00432                 case 'pi':
00433                     if ($regs[3]) {
00434                         return get_string('functiontakesnoargs', 'qtype_calculated', $regs[2]);
00435                     }
00436                     break;
00437 
00438                     // Single argument functions (the most common case)
00439                 case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
00440                 case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
00441                 case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
00442                 case 'exp': case 'expm1': case 'floor': case 'is_finite':
00443                 case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
00444                 case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
00445                 case 'tan': case 'tanh':
00446                     if (!empty($regs[4]) || empty($regs[3])) {
00447                         return get_string('functiontakesonearg', 'qtype_calculated', $regs[2]);
00448                     }
00449                     break;
00450 
00451                     // Functions that take one or two arguments
00452                 case 'log': case 'round':
00453                     if (!empty($regs[5]) || empty($regs[3])) {
00454                         return get_string('functiontakesoneortwoargs', 'qtype_calculated',
00455                                 $regs[2]);
00456                     }
00457                     break;
00458 
00459                     // Functions that must have two arguments
00460                 case 'atan2': case 'fmod': case 'pow':
00461                     if (!empty($regs[5]) || empty($regs[4])) {
00462                         return get_string('functiontakestwoargs', 'qtype_calculated', $regs[2]);
00463                     }
00464                     break;
00465 
00466                     // Functions that take two or more arguments
00467                 case 'min': case 'max':
00468                     if (empty($regs[4])) {
00469                         return get_string('functiontakesatleasttwo', 'qtype_calculated', $regs[2]);
00470                     }
00471                     break;
00472 
00473                 default:
00474                     return get_string('unsupportedformulafunction', 'qtype_calculated', $regs[2]);
00475             }
00476 
00477             // Exchange the function call with '1' and then chack for
00478             // another function call...
00479             if ($regs[1]) {
00480                 // The function call is proceeded by an operator
00481                 $formula = str_replace($regs[0], $regs[1] . '1', $formula);
00482             } else {
00483                 // The function call starts the formula
00484                 $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula);
00485             }
00486         }
00487 
00488         if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) {
00489             return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
00490         } else {
00491             // Formula just might be valid
00492             return false;
00493         }
00494     }
00495 }
 All Data Structures Namespaces Files Functions Variables Enumerations