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
10 require_once("$CFG->dirroot/question/type/shortanswer/questiontype.php");
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
{
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
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!');
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);
62 function get_numerical_units(&$question) {
63 if ($question->options
->units
= get_records('question_numerical_units',
64 'question', $question->id
, 'id ASC')) {
65 $question->options
->units
= array_values($question->options
->units
);
67 $question->options
->units
= array();
72 function get_default_numerical_unit(&$question) {
73 if (isset($question->options
->units
[0])) {
74 foreach ($question->options
->units
as $unit) {
75 if (abs($unit->multiplier
- 1.0) < '1.0e-' . ini_get('precision')) {
84 * Save the units and the answers associated with this question.
86 function save_question_options($question) {
88 // Get old versions of the objects
89 if (!$oldanswers = get_records('question_answers', 'question', $question->id
, 'id ASC')) {
90 $oldanswers = array();
93 if (!$oldoptions = get_records('question_numerical', 'question', $question->id
, 'answer ASC')) {
94 $oldoptions = array();
98 $result = $this->save_numerical_units($question);
99 if (isset($result->error
)) {
102 $units = &$result->units
;
105 // Insert all the new answers
106 foreach ($question->answer
as $key => $dataanswer) {
107 if (!isset( $question->deleteanswer
[$key] ) && !( trim($dataanswer) == 0 && $question->fraction
[$key]== 0 &&trim($question->feedback
[$key])=='')) {
108 $answer = new stdClass
;
109 $answer->question
= $question->id
;
110 if (trim($dataanswer) == '*') {
111 $answer->answer
= '*';
113 $answer->answer
= $this->apply_unit($dataanswer, $units);
114 if ($answer->answer
=== false) {
115 $result->notice
= get_string('invalidnumericanswer', 'quiz');
118 $answer->fraction
= $question->fraction
[$key];
119 $answer->feedback
= trim($question->feedback
[$key]);
121 if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it
122 $answer->id
= $oldanswer->id
;
123 if (! update_record("question_answers", $answer)) {
124 $result->error
= "Could not update quiz answer! (id=$answer->id)";
127 } else { // This is a completely new answer
128 if (! $answer->id
= insert_record("question_answers", $answer)) {
129 $result->error
= "Could not insert quiz answer!";
134 // Set up the options object
135 if (!$options = array_shift($oldoptions)) {
136 $options = new stdClass
;
138 $options->question
= $question->id
;
139 $options->answer
= $answer->id
;
140 if (trim($question->tolerance
[$key]) == '') {
141 $options->tolerance
= '';
143 $options->tolerance
= $this->apply_unit($question->tolerance
[$key], $units);
144 if ($options->tolerance
=== false) {
145 $result->notice
= get_string('invalidnumerictolerance', 'quiz');
150 if (isset($options->id
)) { // reusing existing record
151 if (! update_record('question_numerical', $options)) {
152 $result->error
= "Could not update quiz numerical options! (id=$options->id)";
155 } else { // new options
156 if (! insert_record('question_numerical', $options)) {
157 $result->error
= "Could not insert quiz numerical options!";
163 // delete old answer records
164 if (!empty($oldanswers)) {
165 foreach($oldanswers as $oa) {
166 delete_records('question_answers', 'id', $oa->id
);
170 // delete old answer records
171 if (!empty($oldoptions)) {
172 foreach($oldoptions as $oo) {
173 delete_records('question_numerical', 'id', $oo->id
);
177 // Report any problems.
178 if (!empty($result->notice
)) {
185 function save_numerical_units($question) {
186 $result = new stdClass
;
188 // Delete the units previously saved for this question.
189 delete_records('question_numerical_units', 'question', $question->id
);
191 // Save the new units.
193 foreach ($question->multiplier
as $i => $multiplier) {
194 // Discard any unit which doesn't specify the unit or the multiplier
195 if (!empty($question->multiplier
[$i]) && !empty($question->unit
[$i])) {
196 $units[$i] = new stdClass
;
197 $units[$i]->question
= $question->id
;
198 $units[$i]->multiplier
= $this->apply_unit($question->multiplier
[$i], array());
199 $units[$i]->unit
= $question->unit
[$i];
200 if (! insert_record('question_numerical_units', $units[$i])) {
201 $result->error
= 'Unable to save unit ' . $units[$i]->unit
. ' to the Databse';
206 unset($question->multiplier
, $question->unit
);
208 $result->units
= &$units;
213 * Deletes question from the question-type specific tables
215 * @return boolean Success/Failure
216 * @param object $question The question being deleted
218 function delete_question($questionid) {
219 delete_records("question_numerical", "question", $questionid);
220 delete_records("question_numerical_units", "question", $questionid);
224 function compare_responses(&$question, $state, $teststate) {
225 if (isset($state->responses
['']) && isset($teststate->responses
[''])) {
226 return $state->responses
[''] == $teststate->responses
[''];
232 * Checks whether a response matches a given answer, taking the tolerance
233 * and units into account. Returns a true for if a response matches the
234 * answer, false if it doesn't.
236 function test_response(&$question, &$state, $answer) {
237 // Deal with the match anything answer.
238 if ($answer->answer
== '*') {
242 $response = $this->apply_unit(stripslashes($state->responses
['']), $question->options
->units
);
244 if ($response === false) {
245 return false; // The student did not type a number.
248 // The student did type a number, so check it with tolerances.
249 $this->get_tolerance_interval($answer);
250 return ($answer->min
<= $response && $response <= $answer->max
);
254 function check_response(&$question, &$state){
255 $answers = &$question->options
->answers
;
256 foreach($answers as $aid => $answer) {
257 if($this->test_response($question, $state, $answer)) {
264 function get_correct_responses(&$question, &$state) {
265 $correct = parent
::get_correct_responses($question, $state);
266 if ($correct[''] != '*' && $unit = $this->get_default_numerical_unit($question)) {
267 $correct[''] .= ' '.$unit->unit
;
273 function get_all_responses(&$question, &$state) {
274 $result = new stdClass
;
276 $unit = $this->get_default_numerical_unit($question);
277 if (is_array($question->options
->answers
)) {
278 foreach ($question->options
->answers
as $aid=>$answer) {
280 $r->answer
= $answer->answer
;
281 $r->credit
= $answer->fraction
;
282 $this->get_tolerance_interval($answer);
283 if ($r->answer
!= '*' && $unit) {
284 $r->answer
.= ' ' . $unit->unit
;
286 if ($answer->max
!= $answer->min
) {
287 $max = "$answer->max"; //format_float($answer->max, 2);
288 $min = "$answer->min"; //format_float($answer->max, 2);
289 $r->answer
.= ' ('.$min.'..'.$max.')';
294 $result->id
= $question->id
;
295 $result->responses
= $answers;
299 function get_tolerance_interval(&$answer) {
301 if (empty($answer->tolerance
)) {
302 $answer->tolerance
= 0;
305 // Calculate the interval of correct responses (min/max)
306 if (!isset($answer->tolerancetype
)) {
307 $answer->tolerancetype
= 2; // nominal
310 // We need to add a tiny fraction depending on the set precision to make the
311 // comparison work correctly. Otherwise seemingly equal values can yield
312 // false. (fixes bug #3225)
313 $tolerance = (float)$answer->tolerance +
("1.0e-".ini_get('precision'));
314 switch ($answer->tolerancetype
) {
315 case '1': case 'relative':
316 /// Recalculate the tolerance and fall through
317 /// to the nominal case:
318 $tolerance = $answer->answer
* $tolerance;
319 // Do not fall through to the nominal case because the tiny fraction is a factor of the answer
320 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
321 $max = $answer->answer +
$tolerance;
322 $min = $answer->answer
- $tolerance;
324 case '2': case 'nominal':
325 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
326 // $answer->tolerance 0 or something else
327 if ((float)$answer->tolerance
== 0.0 && abs((float)$answer->answer
) <= $tolerance ){
328 $tolerance = (float) ("1.0e-".ini_get('precision')) * abs((float)$answer->answer
) ; //tiny fraction
329 } else if ((float)$answer->tolerance
!= 0.0 && abs((float)$answer->tolerance
) < abs((float)$answer->answer
) && abs((float)$answer->answer
) <= $tolerance){
330 $tolerance = (1+
("1.0e-".ini_get('precision')) )* abs((float) $answer->tolerance
) ;//tiny fraction
333 $max = $answer->answer +
$tolerance;
334 $min = $answer->answer
- $tolerance;
336 case '3': case 'geometric':
337 $quotient = 1 +
abs($tolerance);
338 $max = $answer->answer
* $quotient;
339 $min = $answer->answer
/ $quotient;
342 error("Unknown tolerance type $answer->tolerancetype");
351 * Checks if the $rawresponse has a unit and applys it if appropriate.
353 * @param string $rawresponse The response string to be converted to a float.
354 * @param array $units An array with the defined units, where the
355 * unit is the key and the multiplier the value.
356 * @return float The rawresponse with the unit taken into
357 * account as a float.
359 function apply_unit($rawresponse, $units) {
360 // Make units more useful
362 foreach ($units as $unit) {
363 $tmpunits[$unit->unit
] = $unit->multiplier
;
365 // remove spaces and normalise decimal places.
366 $search = array(' ', ',');
367 $replace = array('', '.');
368 $rawresponse = str_replace($search, $replace, trim($rawresponse));
370 // Apply any unit that is present.
371 if (ereg('^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$',
372 $rawresponse, $responseparts)) {
374 if (!empty($responseparts[5])) {
376 if (isset($tmpunits[$responseparts[5]])) {
377 // Valid number with unit.
378 return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
380 // Valid number with invalid unit. Must be wrong.
385 // Valid number without unit.
386 return (float)$responseparts[1];
389 // Invalid number. Must be wrong.
393 /// BACKUP FUNCTIONS ////////////////////////////
396 * Backup the data in the question
398 * This is used in question/backuplib.php
400 function backup($bf,$preferences,$question,$level=6) {
404 $numericals = get_records('question_numerical', 'question', $question, 'id ASC');
405 //If there are numericals
407 //Iterate over each numerical
408 foreach ($numericals as $numerical) {
409 $status = fwrite ($bf,start_tag("NUMERICAL",$level,true));
410 //Print numerical contents
411 fwrite ($bf,full_tag("ANSWER",$level+
1,false,$numerical->answer
));
412 fwrite ($bf,full_tag("TOLERANCE",$level+
1,false,$numerical->tolerance
));
413 //Now backup numerical_units
414 $status = question_backup_numerical_units($bf,$preferences,$question,7);
415 $status = fwrite ($bf,end_tag("NUMERICAL",$level,true));
417 //Now print question_answers
418 $status = question_backup_answers($bf,$preferences,$question);
423 /// RESTORE FUNCTIONS /////////////////
426 * Restores the data in the question
428 * This is used in question/restorelib.php
430 function restore($old_question_id,$new_question_id,$info,$restore) {
434 //Get the numerical array
435 $numericals = $info['#']['NUMERICAL'];
437 //Iterate over numericals
438 for($i = 0; $i < sizeof($numericals); $i++
) {
439 $num_info = $numericals[$i];
441 //Now, build the question_numerical record structure
442 $numerical = new stdClass
;
443 $numerical->question
= $new_question_id;
444 $numerical->answer
= backup_todb($num_info['#']['ANSWER']['0']['#']);
445 $numerical->tolerance
= backup_todb($num_info['#']['TOLERANCE']['0']['#']);
447 //We have to recode the answer field
448 $answer = backup_getid($restore->backup_unique_code
,"question_answers",$numerical->answer
);
450 $numerical->answer
= $answer->new_id
;
453 //The structure is equal to the db, so insert the question_numerical
454 $newid = insert_record ("question_numerical", $numerical);
457 if (($i+
1) %
50 == 0) {
458 if (!defined('RESTORE_SILENTLY')) {
460 if (($i+
1) %
1000 == 0) {
467 //Now restore numerical_units
468 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$num_info,$restore);
480 // INITIATION - Without this line the question type is not in use.
481 question_register_questiontype(new question_numerical_qtype());