Merge commit 'catalyst/MOODLE_19_STABLE' into mdl19-linuxchix
[moodle-linuxchix.git] / question / type / numerical / questiontype.php
blobd4011b39cd7d8229fdeba63f8ede786abe53fff0
1 <?php
2 /**
3 * @version $Id$
4 * @author Martin Dougiamas and many others. Tim Hunt.
5 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
6 * @package questionbank
7 * @subpackage questiontypes
8 *//** */
10 require_once("$CFG->dirroot/question/type/shortanswer/questiontype.php");
12 /**
13 * NUMERICAL QUESTION TYPE CLASS
15 * This class contains some special features in order to make the
16 * question type embeddable within a multianswer (cloze) question
18 * This question type behaves like shortanswer in most cases.
19 * Therefore, it extends the shortanswer question type...
20 * @package questionbank
21 * @subpackage questiontypes
23 class question_numerical_qtype extends question_shortanswer_qtype {
25 function name() {
26 return 'numerical';
29 function get_question_options(&$question) {
30 // Get the question answers and their respective tolerances
31 // Note: question_numerical is an extension of the answer table rather than
32 // the question table as is usually the case for qtype
33 // specific tables.
34 global $CFG;
35 if (!$question->options->answers = get_records_sql(
36 "SELECT a.*, n.tolerance " .
37 "FROM {$CFG->prefix}question_answers a, " .
38 " {$CFG->prefix}question_numerical n " .
39 "WHERE a.question = $question->id " .
40 " AND a.id = n.answer " .
41 "ORDER BY a.id ASC")) {
42 notify('Error: Missing question answer for numerical question ' . $question->id . '!');
43 return false;
45 $this->get_numerical_units($question);
47 // If units are defined we strip off the default unit from the answer, if
48 // it is present. (Required for compatibility with the old code and DB).
49 if ($defaultunit = $this->get_default_numerical_unit($question)) {
50 foreach($question->options->answers as $key => $val) {
51 $answer = trim($val->answer);
52 $length = strlen($defaultunit->unit);
53 if ($length && substr($answer, -$length) == $defaultunit->unit) {
54 $question->options->answers[$key]->answer =
55 substr($answer, 0, strlen($answer)-$length);
59 return true;
62 function get_numerical_units(&$question) {
63 if ($units = get_records('question_numerical_units',
64 'question', $question->id, 'id ASC')) {
65 $units = array_values($units);
66 } else {
67 $units = array();
69 foreach ($units as $key => $unit) {
70 $units[$key]->multiplier = clean_param($unit->multiplier, PARAM_NUMBER);
72 $question->options->units = $units;
73 return true;
76 function get_default_numerical_unit(&$question) {
77 if (isset($question->options->units[0])) {
78 foreach ($question->options->units as $unit) {
79 if (abs($unit->multiplier - 1.0) < '1.0e-' . ini_get('precision')) {
80 return $unit;
84 return false;
87 /**
88 * Save the units and the answers associated with this question.
90 function save_question_options($question) {
91 // Get old versions of the objects
92 if (!$oldanswers = get_records('question_answers', 'question', $question->id, 'id ASC')) {
93 $oldanswers = array();
96 if (!$oldoptions = get_records('question_numerical', 'question', $question->id, 'answer ASC')) {
97 $oldoptions = array();
100 // Save the units.
101 $result = $this->save_numerical_units($question);
102 if (isset($result->error)) {
103 return $result;
104 } else {
105 $units = &$result->units;
108 // Insert all the new answers
109 foreach ($question->answer as $key => $dataanswer) {
110 if ( !( trim($dataanswer)=='' && $question->fraction[$key]== 0 && trim($question->feedback[$key])=='')) {
111 $answer = new stdClass;
112 $answer->question = $question->id;
113 if (trim($dataanswer) == '*') {
114 $answer->answer = '*';
115 } else {
116 $answer->answer = $this->apply_unit($dataanswer, $units);
117 if ($answer->answer === false) {
118 $result->notice = get_string('invalidnumericanswer', 'quiz');
121 $answer->fraction = $question->fraction[$key];
122 $answer->feedback = trim($question->feedback[$key]);
124 if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it
125 $answer->id = $oldanswer->id;
126 if (! update_record("question_answers", $answer)) {
127 $result->error = "Could not update quiz answer! (id=$answer->id)";
128 return $result;
130 } else { // This is a completely new answer
131 if (! $answer->id = insert_record("question_answers", $answer)) {
132 $result->error = "Could not insert quiz answer!";
133 return $result;
137 // Set up the options object
138 if (!$options = array_shift($oldoptions)) {
139 $options = new stdClass;
141 $options->question = $question->id;
142 $options->answer = $answer->id;
143 if (trim($question->tolerance[$key]) == '') {
144 $options->tolerance = '';
145 } else {
146 $options->tolerance = $this->apply_unit($question->tolerance[$key], $units);
147 if ($options->tolerance === false) {
148 $result->notice = get_string('invalidnumerictolerance', 'quiz');
152 // Save options
153 if (isset($options->id)) { // reusing existing record
154 if (! update_record('question_numerical', $options)) {
155 $result->error = "Could not update quiz numerical options! (id=$options->id)";
156 return $result;
158 } else { // new options
159 if (! insert_record('question_numerical', $options)) {
160 $result->error = "Could not insert quiz numerical options!";
161 return $result;
166 // delete old answer records
167 if (!empty($oldanswers)) {
168 foreach($oldanswers as $oa) {
169 delete_records('question_answers', 'id', $oa->id);
173 // delete old answer records
174 if (!empty($oldoptions)) {
175 foreach($oldoptions as $oo) {
176 delete_records('question_numerical', 'id', $oo->id);
180 // Report any problems.
181 if (!empty($result->notice)) {
182 return $result;
185 return true;
188 function save_numerical_units($question) {
189 $result = new stdClass;
191 // Delete the units previously saved for this question.
192 delete_records('question_numerical_units', 'question', $question->id);
194 // Save the new units.
195 $units = array();
196 foreach ($question->multiplier as $i => $multiplier) {
197 // Discard any unit which doesn't specify the unit or the multiplier
198 if (!empty($question->multiplier[$i]) && !empty($question->unit[$i])) {
199 $units[$i] = new stdClass;
200 $units[$i]->question = $question->id;
201 $units[$i]->multiplier = $this->apply_unit($question->multiplier[$i], array());
202 $units[$i]->unit = $question->unit[$i];
203 if (! insert_record('question_numerical_units', $units[$i])) {
204 $result->error = 'Unable to save unit ' . $units[$i]->unit . ' to the Databse';
205 return $result;
209 unset($question->multiplier, $question->unit);
211 $result->units = &$units;
212 return $result;
216 * Deletes question from the question-type specific tables
218 * @return boolean Success/Failure
219 * @param object $question The question being deleted
221 function delete_question($questionid) {
222 delete_records("question_numerical", "question", $questionid);
223 delete_records("question_numerical_units", "question", $questionid);
224 return true;
227 function compare_responses(&$question, $state, $teststate) {
228 if (isset($state->responses['']) && isset($teststate->responses[''])) {
229 return $state->responses[''] == $teststate->responses[''];
231 return false;
235 * Checks whether a response matches a given answer, taking the tolerance
236 * and units into account. Returns a true for if a response matches the
237 * answer, false if it doesn't.
239 function test_response(&$question, &$state, $answer) {
240 // Deal with the match anything answer.
241 if ($answer->answer == '*') {
242 return true;
245 $response = $this->apply_unit(stripslashes($state->responses['']), $question->options->units);
247 if ($response === false) {
248 return false; // The student did not type a number.
251 // The student did type a number, so check it with tolerances.
252 $this->get_tolerance_interval($answer);
253 return ($answer->min <= $response && $response <= $answer->max);
256 // ULPGC ecastro
257 function check_response(&$question, &$state){
258 $answers = &$question->options->answers;
259 foreach($answers as $aid => $answer) {
260 if($this->test_response($question, $state, $answer)) {
261 return $aid;
264 return false;
267 function get_correct_responses(&$question, &$state) {
268 $correct = parent::get_correct_responses($question, $state);
269 $unit = $this->get_default_numerical_unit($question);
270 if (isset($correct['']) && $correct[''] != '*' && $unit) {
271 $correct[''] .= ' '.$unit->unit;
273 return $correct;
276 // ULPGC ecastro
277 function get_all_responses(&$question, &$state) {
278 $result = new stdClass;
279 $answers = array();
280 $unit = $this->get_default_numerical_unit($question);
281 if (is_array($question->options->answers)) {
282 foreach ($question->options->answers as $aid=>$answer) {
283 $r = new stdClass;
284 $r->answer = $answer->answer;
285 $r->credit = $answer->fraction;
286 $this->get_tolerance_interval($answer);
287 if ($r->answer != '*' && $unit) {
288 $r->answer .= ' ' . $unit->unit;
290 if ($answer->max != $answer->min) {
291 $max = "$answer->max"; //format_float($answer->max, 2);
292 $min = "$answer->min"; //format_float($answer->max, 2);
293 $r->answer .= ' ('.$min.'..'.$max.')';
295 $answers[$aid] = $r;
298 $result->id = $question->id;
299 $result->responses = $answers;
300 return $result;
303 function get_tolerance_interval(&$answer) {
304 // No tolerance
305 if (empty($answer->tolerance)) {
306 $answer->tolerance = 0;
309 // Calculate the interval of correct responses (min/max)
310 if (!isset($answer->tolerancetype)) {
311 $answer->tolerancetype = 2; // nominal
314 // We need to add a tiny fraction depending on the set precision to make the
315 // comparison work correctly. Otherwise seemingly equal values can yield
316 // false. (fixes bug #3225)
317 $tolerance = (float)$answer->tolerance + ("1.0e-".ini_get('precision'));
318 switch ($answer->tolerancetype) {
319 case '1': case 'relative':
320 /// Recalculate the tolerance and fall through
321 /// to the nominal case:
322 $tolerance = $answer->answer * $tolerance;
323 // Do not fall through to the nominal case because the tiny fraction is a factor of the answer
324 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
325 $max = $answer->answer + $tolerance;
326 $min = $answer->answer - $tolerance;
327 break;
328 case '2': case 'nominal':
329 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
330 // $answer->tolerance 0 or something else
331 if ((float)$answer->tolerance == 0.0 && abs((float)$answer->answer) <= $tolerance ){
332 $tolerance = (float) ("1.0e-".ini_get('precision')) * abs((float)$answer->answer) ; //tiny fraction
333 } else if ((float)$answer->tolerance != 0.0 && abs((float)$answer->tolerance) < abs((float)$answer->answer) && abs((float)$answer->answer) <= $tolerance){
334 $tolerance = (1+("1.0e-".ini_get('precision')) )* abs((float) $answer->tolerance) ;//tiny fraction
337 $max = $answer->answer + $tolerance;
338 $min = $answer->answer - $tolerance;
339 break;
340 case '3': case 'geometric':
341 $quotient = 1 + abs($tolerance);
342 $max = $answer->answer * $quotient;
343 $min = $answer->answer / $quotient;
344 break;
345 default:
346 error("Unknown tolerance type $answer->tolerancetype");
349 $answer->min = $min;
350 $answer->max = $max;
351 return true;
355 * Checks if the $rawresponse has a unit and applys it if appropriate.
357 * @param string $rawresponse The response string to be converted to a float.
358 * @param array $units An array with the defined units, where the
359 * unit is the key and the multiplier the value.
360 * @return float The rawresponse with the unit taken into
361 * account as a float.
363 function apply_unit($rawresponse, $units) {
364 // Make units more useful
365 $tmpunits = array();
366 foreach ($units as $unit) {
367 $tmpunits[$unit->unit] = $unit->multiplier;
369 // remove spaces and normalise decimal places.
370 $search = array(' ', ',');
371 $replace = array('', '.');
372 $rawresponse = str_replace($search, $replace, trim($rawresponse));
374 // Apply any unit that is present.
375 if (ereg('^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$',
376 $rawresponse, $responseparts)) {
378 if (!empty($responseparts[5])) {
380 if (isset($tmpunits[$responseparts[5]])) {
381 // Valid number with unit.
382 return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
383 } else {
384 // Valid number with invalid unit. Must be wrong.
385 return false;
388 } else {
389 // Valid number without unit.
390 return (float)$responseparts[1];
393 // Invalid number. Must be wrong.
394 return false;
397 /// BACKUP FUNCTIONS ////////////////////////////
400 * Backup the data in the question
402 * This is used in question/backuplib.php
404 function backup($bf,$preferences,$question,$level=6) {
406 $status = true;
408 $numericals = get_records('question_numerical', 'question', $question, 'id ASC');
409 //If there are numericals
410 if ($numericals) {
411 //Iterate over each numerical
412 foreach ($numericals as $numerical) {
413 $status = fwrite ($bf,start_tag("NUMERICAL",$level,true));
414 //Print numerical contents
415 fwrite ($bf,full_tag("ANSWER",$level+1,false,$numerical->answer));
416 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$numerical->tolerance));
417 //Now backup numerical_units
418 $status = question_backup_numerical_units($bf,$preferences,$question,7);
419 $status = fwrite ($bf,end_tag("NUMERICAL",$level,true));
421 //Now print question_answers
422 $status = question_backup_answers($bf,$preferences,$question);
424 return $status;
427 /// RESTORE FUNCTIONS /////////////////
430 * Restores the data in the question
432 * This is used in question/restorelib.php
434 function restore($old_question_id,$new_question_id,$info,$restore) {
436 $status = true;
438 //Get the numerical array
439 if (isset($info['#']['NUMERICAL'])) {
440 $numericals = $info['#']['NUMERICAL'];
441 } else {
442 $numericals = array();
445 //Iterate over numericals
446 for($i = 0; $i < sizeof($numericals); $i++) {
447 $num_info = $numericals[$i];
449 //Now, build the question_numerical record structure
450 $numerical = new stdClass;
451 $numerical->question = $new_question_id;
452 $numerical->answer = backup_todb($num_info['#']['ANSWER']['0']['#']);
453 $numerical->tolerance = backup_todb($num_info['#']['TOLERANCE']['0']['#']);
455 //We have to recode the answer field
456 $answer = backup_getid($restore->backup_unique_code,"question_answers",$numerical->answer);
457 if ($answer) {
458 $numerical->answer = $answer->new_id;
461 //The structure is equal to the db, so insert the question_numerical
462 $newid = insert_record ("question_numerical", $numerical);
464 //Do some output
465 if (($i+1) % 50 == 0) {
466 if (!defined('RESTORE_SILENTLY')) {
467 echo ".";
468 if (($i+1) % 1000 == 0) {
469 echo "<br />";
472 backup_flush(300);
475 //Now restore numerical_units
476 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$num_info,$restore);
478 if (!$newid) {
479 $status = false;
483 return $status;
487 * Runs all the code required to set up and save an essay question for testing purposes.
488 * Alternate DB table prefix may be used to facilitate data deletion.
490 function generate_test($name, $courseid = null) {
491 list($form, $question) = default_questiontype::generate_test($name, $courseid);
492 $question->category = $form->category;
494 $form->questiontext = "What is 674 * 36?";
495 $form->generalfeedback = "Thank you";
496 $form->penalty = 0.1;
497 $form->defaultgrade = 1;
498 $form->noanswers = 3;
499 $form->answer = array('24264', '24264', '1');
500 $form->tolerance = array(10, 100, 0);
501 $form->fraction = array(1, 0.5, 0);
502 $form->nounits = 2;
503 $form->unit = array(0 => null, 1 => null);
504 $form->multiplier = array(1, 0);
505 $form->feedback = array('Very good', 'Close, but not quite there', 'Well at least you tried....');
507 if ($courseid) {
508 $course = get_record('course', 'id', $courseid);
511 return $this->save_question($question, $form, $course);
516 // INITIATION - Without this line the question type is not in use.
517 question_register_questiontype(new question_numerical_qtype());