[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Calculated question definition class. 19 * 20 * @package qtype 21 * @subpackage calculated 22 * @copyright 2011 The Open University 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 require_once($CFG->dirroot . '/question/type/questionbase.php'); 30 require_once($CFG->dirroot . '/question/type/numerical/question.php'); 31 32 /** 33 * Represents a calculated question. 34 * 35 * @copyright 2011 The Open University 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class qtype_calculated_question extends qtype_numerical_question 39 implements qtype_calculated_question_with_expressions { 40 41 /** @var qtype_calculated_dataset_loader helper for loading the dataset. */ 42 public $datasetloader; 43 44 /** @var qtype_calculated_variable_substituter stores the dataset we are using. */ 45 public $vs; 46 47 /** 48 * @var bool wheter the dataset item to use should be chose based on attempt 49 * start time, rather than randomly. 50 */ 51 public $synchronised; 52 53 public function start_attempt(question_attempt_step $step, $variant) { 54 qtype_calculated_question_helper::start_attempt($this, $step, $variant); 55 parent::start_attempt($step, $variant); 56 } 57 58 public function apply_attempt_state(question_attempt_step $step) { 59 qtype_calculated_question_helper::apply_attempt_state($this, $step); 60 parent::apply_attempt_state($step); 61 } 62 63 public function calculate_all_expressions() { 64 $this->questiontext = $this->vs->replace_expressions_in_text($this->questiontext); 65 $this->generalfeedback = $this->vs->replace_expressions_in_text($this->generalfeedback); 66 67 foreach ($this->answers as $ans) { 68 if ($ans->answer && $ans->answer !== '*') { 69 $ans->answer = $this->vs->calculate($ans->answer, 70 $ans->correctanswerlength, $ans->correctanswerformat); 71 } 72 $ans->feedback = $this->vs->replace_expressions_in_text($ans->feedback, 73 $ans->correctanswerlength, $ans->correctanswerformat); 74 } 75 } 76 77 public function get_num_variants() { 78 return $this->datasetloader->get_number_of_items(); 79 } 80 81 public function get_variants_selection_seed() { 82 if (!empty($this->synchronised) && 83 $this->datasetloader->datasets_are_synchronised($this->category)) { 84 return 'category' . $this->category; 85 } else { 86 return parent::get_variants_selection_seed(); 87 } 88 } 89 90 public function get_correct_response() { 91 $answer = $this->get_correct_answer(); 92 if (!$answer) { 93 return array(); 94 } 95 96 $response = array('answer' => $this->vs->format_float($answer->answer, 97 $answer->correctanswerlength, $answer->correctanswerformat)); 98 99 if ($this->has_separate_unit_field()) { 100 $response['unit'] = $this->ap->get_default_unit(); 101 } else if ($this->unitdisplay == qtype_numerical::UNITINPUT) { 102 $response['answer'] = $this->ap->add_unit($response['answer']); 103 } 104 105 return $response; 106 } 107 108 } 109 110 111 /** 112 * This interface defines the method that a quetsion type must implement if it 113 * is to work with {@link qtype_calculated_question_helper}. 114 * 115 * As well as this method, the class that implements this interface must have 116 * fields 117 * public $datasetloader; // of type qtype_calculated_dataset_loader 118 * public $vs; // of type qtype_calculated_variable_substituter 119 * 120 * @copyright 2011 The Open University 121 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 122 */ 123 interface qtype_calculated_question_with_expressions { 124 /** 125 * Replace all the expression in the question definition with the values 126 * computed from the selected dataset by calling $this->vs->calculate() and 127 * $this->vs->replace_expressions_in_text() on the parts of the question 128 * that require it. 129 */ 130 public function calculate_all_expressions(); 131 } 132 133 134 /** 135 * Helper class for questions that use datasets. Works with the interface 136 * {@link qtype_calculated_question_with_expressions} and the class 137 * {@link qtype_calculated_dataset_loader} to set up the value of each variable 138 * in start_attempt, and restore that in apply_attempt_state. 139 * 140 * @copyright 2011 The Open University 141 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 142 */ 143 abstract class qtype_calculated_question_helper { 144 public static function start_attempt( 145 qtype_calculated_question_with_expressions $question, 146 question_attempt_step $step, $variant) { 147 148 $question->vs = new qtype_calculated_variable_substituter( 149 $question->datasetloader->get_values($variant), 150 get_string('decsep', 'langconfig')); 151 $question->calculate_all_expressions(); 152 153 foreach ($question->vs->get_values() as $name => $value) { 154 $step->set_qt_var('_var_' . $name, $value); 155 } 156 } 157 158 public static function apply_attempt_state( 159 qtype_calculated_question_with_expressions $question, question_attempt_step $step) { 160 $values = array(); 161 foreach ($step->get_qt_data() as $name => $value) { 162 if (substr($name, 0, 5) === '_var_') { 163 $values[substr($name, 5)] = $value; 164 } 165 } 166 167 $question->vs = new qtype_calculated_variable_substituter( 168 $values, get_string('decsep', 'langconfig')); 169 $question->calculate_all_expressions(); 170 } 171 } 172 173 174 /** 175 * This class is responsible for loading the dataset that a question needs from 176 * the database. 177 * 178 * @copyright 2011 The Open University 179 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 180 */ 181 class qtype_calculated_dataset_loader { 182 /** @var int the id of the question we are helping. */ 183 protected $questionid; 184 185 /** @var int the id of the question we are helping. */ 186 protected $itemsavailable = null; 187 188 /** 189 * Constructor 190 * @param int $questionid the question to load datasets for. 191 */ 192 public function __construct($questionid) { 193 $this->questionid = $questionid; 194 } 195 196 /** 197 * Get the number of items (different values) in each dataset used by this 198 * question. This is the minimum number of items in any dataset used by this 199 * question. 200 * @return int the number of items available. 201 */ 202 public function get_number_of_items() { 203 global $DB; 204 205 if (is_null($this->itemsavailable)) { 206 $this->itemsavailable = $DB->get_field_sql(' 207 SELECT MIN(qdd.itemcount) 208 FROM {question_dataset_definitions} qdd 209 JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition 210 WHERE qd.question = ? 211 ', array($this->questionid), MUST_EXIST); 212 } 213 214 return $this->itemsavailable; 215 } 216 217 /** 218 * Actually query the database for the values. 219 * @param int $itemnumber which set of values to load. 220 * @return array name => value; 221 */ 222 protected function load_values($itemnumber) { 223 global $DB; 224 225 return $DB->get_records_sql_menu(' 226 SELECT qdd.name, qdi.value 227 FROM {question_dataset_items} qdi 228 JOIN {question_dataset_definitions} qdd ON qdd.id = qdi.definition 229 JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition 230 WHERE qd.question = ? 231 AND qdi.itemnumber = ? 232 ', array($this->questionid, $itemnumber)); 233 } 234 235 /** 236 * Load a particular set of values for each dataset used by this question. 237 * @param int $itemnumber which set of values to load. 238 * 0 < $itemnumber <= {@link get_number_of_items()}. 239 * @return array name => value. 240 */ 241 public function get_values($itemnumber) { 242 if ($itemnumber <= 0 || $itemnumber > $this->get_number_of_items()) { 243 $a = new stdClass(); 244 $a->id = $this->questionid; 245 $a->item = $itemnumber; 246 throw new moodle_exception('cannotgetdsfordependent', 'question', '', $a); 247 } 248 249 return $this->load_values($itemnumber); 250 } 251 252 public function datasets_are_synchronised($category) { 253 global $DB; 254 // We need to ensure that there are synchronised datasets, and that they 255 // all use the right category. 256 $categories = $DB->get_record_sql(' 257 SELECT MAX(qdd.category) AS max, 258 MIN(qdd.category) AS min 259 FROM {question_dataset_definitions} qdd 260 JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition 261 WHERE qd.question = ? 262 AND qdd.category <> 0 263 ', array($this->questionid)); 264 265 return $categories && $categories->max == $category && $categories->min == $category; 266 } 267 } 268 269 270 /** 271 * This class holds the current values of all the variables used by a calculated 272 * question. 273 * 274 * It can compute formulae using those values, and can substitute equations 275 * embedded in text. 276 * 277 * @copyright 2011 The Open University 278 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 279 */ 280 class qtype_calculated_variable_substituter { 281 282 /** @var array variable name => value */ 283 protected $values; 284 285 /** @var string character to use for the decimal point in displayed numbers. */ 286 protected $decimalpoint; 287 288 /** @var array variable names wrapped in {...}. Used by {@link substitute_values()}. */ 289 protected $search; 290 291 /** 292 * @var array variable values, with negative numbers wrapped in (...). 293 * Used by {@link substitute_values()}. 294 */ 295 protected $safevalue; 296 297 /** 298 * @var array variable values, with negative numbers wrapped in (...). 299 * Used by {@link substitute_values()}. 300 */ 301 protected $prettyvalue; 302 303 /** 304 * Constructor 305 * @param array $values variable name => value. 306 */ 307 public function __construct(array $values, $decimalpoint) { 308 $this->values = $values; 309 $this->decimalpoint = $decimalpoint; 310 311 // Prepare an array for {@link substitute_values()}. 312 $this->search = array(); 313 $this->replace = array(); 314 foreach ($values as $name => $value) { 315 if (!is_numeric($value)) { 316 $a = new stdClass(); 317 $a->name = '{' . $name . '}'; 318 $a->value = $value; 319 throw new moodle_exception('notvalidnumber', 'qtype_calculated', '', $a); 320 } 321 322 $this->search[] = '{' . $name . '}'; 323 $this->safevalue[] = '(' . $value . ')'; 324 $this->prettyvalue[] = $this->format_float($value); 325 } 326 } 327 328 /** 329 * Display a float properly formatted with a certain number of decimal places. 330 * @param number $x the number to format 331 * @param int $length restrict to this many decimal places or significant 332 * figures. If null, the number is not rounded. 333 * @param int format 1 => decimalformat, 2 => significantfigures. 334 * @return string formtted number. 335 */ 336 public function format_float($x, $length = null, $format = null) { 337 if (!is_null($length) && !is_null($format)) { 338 if ($format == '1' ) { // Answer is to have $length decimals. 339 // Decimal places. 340 $x = sprintf('%.' . $length . 'F', $x); 341 342 } else if ($x) { // Significant figures does only apply if the result is non-zero. 343 $answer = $x; 344 // Convert to positive answer. 345 if ($answer < 0) { 346 $answer = -$answer; 347 $sign = '-'; 348 } else { 349 $sign = ''; 350 } 351 352 // Determine the format 0.[1-9][0-9]* for the answer... 353 $p10 = 0; 354 while ($answer < 1) { 355 --$p10; 356 $answer *= 10; 357 } 358 while ($answer >= 1) { 359 ++$p10; 360 $answer /= 10; 361 } 362 // ... and have the answer rounded of to the correct length. 363 $answer = round($answer, $length); 364 365 // If we rounded up to 1.0, place the answer back into 0.[1-9][0-9]* format. 366 if ($answer >= 1) { 367 ++$p10; 368 $answer /= 10; 369 } 370 371 // Have the answer written on a suitable format. 372 // Either scientific or plain numeric. 373 if (-2 > $p10 || 4 < $p10) { 374 // Use scientific format. 375 $exponent = 'e'.--$p10; 376 $answer *= 10; 377 if (1 == $length) { 378 $x = $sign.$answer.$exponent; 379 } else { 380 // Attach additional zeros at the end of $answer. 381 $answer .= (1 == strlen($answer) ? '.' : '') 382 . '00000000000000000000000000000000000000000x'; 383 $x = $sign 384 .substr($answer, 0, $length +1).$exponent; 385 } 386 } else { 387 // Stick to plain numeric format. 388 $answer *= "1e{$p10}"; 389 if (0.1 <= $answer / "1e{$length}") { 390 $x = $sign.$answer; 391 } else { 392 // Could be an idea to add some zeros here. 393 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '') 394 . '00000000000000000000000000000000000000000x'; 395 $oklen = $length + ($p10 < 1 ? 2-$p10 : 1); 396 $x = $sign.substr($answer, 0, $oklen); 397 } 398 } 399 400 } else { 401 $x = 0.0; 402 } 403 } 404 return str_replace('.', $this->decimalpoint, $x); 405 } 406 407 /** 408 * Return an array of the variables and their values. 409 * @return array name => value. 410 */ 411 public function get_values() { 412 return $this->values; 413 } 414 415 /** 416 * Evaluate an expression using the variable values. 417 * @param string $expression the expression. A PHP expression with placeholders 418 * like {a} for where the variables need to go. 419 * @return float the computed result. 420 */ 421 public function calculate($expression) { 422 // Make sure no malicious code is present in the expression. Refer MDL-46148 for details. 423 if ($error = qtype_calculated_find_formula_errors($expression)) { 424 throw new moodle_exception('illegalformulasyntax', 'qtype_calculated', '', $error); 425 } 426 return $this->calculate_raw($this->substitute_values_for_eval($expression)); 427 } 428 429 /** 430 * Evaluate an expression after the variable values have been substituted. 431 * @param string $expression the expression. A PHP expression with placeholders 432 * like {a} for where the variables need to go. 433 * @return float the computed result. 434 */ 435 protected function calculate_raw($expression) { 436 try { 437 // In older PHP versions this this is a way to validate code passed to eval. 438 // The trick came from http://php.net/manual/en/function.eval.php. 439 if (@eval('return true; $result = ' . $expression . ';')) { 440 return eval('return ' . $expression . ';'); 441 } 442 } catch (Throwable $e) { 443 // PHP7 and later now throws ParseException and friends from eval(), 444 // which is much better. 445 } 446 // In either case of an invalid $expression, we end here. 447 throw new moodle_exception('illegalformulasyntax', 'qtype_calculated', '', $expression); 448 } 449 450 /** 451 * Substitute variable placehodlers like {a} with their value wrapped in (). 452 * @param string $expression the expression. A PHP expression with placeholders 453 * like {a} for where the variables need to go. 454 * @return string the expression with each placeholder replaced by the 455 * corresponding value. 456 */ 457 protected function substitute_values_for_eval($expression) { 458 return str_replace($this->search, $this->safevalue, $expression); 459 } 460 461 /** 462 * Substitute variable placehodlers like {a} with their value without wrapping 463 * the value in anything. 464 * @param string $text some content with placeholders 465 * like {a} for where the variables need to go. 466 * @return string the expression with each placeholder replaced by the 467 * corresponding value. 468 */ 469 protected function substitute_values_pretty($text) { 470 return str_replace($this->search, $this->prettyvalue, $text); 471 } 472 473 /** 474 * Replace any embedded variables (like {a}) or formulae (like {={a} + {b}}) 475 * in some text with the corresponding values. 476 * @param string $text the text to process. 477 * @return string the text with values substituted. 478 */ 479 public function replace_expressions_in_text($text, $length = null, $format = null) { 480 $vs = $this; // Can't use $this in a PHP closure. 481 $text = preg_replace_callback(qtype_calculated::FORMULAS_IN_TEXT_REGEX, 482 function ($matches) use ($vs, $format, $length) { 483 return $vs->format_float($vs->calculate($matches[1]), $length, $format); 484 }, $text); 485 return $this->substitute_values_pretty($text); 486 } 487 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |