Updated the HEAD build version to 20100512
[moodle.git] / question / type / numerical / questiontype.php
CommitLineData
aeb15530 1<?php
1fe641f7 2/**
1fe641f7 3 * @author Martin Dougiamas and many others. Tim Hunt.
4 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
1976496e 5 * @package questionbank
6 * @subpackage questiontypes
1fe641f7 7 *//** */
516cf3eb 8
aaae75b0 9require_once("$CFG->dirroot/question/type/shortanswer/questiontype.php");
516cf3eb 10
1fe641f7 11/**
12 * NUMERICAL QUESTION TYPE CLASS
13 *
14 * This class contains some special features in order to make the
15 * question type embeddable within a multianswer (cloze) question
16 *
17 * This question type behaves like shortanswer in most cases.
18 * Therefore, it extends the shortanswer question type...
1976496e 19 * @package questionbank
20 * @subpackage questiontypes
1fe641f7 21 */
32a189d6 22class question_numerical_qtype extends question_shortanswer_qtype {
516cf3eb 23
c5da9906 24 public $virtualqtype = false;
25 public $unitpenalty = 0;
26 public $raw_unitgrade = 0 ;
27 public $raw_unitpenalty = 0 ;
28 public $valid_numerical_unit = true ;
516cf3eb 29 function name() {
30 return 'numerical';
31 }
aeb15530 32
869309b8 33 function has_wildcards_in_responses() {
34 return true;
35 }
36
37 function requires_qtypes() {
38 return array('shortanswer');
39 }
516cf3eb 40
41 function get_question_options(&$question) {
42 // Get the question answers and their respective tolerances
32a189d6 43 // Note: question_numerical is an extension of the answer table rather than
4f48fb42 44 // the question table as is usually the case for qtype
516cf3eb 45 // specific tables.
fef8f84e 46 global $CFG, $DB, $OUTPUT;
f34488b2 47 if (!$question->options->answers = $DB->get_records_sql(
516cf3eb 48 "SELECT a.*, n.tolerance " .
f34488b2 49 "FROM {question_answers} a, " .
50 " {question_numerical} n " .
51 "WHERE a.question = ? " .
026bec73 52 " AND a.id = n.answer " .
f34488b2 53 "ORDER BY a.id ASC", array($question->id))) {
fef8f84e 54 echo $OUTPUT->notification('Error: Missing question answer for numerical question ' . $question->id . '!');
516cf3eb 55 return false;
56 }
57 $this->get_numerical_units($question);
cf146692
PP
58 //get_numerical_options() need to know if there are units
59 // to set correctly default values
60 $this->get_numerical_options($question);
516cf3eb 61
5a14d563 62 // If units are defined we strip off the default unit from the answer, if
516cf3eb 63 // it is present. (Required for compatibility with the old code and DB).
64 if ($defaultunit = $this->get_default_numerical_unit($question)) {
65 foreach($question->options->answers as $key => $val) {
66 $answer = trim($val->answer);
67 $length = strlen($defaultunit->unit);
1fe641f7 68 if ($length && substr($answer, -$length) == $defaultunit->unit) {
516cf3eb 69 $question->options->answers[$key]->answer =
1fe641f7 70 substr($answer, 0, strlen($answer)-$length);
516cf3eb 71 }
72 }
73 }
c5da9906 74
75 return true;
76 }
77 function get_numerical_options(&$question) {
78 global $DB;
cf146692
PP
79 if (!$options = $DB->get_record('question_numerical_options', array('question' => $question->id))) {
80 $question->options->unitgradingtype = 0;
81 $question->options->unitpenalty = 0;
82 // the default
83 if ($defaultunit = $this->get_default_numerical_unit($question)) {
84 // so units can be graded
85 $question->options->showunits = 0 ;
86 }else {
87 // only numerical will be graded
88 $question->options->showunits = 3 ;
89 }
90 $question->options->unitsleft = 0 ;
91 $question->options->instructions = '' ;
c5da9906 92 } else {
cf146692
PP
93 $question->options->unitgradingtype = $options->unitgradingtype;
94 $question->options->unitpenalty = $options->unitpenalty;
95 $question->options->showunits = $options->showunits ;
96 $question->options->unitsleft = $options->unitsleft ;
97 $question->options->instructions = $options->instructions ;
c5da9906 98 }
aeb15530
PS
99
100
516cf3eb 101 return true;
102 }
516cf3eb 103 function get_numerical_units(&$question) {
f34488b2 104 global $DB;
105 if ($units = $DB->get_records('question_numerical_units', array('question' => $question->id), 'id ASC')) {
3a513ba4 106 $units = array_values($units);
516cf3eb 107 } else {
b4d7d27c 108 $units = array();
516cf3eb 109 }
b4d7d27c 110 foreach ($units as $key => $unit) {
111 $units[$key]->multiplier = clean_param($unit->multiplier, PARAM_NUMBER);
112 }
113 $question->options->units = $units;
516cf3eb 114 return true;
115 }
116
117 function get_default_numerical_unit(&$question) {
ed0ba6da 118 if (isset($question->options->units[0])) {
119 foreach ($question->options->units as $unit) {
120 if (abs($unit->multiplier - 1.0) < '1.0e-' . ini_get('precision')) {
121 return $unit;
516cf3eb 122 }
123 }
124 }
ed0ba6da 125 return false;
516cf3eb 126 }
127
1fe641f7 128 /**
129 * Save the units and the answers associated with this question.
130 */
516cf3eb 131 function save_question_options($question) {
f34488b2 132 global $DB;
516cf3eb 133 // Get old versions of the objects
f34488b2 134 if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) {
516cf3eb 135 $oldanswers = array();
136 }
137
f34488b2 138 if (!$oldoptions = $DB->get_records('question_numerical', array('question' => $question->id), 'answer ASC')) {
516cf3eb 139 $oldoptions = array();
140 }
141
1fe641f7 142 // Save the units.
516cf3eb 143 $result = $this->save_numerical_units($question);
144 if (isset($result->error)) {
145 return $result;
146 } else {
147 $units = &$result->units;
148 }
149
150 // Insert all the new answers
151 foreach ($question->answer as $key => $dataanswer) {
94a6d656 152 // Check for, and ingore, completely blank answer from the form.
153 if (trim($dataanswer) == '' && $question->fraction[$key] == 0 &&
154 html_is_blank($question->feedback[$key])) {
155 continue;
156 }
516cf3eb 157
94a6d656 158 $answer = new stdClass;
159 $answer->question = $question->id;
fac1189d 160 if (trim($dataanswer) === '*') {
94a6d656 161 $answer->answer = '*';
162 } else {
163 $answer->answer = $this->apply_unit($dataanswer, $units);
164 if ($answer->answer === false) {
165 $result->notice = get_string('invalidnumericanswer', 'quiz');
516cf3eb 166 }
94a6d656 167 }
168 $answer->fraction = $question->fraction[$key];
169 $answer->feedback = trim($question->feedback[$key]);
516cf3eb 170
94a6d656 171 if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it
172 $answer->id = $oldanswer->id;
0bcf8b6f 173 $DB->update_record("question_answers", $answer);
94a6d656 174 } else { // This is a completely new answer
0bcf8b6f 175 $answer->id = $DB->insert_record("question_answers", $answer);
94a6d656 176 }
f34488b2 177
94a6d656 178 // Set up the options object
179 if (!$options = array_shift($oldoptions)) {
180 $options = new stdClass;
181 }
182 $options->question = $question->id;
183 $options->answer = $answer->id;
184 if (trim($question->tolerance[$key]) == '') {
185 $options->tolerance = '';
186 } else {
187 $options->tolerance = $this->apply_unit($question->tolerance[$key], $units);
188 if ($options->tolerance === false) {
189 $result->notice = get_string('invalidnumerictolerance', 'quiz');
190 }
191 }
192
193 // Save options
194 if (isset($options->id)) { // reusing existing record
0bcf8b6f 195 $DB->update_record('question_numerical', $options);
94a6d656 196 } else { // new options
0bcf8b6f 197 $DB->insert_record('question_numerical', $options);
1fe641f7 198 }
199 }
200 // delete old answer records
201 if (!empty($oldanswers)) {
202 foreach($oldanswers as $oa) {
f34488b2 203 $DB->delete_records('question_answers', array('id' => $oa->id));
1fe641f7 204 }
205 }
516cf3eb 206
1fe641f7 207 // delete old answer records
208 if (!empty($oldoptions)) {
209 foreach($oldoptions as $oo) {
f34488b2 210 $DB->delete_records('question_numerical', array('id' => $oo->id));
516cf3eb 211 }
212 }
c5da9906 213 $result = $this->save_numerical_options($question);
214 if (isset($result->error)) {
215 return $result;
216 }
1fe641f7 217 // Report any problems.
218 if (!empty($result->notice)) {
219 return $result;
220 }
1fe641f7 221 return true;
516cf3eb 222 }
223
c5da9906 224 function save_numerical_options(&$question) {
225 global $DB;
226 $result = new stdClass;
227 // numerical options
aeb15530 228 $update = true ;
c5da9906 229 $options = $DB->get_record("question_numerical_options", array("question" => $question->id));
230 if (!$options) {
231 $update = false;
232 $options = new stdClass;
233 $options->question = $question->id;
234 }
235 if(isset($question->unitgradingtype)){
236 $options->unitgradingtype = $question->unitgradingtype;
237 }else {
238 $options->unitgradingtype = 0 ;
239 }
240 if(isset($question->unitpenalty)){
241 $options->unitpenalty = $question->unitpenalty;
242 }else {
243 $options->unitpenalty = 0 ;
244 }
cf146692
PP
245 // if we came from the form then 'unitrole' exists
246 if(isset($question->unitrole)){
247 if ($question->unitrole == 0 ){
248 $options->showunits = $question->showunits0;
249 }else {
250 $options->showunits = $question->showunits1;
251 }
252 }else {
253 if(isset($question->showunits)){
254 $options->showunits = $question->showunits;
255 }else {
256 if ($defaultunit = $this->get_default_numerical_unit($question)) {
257 // so units can be graded
258 $options->showunits = 0 ;
259 }else {
260 // only numerical will be graded
261 $options->showunits = 3 ;
262 }
263 }
c5da9906 264 }
265 if(isset($question->unitsleft)){
266 $options->unitsleft = $question->unitsleft;
267 }else {
268 $options->unitsleft = 0 ;
269 }
270 if(isset($question->instructions)){
271 $options->instructions = trim($question->instructions);
272 }else {
273 $options->instructions = '' ;
274 }
275 if ($update) {
276 if (!$DB->update_record("question_numerical_options", $options)) {
277 $result->error = "Could not update numerical question options! (id=$options->id)";
278 return $result;
279 }
280 } else {
281 if (!$DB->insert_record("question_numerical_options", $options)) {
282 $result->error = "Could not insert numerical question options!";
283 return $result;
284 }
285 }
286 return $result;
287 }
288
516cf3eb 289 function save_numerical_units($question) {
f34488b2 290 global $DB;
ed0ba6da 291 $result = new stdClass;
516cf3eb 292
ed0ba6da 293 // Delete the units previously saved for this question.
f34488b2 294 $DB->delete_records('question_numerical_units', array('question' => $question->id));
ed0ba6da 295
26b26662 296 // Nothing to do.
297 if (!isset($question->multiplier)) {
298 $result->units = array();
299 return $result;
300 }
301
ed0ba6da 302 // Save the new units.
516cf3eb 303 $units = array();
cf146692 304 $unitalreadyinsert = array();
ed0ba6da 305 foreach ($question->multiplier as $i => $multiplier) {
516cf3eb 306 // Discard any unit which doesn't specify the unit or the multiplier
cf146692
PP
307 if (!empty($question->multiplier[$i]) && !empty($question->unit[$i])&& !array_key_exists(addslashes($question->unit[$i]),$unitalreadyinsert)) {
308 $unitalreadyinsert[addslashes($question->unit[$i])] = 1 ;
ed0ba6da 309 $units[$i] = new stdClass;
516cf3eb 310 $units[$i]->question = $question->id;
1fe641f7 311 $units[$i]->multiplier = $this->apply_unit($question->multiplier[$i], array());
516cf3eb 312 $units[$i]->unit = $question->unit[$i];
979425b5 313 $DB->insert_record('question_numerical_units', $units[$i]);
516cf3eb 314 }
315 }
316 unset($question->multiplier, $question->unit);
317
516cf3eb 318 $result->units = &$units;
319 return $result;
320 }
321
c5da9906 322 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
323 $state->responses = array();
324 $state->responses['answer'] = '';
325 $state->responses['unit'] = '';
326 /* if ($question->options->showunits == 1){
327 $state->responses['unit'] = '0';
328 }*/
aeb15530 329
c5da9906 330 return true;
331 }
332 function restore_session_and_responses(&$question, &$state) {
333 if(false === strpos($state->responses[''], '|||||')){
334 // temporary
aeb15530 335 $state->responses['answer']= $state->responses[''];
c5da9906 336 $state->responses['unit'] = '';
1c5299bf 337 $this->split_old_answer($state->responses[''], $question->options->units, $state->responses['answer'] ,$state->responses['unit'] );
c5da9906 338 }else {
339 $responses = explode('|||||', $state->responses['']);
340 $state->responses['answer']= $responses[0];
341 $state->responses['unit'] = $responses[1];
342 }
343 // echo "<p> restore response $responses || <pre>";print_r($state);echo "</pre></p>";
344
aeb15530 345 /*
c5da9906 346 if ($question->options->showunits == 1 && isset($question->options->units)){
347 $state->responses['unit']=$this->find_unit_index($question,$state->responses['unit']);
348 }*/
349 return true;
350 }
351
352 function find_unit_index(&$question,$value){
353 $length = 0;
354 $goodkey = 0 ;
355 foreach ($question->options->units as $key => $unit){
356 if($unit->unit ==$value ) {
357 return $key ;
358 }
aeb15530 359 }
c5da9906 360 return 0 ;
361 }
362
363 function split_old_answer($rawresponse, $units, &$answer ,&$unit ) {
364 $answer = $rawresponse ;
365 // remove spaces and normalise decimal places.
366 $search = array(' ', ',');
367 $replace = array('', '.');
368 $rawresponse = str_replace($search, $replace, trim($rawresponse));
369 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
370 $rawresponse, $responseparts)) {
371 $unit = $responseparts[5] ;
372 $answer = $responseparts[1] ;
373 }
374 return ;
375 }
376
377
378 function save_session_and_responses(&$question, &$state) {
379 global $DB;
380 // echo "<p> save session <pre>";print_r($state);echo "</pre></p>";
381
382 $responses = '';
383 if(isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']])){
384 $responses = $state->responses['answer'].'|||||'.$question->options->units[$state->responses['unit']]->unit;
385 }else if(isset($state->responses['unit'])){
386 $responses = $state->responses['answer'].'|||||'.$state->responses['unit'] ;
387 }else {
388 $responses = $state->responses['answer'].'|||||';
389 }
390 // Set the legacy answer field
391 if (!$DB->set_field('question_states', 'answer', $responses, array('id' => $state->id))) {
392 return false;
393 }
394 return true;
395 }
396
397/**
1fe641f7 398 * Deletes question from the question-type specific tables
399 *
400 * @return boolean Success/Failure
401 * @param object $question The question being deleted
402 */
90c3f310 403 function delete_question($questionid) {
f34488b2 404 global $DB;
405 $DB->delete_records("question_numerical", array("question" => $questionid));
c5da9906 406 $DB->delete_records("question_numerical_options", array("question" => $questionid));
f34488b2 407 $DB->delete_records("question_numerical_units", array("question" => $questionid));
516cf3eb 408 return true;
409 }
aeb15530 410
516cf3eb 411
5a14d563 412 function compare_responses(&$question, $state, $teststate) {
c5da9906 413 if ($question->options->showunits == 1 && isset($question->options->units) && isset($question->options->units[$state->responses['unit']] )){
414 $state->responses['unit']=$question->options->units[$state->responses['unit']]->unit;
415 };
416
aeb15530 417
c5da9906 418 $responses = '';
419 $testresponses = '';
420 if (isset($state->responses['answer'])){
421 $responses = $state->responses['answer'];
422 }
423 if (isset($state->responses['unit'])){
424 $responses .= $state->responses['unit'];
425 }
426 if (isset($teststate->responses['answer'])){
427 $testresponses = $teststate->responses['answer'];
428 }
429 if (isset($teststate->responses['unit'])){
430 $testresponses .= $teststate->responses['unit'];
431 }
0a49ee5c 432 // echo "<p> compare response $responses || $testresponses <pre>";print_r($state);echo "</pre></p>";
8b831fbb 433
f0b6151c 434 if ( isset($responses) && isset($testresponses )) {
c5da9906 435
436 return $responses == $testresponses ;
5a14d563 437 }
438 return false;
516cf3eb 439 }
440
1fe641f7 441 /**
442 * Checks whether a response matches a given answer, taking the tolerance
443 * and units into account. Returns a true for if a response matches the
444 * answer, false if it doesn't.
445 */
516cf3eb 446 function test_response(&$question, &$state, $answer) {
55894a42 447 // Deal with the match anything answer.
fac1189d 448 if ($answer->answer === '*') {
55894a42 449 return true;
516cf3eb 450 }
0a49ee5c 451 // echo "<p> test response numerical state <pre>";print_r($state);echo "</pre></p>";
c5da9906 452 // echo "<p> test response numerical question <pre>";print_r($question);echo "</pre></p>";
453
454 if( isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']])){
455 $state->responses['']=$state->responses['answer'].$question->options->units[$state->responses['unit']]->unit;
456 // echo "<p>test responses valid unit </p>";
457 }else if(isset($state->responses['unit'])){
458 $state->responses['']= $state->responses['answer'].$state->responses['unit'] ;// why?
459 }
0a49ee5c 460 // echo "<p> test response numerical state before apply unit <pre>";print_r($state);echo "</pre></p>";
aeb15530 461
294ce987 462 $response = $this->apply_unit($state->responses[''], $question->options->units);
c5da9906 463 // $this->valid_numerical_unit = $this->valid_unit($state->responses[''], $question->options->units);
aeb15530
PS
464 // if ($this->valid_numerical_unit) echo "<p>test responses valid unit </p>";
465 // if (!$this->valid_numerical_unit) echo "<p>test responses not valid unit </p>";
0a49ee5c 466 // echo "<p>state response test $response after apply unit <pre>";print_r($state);echo "</pre></p>";
c5da9906 467 // $this->raw_unitpenalty = 0.1 ;
1fe641f7 468
469 if ($response === false) {
470 return false; // The student did not type a number.
516cf3eb 471 }
1fe641f7 472
473 // The student did type a number, so check it with tolerances.
474 $this->get_tolerance_interval($answer);
475 return ($answer->min <= $response && $response <= $answer->max);
516cf3eb 476 }
477
c5da9906 478 function grade_responses(&$question, &$state, $cmoptions) {
479 // The default implementation uses the test_response method to
480 // compare what the student entered against each of the possible
481 // answers stored in the question, and uses the grade from the
482 // first one that matches. It also sets the marks and penalty.
483 // This should be good enought for most simple question types.
484 // echo "<p>grade responses <pre>";print_r($state->responses);echo "</pre></p>";
aeb15530
PS
485
486 //first the split response from unit choice display is converted as
c5da9906 487 // standard numerical response value.unit
488 /* if (!empty($question->options->showunits) && isset($state->responses['unit'])){
489 $state->responses[''] .= $question->options->units[$state->responses['unit']]->unit ;
490 }
aeb15530 491 */
c5da9906 492 //to apply the unit penalty we need to analyse the response in a more complex way
493 //the apply_unit() function analysis could be used to obtain the infos
aeb15530
PS
494 // however it is used to detect good or bad numbers but also
495 // gives false
c5da9906 496 $state->raw_grade = 0;
497 foreach($question->options->answers as $answer) {
498 if($this->test_response($question, $state, $answer)) {
499 $state->raw_grade = $answer->fraction;
500 $this->raw_unitgrade = $answer->fraction;
c5da9906 501 break;
502 }
503 }
0a49ee5c
PP
504 // in all cases the unit should be tested
505 if( $question->options->showunits == 3) {
506 $this->valid_numerical_unit == true ;
507 }else {
508 $this->valid_numerical_unit = $this->valid_unit($state->responses[''], $question->options->units);
509 }
510 // if ($this->valid_numerical_unit) echo "<p>grade responses valid unit </p>";
511 // if (!$this->valid_numerical_unit) echo "<p>grade responses not valid unit </p>";
c5da9906 512 // apply unit penalty
513 $this->raw_unitpenalty = 0 ;
514 if(!empty($question->options->unitpenalty)&& !$this->valid_numerical_unit ){
515 if($question->options->unitgradingtype == 1){
516 $this->raw_unitpenalty = $question->options->unitpenalty* $state->raw_grade ;
517 }else {
518 $this->raw_unitpenalty = $question->options->unitpenalty* $question->maxgrade;
519 }
520 $state->raw_grade -= $question->options->unitpenalty ;
521 }
522 // echo "<p>grade responses <pre>";print_r($state->responses);echo "</pre></p>";
523
524 // $this->raw_unitpenalty = $question->options->unitpenalty ;
525 // Make sure we don't assign negative or too high marks.
526 // $state->raw_grade -= $question->options->unitpenalty ;
527 $state->raw_grade = min(max((float) $state->raw_grade,
528 0.0), 1.0) * $question->maxgrade;
529
530 // Update the penalty.
531 $state->penalty = $question->penalty * $question->maxgrade;
532
533 // mark the state as graded
534 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
535
536 return true;
537 }
aeb15530
PS
538
539
516cf3eb 540 function get_correct_responses(&$question, &$state) {
541 $correct = parent::get_correct_responses($question, $state);
c85aea03 542 $unit = $this->get_default_numerical_unit($question);
78d2d576 543 $correct['answer']= $correct[''];
c85aea03 544 if (isset($correct['']) && $correct[''] != '*' && $unit) {
516cf3eb 545 $correct[''] .= ' '.$unit->unit;
78d2d576 546 $correct['unit']= $unit->unit;
aeb15530 547 }
516cf3eb 548 return $correct;
549 }
550
551 // ULPGC ecastro
552 function get_all_responses(&$question, &$state) {
1fe641f7 553 $result = new stdClass;
554 $answers = array();
516cf3eb 555 $unit = $this->get_default_numerical_unit($question);
556 if (is_array($question->options->answers)) {
557 foreach ($question->options->answers as $aid=>$answer) {
1fe641f7 558 $r = new stdClass;
516cf3eb 559 $r->answer = $answer->answer;
560 $r->credit = $answer->fraction;
561 $this->get_tolerance_interval($answer);
55894a42 562 if ($r->answer != '*' && $unit) {
563 $r->answer .= ' ' . $unit->unit;
516cf3eb 564 }
565 if ($answer->max != $answer->min) {
566 $max = "$answer->max"; //format_float($answer->max, 2);
567 $min = "$answer->min"; //format_float($answer->max, 2);
568 $r->answer .= ' ('.$min.'..'.$max.')';
569 }
570 $answers[$aid] = $r;
571 }
516cf3eb 572 }
573 $result->id = $question->id;
574 $result->responses = $answers;
575 return $result;
576 }
c5da9906 577 function get_actual_response($question, $state) {
578 // echo "<p>state response numerical GET ACTUAL RESPONSE $question->id $question->qtype <pre>";print_r($state);echo "</pre></p>";
aeb15530 579 if (!empty($state->responses) && !empty($state->responses[''])) {
c5da9906 580 if(false === strpos($state->responses[''], '|||||')){
581 $responses[] = $state->responses[''];
582 }else {
583 $resp = explode('|||||', $state->responses['']);
aeb15530 584 $responses[] = $resp[0].$resp[1];
c5da9906 585 }
586 } else {
587 $responses[] = '';
588 }
aeb15530 589
c5da9906 590 return $responses;
591 }
592
516cf3eb 593
594 function get_tolerance_interval(&$answer) {
595 // No tolerance
596 if (empty($answer->tolerance)) {
1fe641f7 597 $answer->tolerance = 0;
516cf3eb 598 }
599
600 // Calculate the interval of correct responses (min/max)
601 if (!isset($answer->tolerancetype)) {
602 $answer->tolerancetype = 2; // nominal
603 }
604
223ad0b9 605 // We need to add a tiny fraction depending on the set precision to make the
516cf3eb 606 // comparison work correctly. Otherwise seemingly equal values can yield
607 // false. (fixes bug #3225)
223ad0b9 608 $tolerance = (float)$answer->tolerance + ("1.0e-".ini_get('precision'));
516cf3eb 609 switch ($answer->tolerancetype) {
610 case '1': case 'relative':
611 /// Recalculate the tolerance and fall through
612 /// to the nominal case:
613 $tolerance = $answer->answer * $tolerance;
dcd4192a 614 // Do not fall through to the nominal case because the tiny fraction is a factor of the answer
615 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
616 $max = $answer->answer + $tolerance;
617 $min = $answer->answer - $tolerance;
618 break;
516cf3eb 619 case '2': case 'nominal':
620 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
dcd4192a 621 // $answer->tolerance 0 or something else
622 if ((float)$answer->tolerance == 0.0 && abs((float)$answer->answer) <= $tolerance ){
f34488b2 623 $tolerance = (float) ("1.0e-".ini_get('precision')) * abs((float)$answer->answer) ; //tiny fraction
dcd4192a 624 } else if ((float)$answer->tolerance != 0.0 && abs((float)$answer->tolerance) < abs((float)$answer->answer) && abs((float)$answer->answer) <= $tolerance){
f34488b2 625 $tolerance = (1+("1.0e-".ini_get('precision')) )* abs((float) $answer->tolerance) ;//tiny fraction
626 }
627
516cf3eb 628 $max = $answer->answer + $tolerance;
629 $min = $answer->answer - $tolerance;
630 break;
ed0ba6da 631 case '3': case 'geometric':
516cf3eb 632 $quotient = 1 + abs($tolerance);
633 $max = $answer->answer * $quotient;
634 $min = $answer->answer / $quotient;
635 break;
636 default:
0b4f4187 637 print_error('unknowntolerance', 'question', '', $answer->tolerancetype);
516cf3eb 638 }
639
640 $answer->min = $min;
641 $answer->max = $max;
642 return true;
643 }
644
645 /**
1fe641f7 646 * Checks if the $rawresponse has a unit and applys it if appropriate.
647 *
648 * @param string $rawresponse The response string to be converted to a float.
649 * @param array $units An array with the defined units, where the
650 * unit is the key and the multiplier the value.
651 * @return float The rawresponse with the unit taken into
652 * account as a float.
653 */
516cf3eb 654 function apply_unit($rawresponse, $units) {
655 // Make units more useful
656 $tmpunits = array();
657 foreach ($units as $unit) {
658 $tmpunits[$unit->unit] = $unit->multiplier;
659 }
1fe641f7 660 // remove spaces and normalise decimal places.
be1bb80e 661 $rawresponse = trim($rawresponse) ;
516cf3eb 662 $search = array(' ', ',');
be1bb80e
PP
663 // test if a . is present or there are multiple , (i.e. 2,456,789 ) so that we don't need spaces and ,
664 if ( strpos($rawresponse,'.' ) !== false || substr_count($rawresponse,',') > 1 ) {
665 $replace = array('', '');
666 }else { // remove spaces and normalise , to a . .
667 $replace = array('', '.');
668 }
669 $rawresponse = str_replace($search, $replace, $rawresponse);
f34488b2 670
1fe641f7 671 // Apply any unit that is present.
6dbcacee 672 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
1fe641f7 673 $rawresponse, $responseparts)) {
f34488b2 674
1fe641f7 675 if (!empty($responseparts[5])) {
f34488b2 676
1fe641f7 677 if (isset($tmpunits[$responseparts[5]])) {
678 // Valid number with unit.
679 return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
c5da9906 680 } else {
aeb15530 681 // Valid number with invalid unit.
c5da9906 682 return (float)$responseparts[1];
683 }
684
685 } else {
686 // Valid number without unit.
687 return (float)$responseparts[1];
688 }
689 }
690 // Invalid number. Must be wrong.
691 return false;
692 }
cf146692
PP
693/**
694 * function used in in function definition_inner()
695 * of edit_..._form.php for
696 * numerical, calculated, calculatedsimple
697 */
698 function add_units_options(&$mform, &$that){
699 $mform->addElement('header', 'unithandling', get_string('unitshandling', 'qtype_numerical'));
700 // Units are graded
701 $mform->addElement('radio', 'unitrole', get_string('unitgraded1', 'qtype_numerical'), get_string('unitgraded', 'qtype_numerical'),0);
702 $penaltygrp = array();
703 $penaltygrp[] =& $mform->createElement('text', 'unitpenalty', get_string('unitpenalty', 'qtype_numerical') ,
704 array('size' => 6));
705 $unitgradingtypes = array('1' => get_string('decfractionofquestiongrade', 'qtype_numerical'), '2' => get_string('decfractionofresponsegrade', 'qtype_numerical'));
706 $penaltygrp[] =& $mform->createElement('select', 'unitgradingtype', '' , $unitgradingtypes );
707 $mform->addGroup($penaltygrp, 'penaltygrp', get_string('unitpenalty', 'qtype_numerical'),' ' , false);
708 $showunits0grp = array();
709 $showunits0grp[] =& $mform->createElement('radio', 'showunits0', get_string('unitedit', 'qtype_numerical'), get_string('editableunittext', 'qtype_numerical'),0);
710 $showunits0grp[] =& $mform->createElement('radio', 'showunits0', get_string('selectunits', 'qtype_numerical') , get_string('unitchoice', 'qtype_numerical'),1);
711 $mform->addGroup($showunits0grp, 'showunits0grp', get_string('studentunitanswer', 'qtype_numerical'),' OR ' , false);
712 $mform->addElement('htmleditor', 'instructions', get_string('instructions', 'qtype_numerical'),
713 array('rows' => 10, 'course' => $that->coursefilesid));
714 $mform->addElement('static', 'separator1', '<HR/>', '<HR/>');
715 // Units are not graded
716 $mform->addElement('radio', 'unitrole', get_string('unitnotgraded', 'qtype_numerical'), get_string('onlynumerical', 'qtype_numerical'),1);
717 $showunits1grp = array();
718 $showunits1grp[] = & $mform->createElement('radio', 'showunits1', '', get_string('no', 'moodle'),3);
719 $showunits1grp[] = & $mform->createElement('radio', 'showunits1', '', get_string('yes', 'moodle'),2);
720 $mform->addGroup($showunits1grp, 'showunits1grp', get_string('unitdisplay', 'qtype_numerical'),' ' , false);
721 $unitslefts = array('0' => get_string('rightexample', 'qtype_numerical'),'1' => get_string('leftexample', 'qtype_numerical'));
722 $mform->addElement('static', 'separator2', '<HR/>', '<HR/>');
723 $mform->addElement('select', 'unitsleft', get_string('unitposition', 'qtype_numerical') , $unitslefts );
8b831fbb 724 $currentgrp1 = array();
aeb15530 725
c5da9906 726 $mform->setType('unitpenalty', PARAM_NUMBER);
c5da9906 727 $mform->setDefault('unitpenalty', 0.1);
c5da9906 728 $mform->setDefault('unitgradingtype', 1);
cf146692
PP
729 $mform->setHelpButton('penaltygrp', array('penaltygrp', get_string('unitpenalty', 'qtype_numerical'), 'qtype_numerical'));
730 $mform->setDefault('showunits0', 0);
731 $mform->setDefault('showunits1', 3);
c5da9906 732 $mform->setDefault('unitsleft', 0);
c5da9906 733 $mform->setType('instructions', PARAM_RAW);
cf146692
PP
734 $mform->setHelpButton('instructions', array('instructions', get_string('instructions', 'qtype_numerical'), 'quiz'));
735 $mform->disabledIf('penaltygrp', 'unitrole','eq','1');
736 $mform->disabledIf('unitgradingtype', 'unitrole','eq','1');
737 $mform->disabledIf('instructions', 'unitrole','eq','1');
738 $mform->disabledIf('unitsleft', 'showunits1','eq','3');
739 $mform->disabledIf('showunits1','unitrole','eq','0');
740 $mform->disabledIf('showunits0','unitrole','eq','1');
8b831fbb 741
aeb15530 742
c5da9906 743 }
cf146692
PP
744/**
745 * function used in in function definition_inner()
746 * of edit_..._form.php for
747 * numerical, calculated, calculatedsimple
748 */
749 function add_units_elements(& $mform,& $that) {
750 $repeated = array();
751 $repeated[] =& $mform->createElement('header', 'unithdr', get_string('unithdr', 'qtype_numerical', '{no}'));
752
753 $repeated[] =& $mform->createElement('text', 'unit', get_string('unit', 'quiz'));
754 $mform->setType('unit', PARAM_NOTAGS);
755
756 $repeated[] =& $mform->createElement('text', 'multiplier', get_string('multiplier', 'quiz'));
757 $mform->setType('multiplier', PARAM_NUMBER);
758
759 if (isset($this->question->options)){
760 $countunits = count($that->question->options->units);
761 } else {
762 $countunits = 0;
763 }
764 if ($that->question->formoptions->repeatelements){
765 $repeatsatstart = $countunits + 1;
766 } else {
767 $repeatsatstart = $countunits;
768 }
769 $that->repeat_elements($repeated, $repeatsatstart, array(), 'nounits', 'addunits', 2, get_string('addmoreunitblanks', 'qtype_calculated', '{no}'));
770
771 if ($mform->elementExists('multiplier[0]')){
772 $firstunit =& $mform->getElement('multiplier[0]');
773 $firstunit->freeze();
774 $firstunit->setValue('1.0');
775 $firstunit->setPersistantFreeze(true);
776 }
777 }
778/**
779 * function use in in function validation()
780 * of edit_..._form.php for
781 * numerical, calculated, calculatedsimple
782 */
783
784 function validate_numerical_options(& $data, & $errors){
785 $units = $data['unit'];
786 if ($data['unitrole'] == 0 ){
787 $showunits = $data['showunits0'];
788 }else {
789 $showunits = $data['showunits1'];
790 }
791
792 if (($showunits == 0) || ($showunits == 1) || ($showunits == 2)){
793 if (trim($units[0]) == ''){
794 $errors['unit[0]'] = 'You must set a valid unit name' ;
795 }
796 }
797 if ($showunits == 3 ){
798 if (count($units)) {
799 foreach ($units as $key => $unit){
800 if ($units[$key] != ''){
801 $errors["unit[$key]"] = 'You must erase this unit name' ;
802 }
803 }
804 }
805 }
806
807
808 // Check double units.
809 $alreadyseenunits = array();
810 if (isset($data['unit'])) {
811 foreach ($data['unit'] as $key => $unit) {
812 $trimmedunit = trim($unit);
813 if ($trimmedunit!='' && in_array($trimmedunit, $alreadyseenunits)) {
814 $errors["unit[$key]"] = get_string('errorrepeatedunit', 'qtype_numerical');
815 if (trim($data['multiplier'][$key]) == '') {
816 $errors["multiplier[$key]"] = get_string('errornomultiplier', 'qtype_numerical');
817 }
818 } elseif($trimmedunit!='') {
819 $alreadyseenunits[] = $trimmedunit;
820 }
821 }
822 }
823 $units = $data['unit'];
824 if (count($units)) {
825 foreach ($units as $key => $unit){
826 if (is_numeric($unit)){
827 $errors['unit['.$key.']'] = get_string('mustnotbenumeric', 'qtype_calculated');
828 }
829 $trimmedunit = trim($unit);
830 $trimmedmultiplier = trim($data['multiplier'][$key]);
831 if (!empty($trimmedunit)){
832 if (empty($trimmedmultiplier)){
833 $errors['multiplier['.$key.']'] = get_string('youmustenteramultiplierhere', 'qtype_calculated');
834 }
835 if (!is_numeric($trimmedmultiplier)){
836 $errors['multiplier['.$key.']'] = get_string('mustbenumeric', 'qtype_calculated');
837 }
838
839 }
840 }
841 }
842
843 }
c5da9906 844
845 function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
846 // echo "<p>state uestion_grading_details $question->id $question->qtype <pre>";print_r($state);echo "</pre></p>";
847
f0b6151c 848 parent::print_question_grading_details($question, $state, $cmoptions, $options);
aeb15530
PS
849
850 }
851
c5da9906 852 function valid_unit($rawresponse, $units) {
853 // Make units more useful
854 $tmpunits = array();
855 foreach ($units as $unit) {
856 $tmpunits[$unit->unit] = $unit->multiplier;
857 }
858 // remove spaces and normalise decimal places.
859 $search = array(' ', ',');
860 $replace = array('', '.');
861 $rawresponse = str_replace($search, $replace, trim($rawresponse));
862
863 // Apply any unit that is present.
864 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
865 $rawresponse, $responseparts)) {
866
867 if (!empty($responseparts[5])) {
868
869 if (isset($tmpunits[$responseparts[5]])) {
870 // Valid number with unit.
871 return true ; //(float)$responseparts[1] / $tmpunits[$responseparts[5]];
1fe641f7 872 } else {
873 // Valid number with invalid unit. Must be wrong.
874 return false;
875 }
876
516cf3eb 877 } else {
1fe641f7 878 // Valid number without unit.
c5da9906 879 return false ; //(float)$responseparts[1];
516cf3eb 880 }
881 }
1fe641f7 882 // Invalid number. Must be wrong.
883 return false;
516cf3eb 884 }
f34488b2 885
1fe641f7 886 /// BACKUP FUNCTIONS ////////////////////////////
c5d94c41 887
1fe641f7 888 /**
c5d94c41 889 * Backup the data in the question
890 *
891 * This is used in question/backuplib.php
892 */
893 function backup($bf,$preferences,$question,$level=6) {
f34488b2 894 global $DB;
c5d94c41 895
896 $status = true;
897
f34488b2 898 $numericals = $DB->get_records('question_numerical', array('question' => $question), 'id ASC');
c5d94c41 899 //If there are numericals
900 if ($numericals) {
901 //Iterate over each numerical
902 foreach ($numericals as $numerical) {
903 $status = fwrite ($bf,start_tag("NUMERICAL",$level,true));
904 //Print numerical contents
905 fwrite ($bf,full_tag("ANSWER",$level+1,false,$numerical->answer));
906 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$numerical->tolerance));
907 //Now backup numerical_units
908 $status = question_backup_numerical_units($bf,$preferences,$question,7);
909 $status = fwrite ($bf,end_tag("NUMERICAL",$level,true));
910 }
c5da9906 911 $status = question_backup_numerical_options($bf,$preferences,$question,$level);
912 /* $numerical_options = $DB->get_records("question_numerical_options",array("questionid" => $question),"id");
913 if ($numerical_options) {
914 //Iterate over each numerical_option
915 foreach ($numerical_options as $numerical_option) {
916 $status = fwrite ($bf,start_tag("NUMERICAL_OPTIONS",$level,true));
917 //Print numerical_option contents
918 fwrite ($bf,full_tag("INSTRUCTIONS",$level+1,false,$numerical_option->instructions));
919 fwrite ($bf,full_tag("SHOWUNITS",$level+1,false,$numerical_option->showunits));
920 fwrite ($bf,full_tag("UNITSLEFT",$level+1,false,$numerical_option->unitsleft));
921 fwrite ($bf,full_tag("UNITGRADINGTYPE",$level+1,false,$numerical_option->unitgradingtype));
922 fwrite ($bf,full_tag("UNITPENALTY",$level+1,false,$numerical_option->unitpenalty));
923 $status = fwrite ($bf,end_tag("NUMERICAL_OPTIONS",$level,true));
924 }
925 }*/
926
c5d94c41 927 //Now print question_answers
928 $status = question_backup_answers($bf,$preferences,$question);
929 }
930 return $status;
931 }
932
1fe641f7 933 /// RESTORE FUNCTIONS /////////////////
315559d3 934
1fe641f7 935 /**
315559d3 936 * Restores the data in the question
937 *
938 * This is used in question/restorelib.php
939 */
940 function restore($old_question_id,$new_question_id,$info,$restore) {
9db7dab2 941 global $DB;
315559d3 942
943 $status = true;
944
945 //Get the numerical array
27cabbe6 946 if (isset($info['#']['NUMERICAL'])) {
947 $numericals = $info['#']['NUMERICAL'];
948 } else {
949 $numericals = array();
950 }
315559d3 951
952 //Iterate over numericals
953 for($i = 0; $i < sizeof($numericals); $i++) {
954 $num_info = $numericals[$i];
955
956 //Now, build the question_numerical record structure
1fe641f7 957 $numerical = new stdClass;
315559d3 958 $numerical->question = $new_question_id;
959 $numerical->answer = backup_todb($num_info['#']['ANSWER']['0']['#']);
960 $numerical->tolerance = backup_todb($num_info['#']['TOLERANCE']['0']['#']);
961
55894a42 962 //We have to recode the answer field
315559d3 963 $answer = backup_getid($restore->backup_unique_code,"question_answers",$numerical->answer);
964 if ($answer) {
965 $numerical->answer = $answer->new_id;
966 }
967
968 //The structure is equal to the db, so insert the question_numerical
9db7dab2 969 $newid = $DB->insert_record ("question_numerical", $numerical);
315559d3 970
971 //Do some output
972 if (($i+1) % 50 == 0) {
973 if (!defined('RESTORE_SILENTLY')) {
974 echo ".";
975 if (($i+1) % 1000 == 0) {
976 echo "<br />";
977 }
978 }
979 backup_flush(300);
980 }
981
982 //Now restore numerical_units
983 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$num_info,$restore);
984
c5da9906 985 //Now restore numerical_options
986 $status = question_restore_numerical_options ($old_question_id,$new_question_id,$num_info,$restore);
987
315559d3 988 if (!$newid) {
989 $status = false;
990 }
991 }
992
993 return $status;
994 }
995
b9bd6da4 996 /**
997 * Runs all the code required to set up and save an essay question for testing purposes.
998 * Alternate DB table prefix may be used to facilitate data deletion.
999 */
1000 function generate_test($name, $courseid = null) {
1001 global $DB;
1002 list($form, $question) = default_questiontype::generate_test($name, $courseid);
1003 $question->category = $form->category;
1004
1005 $form->questiontext = "What is 674 * 36?";
1006 $form->generalfeedback = "Thank you";
1007 $form->penalty = 0.1;
1008 $form->defaultgrade = 1;
1009 $form->noanswers = 3;
1010 $form->answer = array('24264', '24264', '1');
1011 $form->tolerance = array(10, 100, 0);
1012 $form->fraction = array(1, 0.5, 0);
1013 $form->nounits = 2;
1014 $form->unit = array(0 => null, 1 => null);
1015 $form->multiplier = array(1, 0);
1016 $form->feedback = array('Very good', 'Close, but not quite there', 'Well at least you tried....');
1017
1018 if ($courseid) {
1019 $course = $DB->get_record('course', array('id' => $courseid));
1020 }
1021
1022 return $this->save_question($question, $form, $course);
1023 }
516cf3eb 1024}
516cf3eb 1025
1fe641f7 1026// INITIATION - Without this line the question type is not in use.
a2156789 1027question_register_questiontype(new question_numerical_qtype());
aeb15530 1028