Updated the HEAD build version to 20080828
[moodle.git] / question / type / numerical / questiontype.php
CommitLineData
1fe641f7 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
1976496e 6 * @package questionbank
7 * @subpackage questiontypes
1fe641f7 8 *//** */
516cf3eb 9
aaae75b0 10require_once("$CFG->dirroot/question/type/shortanswer/questiontype.php");
516cf3eb 11
1fe641f7 12/**
13 * NUMERICAL QUESTION TYPE CLASS
14 *
15 * This class contains some special features in order to make the
16 * question type embeddable within a multianswer (cloze) question
17 *
18 * This question type behaves like shortanswer in most cases.
19 * Therefore, it extends the shortanswer question type...
1976496e 20 * @package questionbank
21 * @subpackage questiontypes
1fe641f7 22 */
32a189d6 23class question_numerical_qtype extends question_shortanswer_qtype {
516cf3eb 24
25 function name() {
26 return 'numerical';
27 }
28
29 function get_question_options(&$question) {
30 // Get the question answers and their respective tolerances
32a189d6 31 // Note: question_numerical is an extension of the answer table rather than
4f48fb42 32 // the question table as is usually the case for qtype
516cf3eb 33 // specific tables.
f34488b2 34 global $CFG, $DB;
35 if (!$question->options->answers = $DB->get_records_sql(
516cf3eb 36 "SELECT a.*, n.tolerance " .
f34488b2 37 "FROM {question_answers} a, " .
38 " {question_numerical} n " .
39 "WHERE a.question = ? " .
026bec73 40 " AND a.id = n.answer " .
f34488b2 41 "ORDER BY a.id ASC", array($question->id))) {
df06c4bb 42 notify('Error: Missing question answer for numerical question ' . $question->id . '!');
516cf3eb 43 return false;
44 }
45 $this->get_numerical_units($question);
46
5a14d563 47 // If units are defined we strip off the default unit from the answer, if
516cf3eb 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);
1fe641f7 53 if ($length && substr($answer, -$length) == $defaultunit->unit) {
516cf3eb 54 $question->options->answers[$key]->answer =
1fe641f7 55 substr($answer, 0, strlen($answer)-$length);
516cf3eb 56 }
57 }
58 }
59 return true;
60 }
61
62 function get_numerical_units(&$question) {
f34488b2 63 global $DB;
64 if ($units = $DB->get_records('question_numerical_units', array('question' => $question->id), 'id ASC')) {
3a513ba4 65 $units = array_values($units);
516cf3eb 66 } else {
b4d7d27c 67 $units = array();
516cf3eb 68 }
b4d7d27c 69 foreach ($units as $key => $unit) {
70 $units[$key]->multiplier = clean_param($unit->multiplier, PARAM_NUMBER);
71 }
72 $question->options->units = $units;
516cf3eb 73 return true;
74 }
75
76 function get_default_numerical_unit(&$question) {
ed0ba6da 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;
516cf3eb 81 }
82 }
83 }
ed0ba6da 84 return false;
516cf3eb 85 }
86
1fe641f7 87 /**
88 * Save the units and the answers associated with this question.
89 */
516cf3eb 90 function save_question_options($question) {
f34488b2 91 global $DB;
516cf3eb 92 // Get old versions of the objects
f34488b2 93 if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) {
516cf3eb 94 $oldanswers = array();
95 }
96
f34488b2 97 if (!$oldoptions = $DB->get_records('question_numerical', array('question' => $question->id), 'answer ASC')) {
516cf3eb 98 $oldoptions = array();
99 }
100
1fe641f7 101 // Save the units.
516cf3eb 102 $result = $this->save_numerical_units($question);
103 if (isset($result->error)) {
104 return $result;
105 } else {
106 $units = &$result->units;
107 }
108
109 // Insert all the new answers
110 foreach ($question->answer as $key => $dataanswer) {
47c03ecd 111 if ( !( trim($dataanswer)=='' && $question->fraction[$key]== 0 && trim($question->feedback[$key])=='')) {
516cf3eb 112 $answer = new stdClass;
113 $answer->question = $question->id;
55894a42 114 if (trim($dataanswer) == '*') {
115 $answer->answer = '*';
1fe641f7 116 } else {
117 $answer->answer = $this->apply_unit($dataanswer, $units);
118 if ($answer->answer === false) {
119 $result->notice = get_string('invalidnumericanswer', 'quiz');
120 }
121 }
516cf3eb 122 $answer->fraction = $question->fraction[$key];
123 $answer->feedback = trim($question->feedback[$key]);
124
125 if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it
126 $answer->id = $oldanswer->id;
f34488b2 127 if (! $DB->update_record("question_answers", $answer)) {
516cf3eb 128 $result->error = "Could not update quiz answer! (id=$answer->id)";
129 return $result;
130 }
1fe641f7 131 } else { // This is a completely new answer
f34488b2 132 if (! $answer->id = $DB->insert_record("question_answers", $answer)) {
516cf3eb 133 $result->error = "Could not insert quiz answer!";
134 return $result;
135 }
136 }
137
138 // Set up the options object
139 if (!$options = array_shift($oldoptions)) {
140 $options = new stdClass;
141 }
142 $options->question = $question->id;
143 $options->answer = $answer->id;
1fe641f7 144 if (trim($question->tolerance[$key]) == '') {
145 $options->tolerance = '';
146 } else {
147 $options->tolerance = $this->apply_unit($question->tolerance[$key], $units);
148 if ($options->tolerance === false) {
149 $result->notice = get_string('invalidnumerictolerance', 'quiz');
150 }
151 }
f34488b2 152
516cf3eb 153 // Save options
154 if (isset($options->id)) { // reusing existing record
f34488b2 155 if (! $DB->update_record('question_numerical', $options)) {
516cf3eb 156 $result->error = "Could not update quiz numerical options! (id=$options->id)";
157 return $result;
158 }
159 } else { // new options
f34488b2 160 if (! $DB->insert_record('question_numerical', $options)) {
516cf3eb 161 $result->error = "Could not insert quiz numerical options!";
162 return $result;
163 }
164 }
1fe641f7 165 }
166 }
167 // delete old answer records
168 if (!empty($oldanswers)) {
169 foreach($oldanswers as $oa) {
f34488b2 170 $DB->delete_records('question_answers', array('id' => $oa->id));
1fe641f7 171 }
172 }
516cf3eb 173
1fe641f7 174 // delete old answer records
175 if (!empty($oldoptions)) {
176 foreach($oldoptions as $oo) {
f34488b2 177 $DB->delete_records('question_numerical', array('id' => $oo->id));
516cf3eb 178 }
179 }
1fe641f7 180
181 // Report any problems.
182 if (!empty($result->notice)) {
183 return $result;
184 }
1fe641f7 185 return true;
516cf3eb 186 }
187
188 function save_numerical_units($question) {
f34488b2 189 global $DB;
ed0ba6da 190 $result = new stdClass;
516cf3eb 191
ed0ba6da 192 // Delete the units previously saved for this question.
f34488b2 193 $DB->delete_records('question_numerical_units', array('question' => $question->id));
ed0ba6da 194
195 // Save the new units.
516cf3eb 196 $units = array();
ed0ba6da 197 foreach ($question->multiplier as $i => $multiplier) {
516cf3eb 198 // Discard any unit which doesn't specify the unit or the multiplier
199 if (!empty($question->multiplier[$i]) && !empty($question->unit[$i])) {
ed0ba6da 200 $units[$i] = new stdClass;
516cf3eb 201 $units[$i]->question = $question->id;
1fe641f7 202 $units[$i]->multiplier = $this->apply_unit($question->multiplier[$i], array());
516cf3eb 203 $units[$i]->unit = $question->unit[$i];
f34488b2 204 if (! $DB->insert_record('question_numerical_units', $units[$i])) {
ed0ba6da 205 $result->error = 'Unable to save unit ' . $units[$i]->unit . ' to the Databse';
206 return $result;
207 }
516cf3eb 208 }
209 }
210 unset($question->multiplier, $question->unit);
211
516cf3eb 212 $result->units = &$units;
213 return $result;
214 }
215
216 /**
1fe641f7 217 * Deletes question from the question-type specific tables
218 *
219 * @return boolean Success/Failure
220 * @param object $question The question being deleted
221 */
90c3f310 222 function delete_question($questionid) {
f34488b2 223 global $DB;
224 $DB->delete_records("question_numerical", array("question" => $questionid));
225 $DB->delete_records("question_numerical_units", array("question" => $questionid));
516cf3eb 226 return true;
227 }
228
5a14d563 229 function compare_responses(&$question, $state, $teststate) {
230 if (isset($state->responses['']) && isset($teststate->responses[''])) {
231 return $state->responses[''] == $teststate->responses[''];
232 }
233 return false;
516cf3eb 234 }
235
1fe641f7 236 /**
237 * Checks whether a response matches a given answer, taking the tolerance
238 * and units into account. Returns a true for if a response matches the
239 * answer, false if it doesn't.
240 */
516cf3eb 241 function test_response(&$question, &$state, $answer) {
55894a42 242 // Deal with the match anything answer.
243 if ($answer->answer == '*') {
244 return true;
516cf3eb 245 }
246
294ce987 247 $response = $this->apply_unit($state->responses[''], $question->options->units);
1fe641f7 248
249 if ($response === false) {
250 return false; // The student did not type a number.
516cf3eb 251 }
1fe641f7 252
253 // The student did type a number, so check it with tolerances.
254 $this->get_tolerance_interval($answer);
255 return ($answer->min <= $response && $response <= $answer->max);
516cf3eb 256 }
257
258 // ULPGC ecastro
259 function check_response(&$question, &$state){
260 $answers = &$question->options->answers;
261 foreach($answers as $aid => $answer) {
262 if($this->test_response($question, $state, $answer)) {
263 return $aid;
264 }
265 }
266 return false;
267 }
268
516cf3eb 269 function get_correct_responses(&$question, &$state) {
270 $correct = parent::get_correct_responses($question, $state);
c85aea03 271 $unit = $this->get_default_numerical_unit($question);
272 if (isset($correct['']) && $correct[''] != '*' && $unit) {
516cf3eb 273 $correct[''] .= ' '.$unit->unit;
274 }
275 return $correct;
276 }
277
278 // ULPGC ecastro
279 function get_all_responses(&$question, &$state) {
1fe641f7 280 $result = new stdClass;
281 $answers = array();
516cf3eb 282 $unit = $this->get_default_numerical_unit($question);
283 if (is_array($question->options->answers)) {
284 foreach ($question->options->answers as $aid=>$answer) {
1fe641f7 285 $r = new stdClass;
516cf3eb 286 $r->answer = $answer->answer;
287 $r->credit = $answer->fraction;
288 $this->get_tolerance_interval($answer);
55894a42 289 if ($r->answer != '*' && $unit) {
290 $r->answer .= ' ' . $unit->unit;
516cf3eb 291 }
292 if ($answer->max != $answer->min) {
293 $max = "$answer->max"; //format_float($answer->max, 2);
294 $min = "$answer->min"; //format_float($answer->max, 2);
295 $r->answer .= ' ('.$min.'..'.$max.')';
296 }
297 $answers[$aid] = $r;
298 }
516cf3eb 299 }
300 $result->id = $question->id;
301 $result->responses = $answers;
302 return $result;
303 }
304
305 function get_tolerance_interval(&$answer) {
306 // No tolerance
307 if (empty($answer->tolerance)) {
1fe641f7 308 $answer->tolerance = 0;
516cf3eb 309 }
310
311 // Calculate the interval of correct responses (min/max)
312 if (!isset($answer->tolerancetype)) {
313 $answer->tolerancetype = 2; // nominal
314 }
315
223ad0b9 316 // We need to add a tiny fraction depending on the set precision to make the
516cf3eb 317 // comparison work correctly. Otherwise seemingly equal values can yield
318 // false. (fixes bug #3225)
223ad0b9 319 $tolerance = (float)$answer->tolerance + ("1.0e-".ini_get('precision'));
516cf3eb 320 switch ($answer->tolerancetype) {
321 case '1': case 'relative':
322 /// Recalculate the tolerance and fall through
323 /// to the nominal case:
324 $tolerance = $answer->answer * $tolerance;
dcd4192a 325 // Do not fall through to the nominal case because the tiny fraction is a factor of the answer
326 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
327 $max = $answer->answer + $tolerance;
328 $min = $answer->answer - $tolerance;
329 break;
516cf3eb 330 case '2': case 'nominal':
331 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
dcd4192a 332 // $answer->tolerance 0 or something else
333 if ((float)$answer->tolerance == 0.0 && abs((float)$answer->answer) <= $tolerance ){
f34488b2 334 $tolerance = (float) ("1.0e-".ini_get('precision')) * abs((float)$answer->answer) ; //tiny fraction
dcd4192a 335 } else if ((float)$answer->tolerance != 0.0 && abs((float)$answer->tolerance) < abs((float)$answer->answer) && abs((float)$answer->answer) <= $tolerance){
f34488b2 336 $tolerance = (1+("1.0e-".ini_get('precision')) )* abs((float) $answer->tolerance) ;//tiny fraction
337 }
338
516cf3eb 339 $max = $answer->answer + $tolerance;
340 $min = $answer->answer - $tolerance;
341 break;
ed0ba6da 342 case '3': case 'geometric':
516cf3eb 343 $quotient = 1 + abs($tolerance);
344 $max = $answer->answer * $quotient;
345 $min = $answer->answer / $quotient;
346 break;
347 default:
0b4f4187 348 print_error('unknowntolerance', 'question', '', $answer->tolerancetype);
516cf3eb 349 }
350
351 $answer->min = $min;
352 $answer->max = $max;
353 return true;
354 }
355
356 /**
1fe641f7 357 * Checks if the $rawresponse has a unit and applys it if appropriate.
358 *
359 * @param string $rawresponse The response string to be converted to a float.
360 * @param array $units An array with the defined units, where the
361 * unit is the key and the multiplier the value.
362 * @return float The rawresponse with the unit taken into
363 * account as a float.
364 */
516cf3eb 365 function apply_unit($rawresponse, $units) {
366 // Make units more useful
367 $tmpunits = array();
368 foreach ($units as $unit) {
369 $tmpunits[$unit->unit] = $unit->multiplier;
370 }
1fe641f7 371 // remove spaces and normalise decimal places.
516cf3eb 372 $search = array(' ', ',');
373 $replace = array('', '.');
1fe641f7 374 $rawresponse = str_replace($search, $replace, trim($rawresponse));
f34488b2 375
1fe641f7 376 // Apply any unit that is present.
377 if (ereg('^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$',
378 $rawresponse, $responseparts)) {
f34488b2 379
1fe641f7 380 if (!empty($responseparts[5])) {
f34488b2 381
1fe641f7 382 if (isset($tmpunits[$responseparts[5]])) {
383 // Valid number with unit.
384 return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
385 } else {
386 // Valid number with invalid unit. Must be wrong.
387 return false;
388 }
389
516cf3eb 390 } else {
1fe641f7 391 // Valid number without unit.
516cf3eb 392 return (float)$responseparts[1];
393 }
394 }
1fe641f7 395 // Invalid number. Must be wrong.
396 return false;
516cf3eb 397 }
f34488b2 398
1fe641f7 399 /// BACKUP FUNCTIONS ////////////////////////////
c5d94c41 400
1fe641f7 401 /**
c5d94c41 402 * Backup the data in the question
403 *
404 * This is used in question/backuplib.php
405 */
406 function backup($bf,$preferences,$question,$level=6) {
f34488b2 407 global $DB;
c5d94c41 408
409 $status = true;
410
f34488b2 411 $numericals = $DB->get_records('question_numerical', array('question' => $question), 'id ASC');
c5d94c41 412 //If there are numericals
413 if ($numericals) {
414 //Iterate over each numerical
415 foreach ($numericals as $numerical) {
416 $status = fwrite ($bf,start_tag("NUMERICAL",$level,true));
417 //Print numerical contents
418 fwrite ($bf,full_tag("ANSWER",$level+1,false,$numerical->answer));
419 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$numerical->tolerance));
420 //Now backup numerical_units
421 $status = question_backup_numerical_units($bf,$preferences,$question,7);
422 $status = fwrite ($bf,end_tag("NUMERICAL",$level,true));
423 }
424 //Now print question_answers
425 $status = question_backup_answers($bf,$preferences,$question);
426 }
427 return $status;
428 }
429
1fe641f7 430 /// RESTORE FUNCTIONS /////////////////
315559d3 431
1fe641f7 432 /**
315559d3 433 * Restores the data in the question
434 *
435 * This is used in question/restorelib.php
436 */
437 function restore($old_question_id,$new_question_id,$info,$restore) {
9db7dab2 438 global $DB;
315559d3 439
440 $status = true;
441
442 //Get the numerical array
27cabbe6 443 if (isset($info['#']['NUMERICAL'])) {
444 $numericals = $info['#']['NUMERICAL'];
445 } else {
446 $numericals = array();
447 }
315559d3 448
449 //Iterate over numericals
450 for($i = 0; $i < sizeof($numericals); $i++) {
451 $num_info = $numericals[$i];
452
453 //Now, build the question_numerical record structure
1fe641f7 454 $numerical = new stdClass;
315559d3 455 $numerical->question = $new_question_id;
456 $numerical->answer = backup_todb($num_info['#']['ANSWER']['0']['#']);
457 $numerical->tolerance = backup_todb($num_info['#']['TOLERANCE']['0']['#']);
458
55894a42 459 //We have to recode the answer field
315559d3 460 $answer = backup_getid($restore->backup_unique_code,"question_answers",$numerical->answer);
461 if ($answer) {
462 $numerical->answer = $answer->new_id;
463 }
464
465 //The structure is equal to the db, so insert the question_numerical
9db7dab2 466 $newid = $DB->insert_record ("question_numerical", $numerical);
315559d3 467
468 //Do some output
469 if (($i+1) % 50 == 0) {
470 if (!defined('RESTORE_SILENTLY')) {
471 echo ".";
472 if (($i+1) % 1000 == 0) {
473 echo "<br />";
474 }
475 }
476 backup_flush(300);
477 }
478
479 //Now restore numerical_units
480 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$num_info,$restore);
481
482 if (!$newid) {
483 $status = false;
484 }
485 }
486
487 return $status;
488 }
489
b9bd6da4 490 /**
491 * Runs all the code required to set up and save an essay question for testing purposes.
492 * Alternate DB table prefix may be used to facilitate data deletion.
493 */
494 function generate_test($name, $courseid = null) {
495 global $DB;
496 list($form, $question) = default_questiontype::generate_test($name, $courseid);
497 $question->category = $form->category;
498
499 $form->questiontext = "What is 674 * 36?";
500 $form->generalfeedback = "Thank you";
501 $form->penalty = 0.1;
502 $form->defaultgrade = 1;
503 $form->noanswers = 3;
504 $form->answer = array('24264', '24264', '1');
505 $form->tolerance = array(10, 100, 0);
506 $form->fraction = array(1, 0.5, 0);
507 $form->nounits = 2;
508 $form->unit = array(0 => null, 1 => null);
509 $form->multiplier = array(1, 0);
510 $form->feedback = array('Very good', 'Close, but not quite there', 'Well at least you tried....');
511
512 if ($courseid) {
513 $course = $DB->get_record('course', array('id' => $courseid));
514 }
515
516 return $this->save_question($question, $form, $course);
517 }
516cf3eb 518}
516cf3eb 519
1fe641f7 520// INITIATION - Without this line the question type is not in use.
a2156789 521question_register_questiontype(new question_numerical_qtype());
516cf3eb 522?>