MDL-20296 creating common functions to handle units in numerical, calculated and...
[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 ;
04e91671 28 public $valid_numerical_unit = false ;
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
04e91671 85 $question->options->showunits = NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ;
cf146692
PP
86 }else {
87 // only numerical will be graded
04e91671 88 $question->options->showunits = NUMERICALQUESTIONUNITNODISPLAY ;
cf146692
PP
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
04e91671 258 $options->showunits = NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ;
cf146692
PP
259 }else {
260 // only numerical will be graded
04e91671 261 $options->showunits = NUMERICALQUESTIONUNITNODISPLAY ;
cf146692
PP
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'] = '';
aeb15530 326
c5da9906 327 return true;
328 }
329 function restore_session_and_responses(&$question, &$state) {
330 if(false === strpos($state->responses[''], '|||||')){
331 // temporary
aeb15530 332 $state->responses['answer']= $state->responses[''];
c5da9906 333 $state->responses['unit'] = '';
1c5299bf 334 $this->split_old_answer($state->responses[''], $question->options->units, $state->responses['answer'] ,$state->responses['unit'] );
c5da9906 335 }else {
336 $responses = explode('|||||', $state->responses['']);
337 $state->responses['answer']= $responses[0];
338 $state->responses['unit'] = $responses[1];
339 }
c5da9906 340
c5da9906 341 return true;
342 }
343
344 function find_unit_index(&$question,$value){
345 $length = 0;
346 $goodkey = 0 ;
347 foreach ($question->options->units as $key => $unit){
348 if($unit->unit ==$value ) {
349 return $key ;
350 }
aeb15530 351 }
c5da9906 352 return 0 ;
353 }
354
355 function split_old_answer($rawresponse, $units, &$answer ,&$unit ) {
356 $answer = $rawresponse ;
357 // remove spaces and normalise decimal places.
358 $search = array(' ', ',');
359 $replace = array('', '.');
360 $rawresponse = str_replace($search, $replace, trim($rawresponse));
361 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
362 $rawresponse, $responseparts)) {
363 $unit = $responseparts[5] ;
364 $answer = $responseparts[1] ;
365 }
366 return ;
367 }
368
369
370 function save_session_and_responses(&$question, &$state) {
371 global $DB;
c5da9906 372
373 $responses = '';
374 if(isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']])){
375 $responses = $state->responses['answer'].'|||||'.$question->options->units[$state->responses['unit']]->unit;
376 }else if(isset($state->responses['unit'])){
377 $responses = $state->responses['answer'].'|||||'.$state->responses['unit'] ;
378 }else {
379 $responses = $state->responses['answer'].'|||||';
380 }
381 // Set the legacy answer field
382 if (!$DB->set_field('question_states', 'answer', $responses, array('id' => $state->id))) {
383 return false;
384 }
385 return true;
386 }
387
04e91671 388 /**
1fe641f7 389 * Deletes question from the question-type specific tables
390 *
391 * @return boolean Success/Failure
392 * @param object $question The question being deleted
393 */
90c3f310 394 function delete_question($questionid) {
f34488b2 395 global $DB;
396 $DB->delete_records("question_numerical", array("question" => $questionid));
c5da9906 397 $DB->delete_records("question_numerical_options", array("question" => $questionid));
f34488b2 398 $DB->delete_records("question_numerical_units", array("question" => $questionid));
516cf3eb 399 return true;
400 }
aeb15530 401
516cf3eb 402
5a14d563 403 function compare_responses(&$question, $state, $teststate) {
aa384ade 404
04e91671 405 if ($question->options->showunits == NUMERICALQUESTIONUNITMULTICHOICEDISPLAY && isset($question->options->units) && isset($question->options->units[$state->responses['unit']] )){
c5da9906 406 $state->responses['unit']=$question->options->units[$state->responses['unit']]->unit;
407 };
408
aeb15530 409
c5da9906 410 $responses = '';
411 $testresponses = '';
412 if (isset($state->responses['answer'])){
413 $responses = $state->responses['answer'];
414 }
415 if (isset($state->responses['unit'])){
416 $responses .= $state->responses['unit'];
417 }
418 if (isset($teststate->responses['answer'])){
419 $testresponses = $teststate->responses['answer'];
420 }
421 if (isset($teststate->responses['unit'])){
422 $testresponses .= $teststate->responses['unit'];
423 }
8b831fbb 424
f0b6151c 425 if ( isset($responses) && isset($testresponses )) {
c5da9906 426
427 return $responses == $testresponses ;
5a14d563 428 }
429 return false;
516cf3eb 430 }
431
1fe641f7 432 /**
433 * Checks whether a response matches a given answer, taking the tolerance
434 * and units into account. Returns a true for if a response matches the
435 * answer, false if it doesn't.
436 */
516cf3eb 437 function test_response(&$question, &$state, $answer) {
55894a42 438 // Deal with the match anything answer.
fac1189d 439 if ($answer->answer === '*') {
55894a42 440 return true;
516cf3eb 441 }
04e91671
PP
442 /* To be able to test (old) questions that do not have an unit
443 * input element the test is done using the $state->responses['']
444 * which contains the response which is analyzed by apply_unit()
445 * If the data comes from the numerical or calculated display
446 * the $state->responses['unit'] comes from either
447 * a multichoice radio element NUMERICALQUESTIONUNITMULTICHOICEDISPLAY
448 * where the $state->responses['unit'] value is the key => unit object
449 * in the the $question->options->units array
450 * or an input text element NUMERICALUNITTEXTINPUTDISPLAY
451 * which contains the student response
452 * for NUMERICALQUESTIONUNITTEXTDISPLAY and NUMERICALQUESTIONUNITNODISPLAY
453 *
454 */
04e91671 455 if ( ($question->options->showunits == NUMERICALQUESTIONUNITMULTICHOICEDISPLAY ||
aa384ade
PP
456 $question->options->showunits == NUMERICALQUESTIONUNITTEXTINPUTDISPLAY ||
457 $question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY ) &&
04e91671
PP
458 isset($state->responses['unit']) ){
459 $state->responses['']= $state->responses['answer'].$state->responses['unit'] ;
460
461 }else if (!isset($state->responses['']) && isset($state->responses['answer'])){
462
463 $state->responses['']= $state->responses['answer'] ;
c5da9906 464 }
04e91671
PP
465
466 $response = $this->apply_unit($state->responses[''], $question->options->units);
1fe641f7 467
468 if ($response === false) {
469 return false; // The student did not type a number.
516cf3eb 470 }
1fe641f7 471
472 // The student did type a number, so check it with tolerances.
473 $this->get_tolerance_interval($answer);
474 return ($answer->min <= $response && $response <= $answer->max);
516cf3eb 475 }
476
04e91671
PP
477 /**
478 * Performs response processing and grading
479 * The function was redefined for handling correctly the two parts
480 * number and unit of numerical or calculated questions
481 * The code handles also the case when there no unit defined by the user or
482 * when used in a multianswer (Cloze) question.
483 * This function performs response processing and grading and updates
484 * the state accordingly.
485 * @return boolean Indicates success or failure.
486 * @param object $question The question to be graded. Question type
487 * specific information is included.
488 * @param object $state The state of the question to grade. The current
489 * responses are in ->responses. The last graded state
490 * is in ->last_graded (hence the most recently graded
491 * responses are in ->last_graded->responses). The
492 * question type specific information is also
493 * included. The ->raw_grade and ->penalty fields
494 * must be updated. The method is able to
495 * close the question session (preventing any further
496 * attempts at this question) by setting
497 * $state->event to QUESTION_EVENTCLOSEANDGRADE
498 * @param object $cmoptions
499 */
500 function grade_responses(&$question, &$state, $cmoptions) {
c5da9906 501 //to apply the unit penalty we need to analyse the response in a more complex way
502 //the apply_unit() function analysis could be used to obtain the infos
aeb15530
PS
503 // however it is used to detect good or bad numbers but also
504 // gives false
c5da9906 505 $state->raw_grade = 0;
506 foreach($question->options->answers as $answer) {
507 if($this->test_response($question, $state, $answer)) {
508 $state->raw_grade = $answer->fraction;
509 $this->raw_unitgrade = $answer->fraction;
c5da9906 510 break;
511 }
512 }
0a49ee5c 513 // in all cases the unit should be tested
aa384ade
PP
514 if( $question->options->showunits == NUMERICALQUESTIONUNITNODISPLAY ||
515 $question->options->showunits == NUMERICALQUESTIONUNITTEXTDISPLAY ) {
516 $this->valid_numerical_unit = true ;
0a49ee5c
PP
517 }else {
518 $this->valid_numerical_unit = $this->valid_unit($state->responses[''], $question->options->units);
519 }
c5da9906 520 // apply unit penalty
521 $this->raw_unitpenalty = 0 ;
aa384ade
PP
522 if(!empty($question->options->unitpenalty)&& $this->valid_numerical_unit != true ){
523
c5da9906 524 if($question->options->unitgradingtype == 1){
04e91671 525 $this->raw_unitpenalty = $question->options->unitpenalty * $state->raw_grade ;
c5da9906 526 }else {
04e91671 527 $this->raw_unitpenalty = $question->options->unitpenalty * $question->maxgrade;
c5da9906 528 }
04e91671 529 $state->raw_grade -= $this->raw_unitpenalty ;
c5da9906 530 }
c5da9906 531 // Make sure we don't assign negative or too high marks.
c5da9906 532 $state->raw_grade = min(max((float) $state->raw_grade,
533 0.0), 1.0) * $question->maxgrade;
534
535 // Update the penalty.
536 $state->penalty = $question->penalty * $question->maxgrade;
537
538 // mark the state as graded
539 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
540
541 return true;
542 }
aeb15530
PS
543
544
516cf3eb 545 function get_correct_responses(&$question, &$state) {
546 $correct = parent::get_correct_responses($question, $state);
c85aea03 547 $unit = $this->get_default_numerical_unit($question);
78d2d576 548 $correct['answer']= $correct[''];
c85aea03 549 if (isset($correct['']) && $correct[''] != '*' && $unit) {
516cf3eb 550 $correct[''] .= ' '.$unit->unit;
78d2d576 551 $correct['unit']= $unit->unit;
aeb15530 552 }
516cf3eb 553 return $correct;
554 }
555
556 // ULPGC ecastro
557 function get_all_responses(&$question, &$state) {
1fe641f7 558 $result = new stdClass;
559 $answers = array();
516cf3eb 560 $unit = $this->get_default_numerical_unit($question);
561 if (is_array($question->options->answers)) {
562 foreach ($question->options->answers as $aid=>$answer) {
1fe641f7 563 $r = new stdClass;
516cf3eb 564 $r->answer = $answer->answer;
565 $r->credit = $answer->fraction;
566 $this->get_tolerance_interval($answer);
55894a42 567 if ($r->answer != '*' && $unit) {
568 $r->answer .= ' ' . $unit->unit;
516cf3eb 569 }
570 if ($answer->max != $answer->min) {
571 $max = "$answer->max"; //format_float($answer->max, 2);
572 $min = "$answer->min"; //format_float($answer->max, 2);
573 $r->answer .= ' ('.$min.'..'.$max.')';
574 }
575 $answers[$aid] = $r;
576 }
516cf3eb 577 }
578 $result->id = $question->id;
579 $result->responses = $answers;
580 return $result;
581 }
c5da9906 582 function get_actual_response($question, $state) {
aeb15530 583 if (!empty($state->responses) && !empty($state->responses[''])) {
c5da9906 584 if(false === strpos($state->responses[''], '|||||')){
585 $responses[] = $state->responses[''];
586 }else {
587 $resp = explode('|||||', $state->responses['']);
aeb15530 588 $responses[] = $resp[0].$resp[1];
c5da9906 589 }
590 } else {
591 $responses[] = '';
592 }
aeb15530 593
c5da9906 594 return $responses;
595 }
596
516cf3eb 597
598 function get_tolerance_interval(&$answer) {
599 // No tolerance
600 if (empty($answer->tolerance)) {
1fe641f7 601 $answer->tolerance = 0;
516cf3eb 602 }
603
604 // Calculate the interval of correct responses (min/max)
605 if (!isset($answer->tolerancetype)) {
606 $answer->tolerancetype = 2; // nominal
607 }
608
223ad0b9 609 // We need to add a tiny fraction depending on the set precision to make the
516cf3eb 610 // comparison work correctly. Otherwise seemingly equal values can yield
611 // false. (fixes bug #3225)
223ad0b9 612 $tolerance = (float)$answer->tolerance + ("1.0e-".ini_get('precision'));
516cf3eb 613 switch ($answer->tolerancetype) {
614 case '1': case 'relative':
615 /// Recalculate the tolerance and fall through
616 /// to the nominal case:
617 $tolerance = $answer->answer * $tolerance;
dcd4192a 618 // Do not fall through to the nominal case because the tiny fraction is a factor of the answer
619 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
620 $max = $answer->answer + $tolerance;
621 $min = $answer->answer - $tolerance;
622 break;
516cf3eb 623 case '2': case 'nominal':
624 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
dcd4192a 625 // $answer->tolerance 0 or something else
626 if ((float)$answer->tolerance == 0.0 && abs((float)$answer->answer) <= $tolerance ){
f34488b2 627 $tolerance = (float) ("1.0e-".ini_get('precision')) * abs((float)$answer->answer) ; //tiny fraction
dcd4192a 628 } else if ((float)$answer->tolerance != 0.0 && abs((float)$answer->tolerance) < abs((float)$answer->answer) && abs((float)$answer->answer) <= $tolerance){
f34488b2 629 $tolerance = (1+("1.0e-".ini_get('precision')) )* abs((float) $answer->tolerance) ;//tiny fraction
630 }
631
516cf3eb 632 $max = $answer->answer + $tolerance;
633 $min = $answer->answer - $tolerance;
634 break;
ed0ba6da 635 case '3': case 'geometric':
516cf3eb 636 $quotient = 1 + abs($tolerance);
637 $max = $answer->answer * $quotient;
638 $min = $answer->answer / $quotient;
639 break;
640 default:
0b4f4187 641 print_error('unknowntolerance', 'question', '', $answer->tolerancetype);
516cf3eb 642 }
643
644 $answer->min = $min;
645 $answer->max = $max;
646 return true;
647 }
648
649 /**
1fe641f7 650 * Checks if the $rawresponse has a unit and applys it if appropriate.
651 *
652 * @param string $rawresponse The response string to be converted to a float.
653 * @param array $units An array with the defined units, where the
654 * unit is the key and the multiplier the value.
655 * @return float The rawresponse with the unit taken into
656 * account as a float.
657 */
516cf3eb 658 function apply_unit($rawresponse, $units) {
659 // Make units more useful
660 $tmpunits = array();
661 foreach ($units as $unit) {
662 $tmpunits[$unit->unit] = $unit->multiplier;
663 }
1fe641f7 664 // remove spaces and normalise decimal places.
be1bb80e 665 $rawresponse = trim($rawresponse) ;
516cf3eb 666 $search = array(' ', ',');
be1bb80e
PP
667 // test if a . is present or there are multiple , (i.e. 2,456,789 ) so that we don't need spaces and ,
668 if ( strpos($rawresponse,'.' ) !== false || substr_count($rawresponse,',') > 1 ) {
669 $replace = array('', '');
670 }else { // remove spaces and normalise , to a . .
671 $replace = array('', '.');
672 }
673 $rawresponse = str_replace($search, $replace, $rawresponse);
f34488b2 674
1fe641f7 675 // Apply any unit that is present.
6dbcacee 676 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
1fe641f7 677 $rawresponse, $responseparts)) {
f34488b2 678
1fe641f7 679 if (!empty($responseparts[5])) {
f34488b2 680
1fe641f7 681 if (isset($tmpunits[$responseparts[5]])) {
682 // Valid number with unit.
683 return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
c5da9906 684 } else {
aeb15530 685 // Valid number with invalid unit.
c5da9906 686 return (float)$responseparts[1];
687 }
688
689 } else {
690 // Valid number without unit.
691 return (float)$responseparts[1];
692 }
693 }
694 // Invalid number. Must be wrong.
695 return false;
696 }
04e91671
PP
697 /**
698 * function used in function definition_inner()
699 * of edit_..._form.php for
700 * numerical, calculated, calculatedsimple
701 */
cf146692
PP
702 function add_units_options(&$mform, &$that){
703 $mform->addElement('header', 'unithandling', get_string('unitshandling', 'qtype_numerical'));
704 // Units are graded
705 $mform->addElement('radio', 'unitrole', get_string('unitgraded1', 'qtype_numerical'), get_string('unitgraded', 'qtype_numerical'),0);
706 $penaltygrp = array();
707 $penaltygrp[] =& $mform->createElement('text', 'unitpenalty', get_string('unitpenalty', 'qtype_numerical') ,
708 array('size' => 6));
709 $unitgradingtypes = array('1' => get_string('decfractionofquestiongrade', 'qtype_numerical'), '2' => get_string('decfractionofresponsegrade', 'qtype_numerical'));
710 $penaltygrp[] =& $mform->createElement('select', 'unitgradingtype', '' , $unitgradingtypes );
711 $mform->addGroup($penaltygrp, 'penaltygrp', get_string('unitpenalty', 'qtype_numerical'),' ' , false);
712 $showunits0grp = array();
713 $showunits0grp[] =& $mform->createElement('radio', 'showunits0', get_string('unitedit', 'qtype_numerical'), get_string('editableunittext', 'qtype_numerical'),0);
714 $showunits0grp[] =& $mform->createElement('radio', 'showunits0', get_string('selectunits', 'qtype_numerical') , get_string('unitchoice', 'qtype_numerical'),1);
715 $mform->addGroup($showunits0grp, 'showunits0grp', get_string('studentunitanswer', 'qtype_numerical'),' OR ' , false);
716 $mform->addElement('htmleditor', 'instructions', get_string('instructions', 'qtype_numerical'),
717 array('rows' => 10, 'course' => $that->coursefilesid));
718 $mform->addElement('static', 'separator1', '<HR/>', '<HR/>');
719 // Units are not graded
720 $mform->addElement('radio', 'unitrole', get_string('unitnotgraded', 'qtype_numerical'), get_string('onlynumerical', 'qtype_numerical'),1);
721 $showunits1grp = array();
722 $showunits1grp[] = & $mform->createElement('radio', 'showunits1', '', get_string('no', 'moodle'),3);
723 $showunits1grp[] = & $mform->createElement('radio', 'showunits1', '', get_string('yes', 'moodle'),2);
724 $mform->addGroup($showunits1grp, 'showunits1grp', get_string('unitdisplay', 'qtype_numerical'),' ' , false);
725 $unitslefts = array('0' => get_string('rightexample', 'qtype_numerical'),'1' => get_string('leftexample', 'qtype_numerical'));
726 $mform->addElement('static', 'separator2', '<HR/>', '<HR/>');
727 $mform->addElement('select', 'unitsleft', get_string('unitposition', 'qtype_numerical') , $unitslefts );
8b831fbb 728 $currentgrp1 = array();
aeb15530 729
c5da9906 730 $mform->setType('unitpenalty', PARAM_NUMBER);
c5da9906 731 $mform->setDefault('unitpenalty', 0.1);
c5da9906 732 $mform->setDefault('unitgradingtype', 1);
cf146692
PP
733 $mform->setHelpButton('penaltygrp', array('penaltygrp', get_string('unitpenalty', 'qtype_numerical'), 'qtype_numerical'));
734 $mform->setDefault('showunits0', 0);
735 $mform->setDefault('showunits1', 3);
c5da9906 736 $mform->setDefault('unitsleft', 0);
c5da9906 737 $mform->setType('instructions', PARAM_RAW);
cf146692
PP
738 $mform->setHelpButton('instructions', array('instructions', get_string('instructions', 'qtype_numerical'), 'quiz'));
739 $mform->disabledIf('penaltygrp', 'unitrole','eq','1');
740 $mform->disabledIf('unitgradingtype', 'unitrole','eq','1');
741 $mform->disabledIf('instructions', 'unitrole','eq','1');
742 $mform->disabledIf('unitsleft', 'showunits1','eq','3');
743 $mform->disabledIf('showunits1','unitrole','eq','0');
744 $mform->disabledIf('showunits0','unitrole','eq','1');
8b831fbb 745
aeb15530 746
c5da9906 747 }
cf146692
PP
748/**
749 * function used in in function definition_inner()
750 * of edit_..._form.php for
751 * numerical, calculated, calculatedsimple
752 */
753 function add_units_elements(& $mform,& $that) {
754 $repeated = array();
755 $repeated[] =& $mform->createElement('header', 'unithdr', get_string('unithdr', 'qtype_numerical', '{no}'));
756
757 $repeated[] =& $mform->createElement('text', 'unit', get_string('unit', 'quiz'));
758 $mform->setType('unit', PARAM_NOTAGS);
759
760 $repeated[] =& $mform->createElement('text', 'multiplier', get_string('multiplier', 'quiz'));
761 $mform->setType('multiplier', PARAM_NUMBER);
762
04e91671 763 if (isset($that->question->options)){
cf146692
PP
764 $countunits = count($that->question->options->units);
765 } else {
766 $countunits = 0;
767 }
768 if ($that->question->formoptions->repeatelements){
769 $repeatsatstart = $countunits + 1;
770 } else {
771 $repeatsatstart = $countunits;
772 }
773 $that->repeat_elements($repeated, $repeatsatstart, array(), 'nounits', 'addunits', 2, get_string('addmoreunitblanks', 'qtype_calculated', '{no}'));
774
775 if ($mform->elementExists('multiplier[0]')){
776 $firstunit =& $mform->getElement('multiplier[0]');
777 $firstunit->freeze();
778 $firstunit->setValue('1.0');
779 $firstunit->setPersistantFreeze(true);
638406cb 780 $mform->setHelpButton('multiplier[0]', array('numericalmultiplier', get_string('numericalmultiplier', 'qtype_numerical'), 'qtype_numerical'));
cf146692
PP
781 }
782 }
783/**
784 * function use in in function validation()
785 * of edit_..._form.php for
786 * numerical, calculated, calculatedsimple
787 */
788
789 function validate_numerical_options(& $data, & $errors){
790 $units = $data['unit'];
791 if ($data['unitrole'] == 0 ){
792 $showunits = $data['showunits0'];
793 }else {
794 $showunits = $data['showunits1'];
795 }
796
04e91671
PP
797 if (($showunits == NUMERICALQUESTIONUNITTEXTINPUTDISPLAY) ||
798 ($showunits == NUMERICALQUESTIONUNITMULTICHOICEDISPLAY ) ||
799 ($showunits == NUMERICALQUESTIONUNITTEXTDISPLAY )){
cf146692
PP
800 if (trim($units[0]) == ''){
801 $errors['unit[0]'] = 'You must set a valid unit name' ;
802 }
803 }
04e91671 804 if ($showunits == NUMERICALQUESTIONUNITNODISPLAY ){
cf146692
PP
805 if (count($units)) {
806 foreach ($units as $key => $unit){
807 if ($units[$key] != ''){
808 $errors["unit[$key]"] = 'You must erase this unit name' ;
809 }
810 }
811 }
812 }
813
814
815 // Check double units.
816 $alreadyseenunits = array();
817 if (isset($data['unit'])) {
818 foreach ($data['unit'] as $key => $unit) {
819 $trimmedunit = trim($unit);
820 if ($trimmedunit!='' && in_array($trimmedunit, $alreadyseenunits)) {
821 $errors["unit[$key]"] = get_string('errorrepeatedunit', 'qtype_numerical');
822 if (trim($data['multiplier'][$key]) == '') {
823 $errors["multiplier[$key]"] = get_string('errornomultiplier', 'qtype_numerical');
824 }
825 } elseif($trimmedunit!='') {
826 $alreadyseenunits[] = $trimmedunit;
827 }
828 }
829 }
830 $units = $data['unit'];
831 if (count($units)) {
832 foreach ($units as $key => $unit){
833 if (is_numeric($unit)){
834 $errors['unit['.$key.']'] = get_string('mustnotbenumeric', 'qtype_calculated');
835 }
836 $trimmedunit = trim($unit);
837 $trimmedmultiplier = trim($data['multiplier'][$key]);
838 if (!empty($trimmedunit)){
839 if (empty($trimmedmultiplier)){
840 $errors['multiplier['.$key.']'] = get_string('youmustenteramultiplierhere', 'qtype_calculated');
841 }
842 if (!is_numeric($trimmedmultiplier)){
843 $errors['multiplier['.$key.']'] = get_string('mustbenumeric', 'qtype_calculated');
844 }
845
846 }
847 }
848 }
849
850 }
c5da9906 851
aeb15530 852
04e91671 853 function valid_unit($rawresponse, $units) {
c5da9906 854 // Make units more useful
855 $tmpunits = array();
856 foreach ($units as $unit) {
857 $tmpunits[$unit->unit] = $unit->multiplier;
858 }
859 // remove spaces and normalise decimal places.
860 $search = array(' ', ',');
861 $replace = array('', '.');
862 $rawresponse = str_replace($search, $replace, trim($rawresponse));
863
864 // Apply any unit that is present.
865 if (preg_match('~^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$~',
866 $rawresponse, $responseparts)) {
867
868 if (!empty($responseparts[5])) {
869
870 if (isset($tmpunits[$responseparts[5]])) {
871 // Valid number with unit.
872 return true ; //(float)$responseparts[1] / $tmpunits[$responseparts[5]];
1fe641f7 873 } else {
874 // Valid number with invalid unit. Must be wrong.
875 return false;
876 }
877
516cf3eb 878 } else {
1fe641f7 879 // Valid number without unit.
c5da9906 880 return false ; //(float)$responseparts[1];
516cf3eb 881 }
882 }
1fe641f7 883 // Invalid number. Must be wrong.
884 return false;
516cf3eb 885 }
f34488b2 886
1fe641f7 887 /// BACKUP FUNCTIONS ////////////////////////////
c5d94c41 888
1fe641f7 889 /**
c5d94c41 890 * Backup the data in the question
891 *
892 * This is used in question/backuplib.php
893 */
894 function backup($bf,$preferences,$question,$level=6) {
f34488b2 895 global $DB;
c5d94c41 896
897 $status = true;
898
f34488b2 899 $numericals = $DB->get_records('question_numerical', array('question' => $question), 'id ASC');
c5d94c41 900 //If there are numericals
901 if ($numericals) {
902 //Iterate over each numerical
903 foreach ($numericals as $numerical) {
904 $status = fwrite ($bf,start_tag("NUMERICAL",$level,true));
905 //Print numerical contents
906 fwrite ($bf,full_tag("ANSWER",$level+1,false,$numerical->answer));
907 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$numerical->tolerance));
908 //Now backup numerical_units
909 $status = question_backup_numerical_units($bf,$preferences,$question,7);
910 $status = fwrite ($bf,end_tag("NUMERICAL",$level,true));
911 }
c5da9906 912 $status = question_backup_numerical_options($bf,$preferences,$question,$level);
913 /* $numerical_options = $DB->get_records("question_numerical_options",array("questionid" => $question),"id");
914 if ($numerical_options) {
915 //Iterate over each numerical_option
916 foreach ($numerical_options as $numerical_option) {
917 $status = fwrite ($bf,start_tag("NUMERICAL_OPTIONS",$level,true));
918 //Print numerical_option contents
919 fwrite ($bf,full_tag("INSTRUCTIONS",$level+1,false,$numerical_option->instructions));
920 fwrite ($bf,full_tag("SHOWUNITS",$level+1,false,$numerical_option->showunits));
921 fwrite ($bf,full_tag("UNITSLEFT",$level+1,false,$numerical_option->unitsleft));
922 fwrite ($bf,full_tag("UNITGRADINGTYPE",$level+1,false,$numerical_option->unitgradingtype));
923 fwrite ($bf,full_tag("UNITPENALTY",$level+1,false,$numerical_option->unitpenalty));
924 $status = fwrite ($bf,end_tag("NUMERICAL_OPTIONS",$level,true));
925 }
926 }*/
927
c5d94c41 928 //Now print question_answers
929 $status = question_backup_answers($bf,$preferences,$question);
930 }
931 return $status;
932 }
933
1fe641f7 934 /// RESTORE FUNCTIONS /////////////////
315559d3 935
1fe641f7 936 /**
315559d3 937 * Restores the data in the question
938 *
939 * This is used in question/restorelib.php
940 */
941 function restore($old_question_id,$new_question_id,$info,$restore) {
9db7dab2 942 global $DB;
315559d3 943
944 $status = true;
945
946 //Get the numerical array
27cabbe6 947 if (isset($info['#']['NUMERICAL'])) {
948 $numericals = $info['#']['NUMERICAL'];
949 } else {
950 $numericals = array();
951 }
315559d3 952
953 //Iterate over numericals
954 for($i = 0; $i < sizeof($numericals); $i++) {
955 $num_info = $numericals[$i];
956
957 //Now, build the question_numerical record structure
1fe641f7 958 $numerical = new stdClass;
315559d3 959 $numerical->question = $new_question_id;
960 $numerical->answer = backup_todb($num_info['#']['ANSWER']['0']['#']);
961 $numerical->tolerance = backup_todb($num_info['#']['TOLERANCE']['0']['#']);
962
55894a42 963 //We have to recode the answer field
315559d3 964 $answer = backup_getid($restore->backup_unique_code,"question_answers",$numerical->answer);
965 if ($answer) {
966 $numerical->answer = $answer->new_id;
967 }
968
969 //The structure is equal to the db, so insert the question_numerical
9db7dab2 970 $newid = $DB->insert_record ("question_numerical", $numerical);
315559d3 971
972 //Do some output
973 if (($i+1) % 50 == 0) {
974 if (!defined('RESTORE_SILENTLY')) {
975 echo ".";
976 if (($i+1) % 1000 == 0) {
977 echo "<br />";
978 }
979 }
980 backup_flush(300);
981 }
982
983 //Now restore numerical_units
984 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$num_info,$restore);
985
c5da9906 986 //Now restore numerical_options
987 $status = question_restore_numerical_options ($old_question_id,$new_question_id,$num_info,$restore);
988
315559d3 989 if (!$newid) {
990 $status = false;
991 }
992 }
993
994 return $status;
995 }
996
b9bd6da4 997 /**
998 * Runs all the code required to set up and save an essay question for testing purposes.
999 * Alternate DB table prefix may be used to facilitate data deletion.
1000 */
1001 function generate_test($name, $courseid = null) {
1002 global $DB;
1003 list($form, $question) = default_questiontype::generate_test($name, $courseid);
1004 $question->category = $form->category;
1005
1006 $form->questiontext = "What is 674 * 36?";
1007 $form->generalfeedback = "Thank you";
1008 $form->penalty = 0.1;
1009 $form->defaultgrade = 1;
1010 $form->noanswers = 3;
1011 $form->answer = array('24264', '24264', '1');
1012 $form->tolerance = array(10, 100, 0);
1013 $form->fraction = array(1, 0.5, 0);
1014 $form->nounits = 2;
1015 $form->unit = array(0 => null, 1 => null);
1016 $form->multiplier = array(1, 0);
1017 $form->feedback = array('Very good', 'Close, but not quite there', 'Well at least you tried....');
1018
1019 if ($courseid) {
1020 $course = $DB->get_record('course', array('id' => $courseid));
1021 }
1022
1023 return $this->save_question($question, $form, $course);
1024 }
516cf3eb 1025}
516cf3eb 1026
1fe641f7 1027// INITIATION - Without this line the question type is not in use.
a2156789 1028question_register_questiontype(new question_numerical_qtype());
04e91671
PP
1029if ( ! defined ("NUMERICALQUESTIONUNITTEXTINPUTDISPLAY")) {
1030 define("NUMERICALQUESTIONUNITTEXTINPUTDISPLAY", 0);
1031}
1032if ( ! defined ("NUMERICALQUESTIONUNITMULTICHOICEDISPLAY")) {
1033 define("NUMERICALQUESTIONUNITMULTICHOICEDISPLAY", 1);
1034}
1035if ( ! defined ("NUMERICALQUESTIONUNITTEXTDISPLAY")) {
1036 define("NUMERICALQUESTIONUNITTEXTDISPLAY", 2);
1037}
1038if ( ! defined ("NUMERICALQUESTIONUNITNODISPLAY")) {
1039 define("NUMERICALQUESTIONUNITNODISPLAY", 3);
1040}