workaround for some more uninitialized offset warnings
[moodle.git] / question / questiontypes / numerical / questiontype.php
CommitLineData
516cf3eb 1<?php // $Id$
2
3/////////////////
4/// NUMERICAL ///
5/////////////////
6
7/// QUESTION TYPE CLASS //////////////////
8
9///
10/// This class contains some special features in order to make the
11/// question type embeddable within a multianswer (cloze) question
12///
13
14/// This question type behaves like shortanswer in most cases.
15/// Therefore, it extends the shortanswer question type...
16
e586cfb4 17require_once("$CFG->dirroot/question/questiontypes/shortanswer/questiontype.php");
516cf3eb 18
19class quiz_numerical_qtype extends quiz_shortanswer_qtype {
20
21 function name() {
22 return 'numerical';
23 }
24
25 function get_question_options(&$question) {
26 // Get the question answers and their respective tolerances
27 // Note: quiz_numerical is an extension of the answer table rather than
4f48fb42 28 // the question table as is usually the case for qtype
516cf3eb 29 // specific tables.
30 global $CFG;
31 if (!$question->options->answers = get_records_sql(
32 "SELECT a.*, n.tolerance " .
dc1f00de 33 "FROM {$CFG->prefix}question_answers a, " .
516cf3eb 34 " {$CFG->prefix}quiz_numerical n " .
35 "WHERE a.question = $question->id " .
36 "AND a.id = n.answer;")) {
37 notify('Error: Missing question answer!');
38 return false;
39 }
40 $this->get_numerical_units($question);
41
42 // If units are defined we strip off the defaultunit from the answer, if
43 // it is present. (Required for compatibility with the old code and DB).
44 if ($defaultunit = $this->get_default_numerical_unit($question)) {
45 foreach($question->options->answers as $key => $val) {
46 $answer = trim($val->answer);
47 $length = strlen($defaultunit->unit);
48 if (substr($answer, -$length) == $defaultunit->unit) {
49 $question->options->answers[$key]->answer =
50 substr($answer, 0, strlen($answer)-$length);
51 }
52 }
53 }
54 return true;
55 }
56
57 function get_numerical_units(&$question) {
58 if ($question->options->units = get_records('quiz_numerical_units',
59 'question', $question->id, 'id ASC')) {
60 $question->options->units = array_values($question->options->units);
61 usort($question->options->units, create_function('$a, $b', // make sure the default unit is at index 0
62 'if (1.0 === (float)$a->multiplier) { return -1; } else '.
63 'if (1.0 === (float)$b->multiplier) { return 1; } else { return 0; }'));
64 array_walk($question->options->units, create_function('$val',
65 '$val->multiplier = (float)$val->multiplier;'));
66 } else {
67 $question->options->units = array();
68 }
69 return true;
70 }
71
72 function get_default_numerical_unit(&$question) {
73 $unit = new stdClass;
74 $unit->unit = '';
75 $unit->multiplier = 1.0;
76 if (!isset($question->options->units[0])) {
77 // do nothing
78 } else if (1.0 === (float)$question->options->units[0]->multiplier) {
79 $unit->unit = $question->options->units[0]->unit;
80 } else {
81 foreach ($question->options->units as $u) {
82 if (1.0 === (float)$unit->multiplier) {
83 $unit->unit = $u->unit;
84 break;
85 }
86 }
87 }
88 return $unit;
89 }
90
91 function save_question_options($question) {
92 // save_question_options supports the definition of multiple answers
93 // for numerical questions. This is not currently used by the editing
94 // interface, but the GIFT format supports it. The multianswer qtype,
95 // for example can make use of this feature.
96 // Get old versions of the objects
dc1f00de 97 if (!$oldanswers = get_records("question_answers", "question", $question->id)) {
516cf3eb 98 $oldanswers = array();
99 }
100
101 if (!$oldoptions = get_records("quiz_numerical", "question", $question->id)) {
102 $oldoptions = array();
103 }
104
105 $result = $this->save_numerical_units($question);
106 if (isset($result->error)) {
107 return $result;
108 } else {
109 $units = &$result->units;
110 }
111
112 // Insert all the new answers
113 foreach ($question->answer as $key => $dataanswer) {
114 if ($dataanswer != "") {
115 $answer = new stdClass;
116 $answer->question = $question->id;
117 $answer->answer = trim($dataanswer);
118 $answer->fraction = $question->fraction[$key];
119 $answer->feedback = trim($question->feedback[$key]);
120
121 if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it
122 $answer->id = $oldanswer->id;
dc1f00de 123 if (! update_record("question_answers", $answer)) {
516cf3eb 124 $result->error = "Could not update quiz answer! (id=$answer->id)";
125 return $result;
126 }
127 } else { // This is a completely new answer
dc1f00de 128 if (! $answer->id = insert_record("question_answers", $answer)) {
516cf3eb 129 $result->error = "Could not insert quiz answer!";
130 return $result;
131 }
132 }
133
134 // Set up the options object
135 if (!$options = array_shift($oldoptions)) {
136 $options = new stdClass;
137 }
138 $options->question = $question->id;
139 $options->answer = $answer->id;
140 $options->tolerance = $this->apply_unit($question->tolerance[$key], $units);
141
142 // Save options
143 if (isset($options->id)) { // reusing existing record
144 if (! update_record('quiz_numerical', $options)) {
145 $result->error = "Could not update quiz numerical options! (id=$options->id)";
146 return $result;
147 }
148 } else { // new options
149 if (! insert_record('quiz_numerical', $options)) {
150 $result->error = "Could not insert quiz numerical options!";
151 return $result;
152 }
153 }
154
155 // delete old answer records
156 if (!empty($oldanswers)) {
157 foreach($oldanswers as $oa) {
dc1f00de 158 delete_records('question_answers', 'id', $oa->id);
516cf3eb 159 }
160 }
161
162 // delete old answer records
163 if (!empty($oldoptions)) {
164 foreach($oldoptions as $oo) {
165 delete_records('quiz_numerical', 'id', $oo->id);
166 }
167 }
168
169 }
170 }
171 }
172
173 function save_numerical_units($question) {
174 if (!$oldunits = get_records("quiz_numerical_units", "question", $question->id)) {
175 $oldunits = array();
176 }
177
178 // Set the units
179 $units = array();
180 $keys = array();
181 $oldunits = array_values($oldunits);
182 usort($oldunits, create_function('$a, $b', // make sure the default unit is at index 0
183 'if (1.0 === (float)$a->multiplier) { return -1; } else '.
184 'if (1.0 === (float)$b->multiplier) { return 1; } else { return 0; }'));
185 foreach ($oldunits as $unit) {
186 $units[] = clone($unit);
187 }
188 $n = isset($question->multiplier) ? count($question->multiplier) : 0;
189 for ($i = 0; $i < $n; $i++) {
190 // Discard any unit which doesn't specify the unit or the multiplier
191 if (!empty($question->multiplier[$i]) && !empty($question->unit[$i])) {
192 $units[$i]->question = $question->id;
193 $units[$i]->multiplier =
194 $this->apply_unit($question->multiplier[$i], array());
195 $units[$i]->unit = $question->unit[$i];
196 } else {
197 unset($units[$i]);
198 }
199 }
200 unset($question->multiplier, $question->unit);
201
202 /// Save units
203 for ($i = 0; $i < $n; $i++) {
204 if (!isset($units[$i]) && isset($oldunits[$i])) { // Delete if it hasn't been resubmitted
205 delete_records('quiz_numerical_units', 'id', $oldunits[$i]->id);
206 } else if ($oldunits != $units) { // answer has changed or is new
207 if (isset($oldunits[$i]->id)) { // answer has changed
208 $units[$i]->id = $oldunits[$i]->id;
209 if (! update_record('quiz_numerical_units', $units[$i])) {
210 $result->error = "Could not update quiz_numerical_unit $units[$i]->unit";
211 return $result;
212 }
213 } else if (isset($units[$i])) { // answer is new
214 if (! insert_record('quiz_numerical_units', $units[$i])) {
215 $result->error = "Unable to insert new unit $units[$i]->unit";
216 return $result;
217 }
218 }
219 }
220 }
221 $result->units = &$units;
222 return $result;
223 }
224
225 /**
226 * Deletes question from the question-type specific tables
227 *
228 * @return boolean Success/Failure
229 * @param object $question The question being deleted
230 */
231 function delete_question($question) {
232 delete_records("quiz_numerical", "question", $question->id);
233 delete_records("quiz_numerical_units", "question", $question->id);
234 return true;
235 }
236
237 function compare_responses(&$question, &$state, &$teststate) {
238 $response = isset($state->responses['']) ? $state->responses[''] : '';
239 $testresponse = isset($teststate->responses[''])
240 ? $teststate->responses[''] : '';
241 return ($response == $testresponse);
242 }
243
244
245
246 // Checks whether a response matches a given answer, taking the tolerance
247 // into account. Returns a true for if a response matches the answer, false
248 // if it doesn't.
249 function test_response(&$question, &$state, $answer) {
250 if (isset($state->responses[''])) {
251 $response = $this->apply_unit(stripslashes($state->responses['']),
252 $question->options->units);
253 } else {
254 $response = '';
255 }
256
257 if (is_numeric($response) && is_numeric($answer->answer)) {
258 $this->get_tolerance_interval($answer);
259 return ($answer->min <= $response && $answer->max >= $response);
260 } else {
261 return ($response == $answer->answer);
262 }
263 }
264
265 // ULPGC ecastro
266 function check_response(&$question, &$state){
267 $answers = &$question->options->answers;
268 foreach($answers as $aid => $answer) {
269 if($this->test_response($question, $state, $answer)) {
270 return $aid;
271 }
272 }
273 return false;
274 }
275
516cf3eb 276 function grade_responses(&$question, &$state, $cmoptions) {
277 $answers = &$question->options->answers;
278 $state->raw_grade = 0;
279 foreach($answers as $answer) {
280 if($this->test_response($question, $state, $answer)) {
281 if ($state->raw_grade < $answer->fraction) {
282 $state->raw_grade = $answer->fraction;
283 }
284 }
285 }
286 if (empty($state->raw_grade)) {
287 $state->raw_grade = 0;
288 }
289
290 // Make sure we don't assign negative or too high marks
291 $state->raw_grade = min(max((float) $state->raw_grade,
292 0.0), 1.0) * $question->maxgrade;
293 $state->penalty = $question->penalty * $question->maxgrade;
294
295 return true;
296 }
297
298 function get_correct_responses(&$question, &$state) {
299 $correct = parent::get_correct_responses($question, $state);
300 if ($unit = $this->get_default_numerical_unit($question)) {
301 $correct[''] .= ' '.$unit->unit;
302 }
303 return $correct;
304 }
305
306 // ULPGC ecastro
307 function get_all_responses(&$question, &$state) {
308 unset($answers);
309 $unit = $this->get_default_numerical_unit($question);
310 if (is_array($question->options->answers)) {
311 foreach ($question->options->answers as $aid=>$answer) {
312 unset ($r);
313 $r->answer = $answer->answer;
314 $r->credit = $answer->fraction;
315 $this->get_tolerance_interval($answer);
316 if ($unit) {
317 $r->answer .= ' '.$unit->unit;
318 }
319 if ($answer->max != $answer->min) {
320 $max = "$answer->max"; //format_float($answer->max, 2);
321 $min = "$answer->min"; //format_float($answer->max, 2);
322 $r->answer .= ' ('.$min.'..'.$max.')';
323 }
324 $answers[$aid] = $r;
325 }
326 } else {
327 $answers[]="error"; // just for debugging, eliminate
328 }
329 $result->id = $question->id;
330 $result->responses = $answers;
331 return $result;
332 }
333
334 function get_tolerance_interval(&$answer) {
335 // No tolerance
336 if (empty($answer->tolerance)) {
337 $answer->min = $answer->max = $answer->answer;
338 return true;
339 }
340
341 // Calculate the interval of correct responses (min/max)
342 if (!isset($answer->tolerancetype)) {
343 $answer->tolerancetype = 2; // nominal
344 }
345
346 // We need to add a tiny fraction (0.00000000000000001) to make the
347 // comparison work correctly. Otherwise seemingly equal values can yield
348 // false. (fixes bug #3225)
349 $tolerance = (float)$answer->tolerance + 0.00000000000000001;
350 switch ($answer->tolerancetype) {
351 case '1': case 'relative':
352 /// Recalculate the tolerance and fall through
353 /// to the nominal case:
354 $tolerance = $answer->answer * $tolerance;
355 // Falls through to the nominal case -
356 case '2': case 'nominal':
357 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
358 $max = $answer->answer + $tolerance;
359 $min = $answer->answer - $tolerance;
360 break;
361 case '3': case 'geometric':
362 $quotient = 1 + abs($tolerance);
363 $max = $answer->answer * $quotient;
364 $min = $answer->answer / $quotient;
365 break;
366 default:
367 error("Unknown tolerance type $answer->tolerancetype");
368 }
369
370 $answer->min = $min;
371 $answer->max = $max;
372 return true;
373 }
374
375 /**
376 * Checks if the $rawresponse has a unit and applys it if appropriate.
377 *
378 * @param string $rawresponse The response string to be converted to a float.
379 * @param array $units An array with the defined units, where the
380 * unit is the key and the multiplier the value.
381 * @return float The rawresponse with the unit taken into
382 * account as a float.
383 */
384 function apply_unit($rawresponse, $units) {
385 // Make units more useful
386 $tmpunits = array();
387 foreach ($units as $unit) {
388 $tmpunits[$unit->unit] = $unit->multiplier;
389 }
390
391 $search = array(' ', ',');
392 $replace = array('', '.');
393 $rawresponse = str_replace($search, $replace, $rawresponse); // remove spaces
394 if (ereg(
395 '^([+-]?([0-9]+(\\.[0-9]*)?|[.][0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$',
396 $rawresponse, $responseparts)) {
397 $responsenum = (float)$responseparts[1];
398 if (isset($tmpunits[$responseparts[5]])) {
399 return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
400 } else {
401 return (float)$responseparts[1];
402 }
403 }
404 return $rawresponse;
405 }
406}
407//// END OF CLASS ////
408
409//////////////////////////////////////////////////////////////////////////
410//// INITIATION - Without this line the question type is not in use... ///
411//////////////////////////////////////////////////////////////////////////
f02c6f01 412$QTYPES[NUMERICAL]= new quiz_numerical_qtype();
516cf3eb 413
414?>