|
Moodle
2.2.1
http://www.collinsharper.com
|
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 }