merge from 18-STABLE MDL-8809
[moodle.git] / question / type / calculated / questiontype.php
CommitLineData
516cf3eb 1<?php // $Id$
2
3/////////////////
a2156789 4// CALCULATED ///
516cf3eb 5/////////////////
6
7/// QUESTION TYPE CLASS //////////////////
8
aaae75b0 9require_once("$CFG->dirroot/question/type/datasetdependent/abstractqtype.php");
516cf3eb 10
32a189d6 11class question_calculated_qtype extends question_dataset_dependent_questiontype {
516cf3eb 12
13 // Used by the function custom_generator_tools:
14 var $calcgenerateidhasbeenadded = false;
15
16 function name() {
17 return 'calculated';
18 }
19
20 function get_question_options(&$question) {
21 // First get the datasets and default options
22 if(false === parent::get_question_options($question)) {
23 return false;
24 }
25
32a189d6 26 if (!$options = get_record('question_calculated', 'question', $question->id)) {
516cf3eb 27 notify("No options were found for calculated question
28 #{$question->id}! Proceeding with defaults.");
29 $options = new stdClass;
30 $options->tolerance = 0.01;
31 $options->tolerancetype = 1; // relative
32 $options->correctanswerlength = 2;
33 $options->correctanswerformat = 1; // decimals
34 }
35
36 // For historic reasons we also need these fields in the answer objects.
37 // This should eventually be removed and related code changed to use
38 // the values in $question->options instead.
39 foreach ($question->options->answers as $key => $answer) {
40 $answer = &$question->options->answers[$key]; // for PHP 4.x
41 $answer->calcid = $options->id;
42 $answer->tolerance = $options->tolerance;
43 $answer->tolerancetype = $options->tolerancetype;
44 $answer->correctanswerlength = $options->correctanswerlength;
45 $answer->correctanswerformat = $options->correctanswerformat;
46 }
47
48 $virtualqtype = $this->get_virtual_qtype();
49 $virtualqtype->get_numerical_units($question);
50
51 return true;
52 }
53
54 function save_question_options($question) {
55 //$options = $question->subtypeoptions;
56 // Get old answers:
57 global $CFG;
58 if (!$oldanswers = get_records_sql(
59 "SELECT a.*, c.tolerance, c.tolerancetype,
60 c.correctanswerlength, c.id AS calcid
dc1f00de 61 FROM {$CFG->prefix}question_answers a,
32a189d6 62 {$CFG->prefix}question_calculated c
f07d1d31 63 WHERE c.question = $question->id AND a.id = c.answer
64 ORDER BY a.id ASC")) {
516cf3eb 65 $oldanswers = array();
66 }
67
68 // Update with new answers
69 $answerrec->question = $calcrec->question = $question->id;
70 $n = count($question->answers);
71 for ($i = 0; $i < $n; $i++) {
72 $answerrec->answer = $question->answers[$i];
73 $answerrec->fraction = isset($question->fraction[$i])
74 ? $question->fraction[$i] : 1.0;
75 $answerrec->feedback = isset($question->feedback[$i])
76 ? $question->feedback[$i] : '';
77 $calcrec->tolerance = isset($question->tolerance[$i])
78 ? $question->tolerance[$i]
79 : $question->tolerance[0];
80 $calcrec->tolerancetype = isset($question->tolerancetype[$i])
81 ? $question->tolerancetype[$i]
82 : $question->tolerancetype[0];
83 $calcrec->correctanswerlength = isset($question->correctanswerlength[$i])
84 ? $question->correctanswerlength[$i]
85 : $question->correctanswerlength[0];
86 $calcrec->correctanswerformat = isset($question->correctanswerformat[$i])
87 ? $question->correctanswerformat[$i]
88 : $question->correctanswerformat[0];
89 if ($oldanswer = array_shift($oldanswers)) {
90 // Reuse old records:
91 $calcrec->answer = $answerrec->id = $oldanswer->id;
92 $calcrec->id = $oldanswer->calcid;
dc1f00de 93 if (!update_record('question_answers', $answerrec)) {
516cf3eb 94 error("Unable to update answer for calculated question #{$question->id}!");
95 } else {
96 // notify("Answer updated successfully for calculated question $question->name");
97 }
32a189d6 98 if (!update_record('question_calculated', $calcrec)) {
516cf3eb 99 error("Unable to update options for calculated question #{$question->id}!");
100 } else {
101 // notify("Options updated successfully for calculated question $question->name");
102 }
103 } else {
104 unset($answerrec->id);
105 unset($calcrec->id);
dc1f00de 106 if (!($calcrec->answer = insert_record('question_answers',
516cf3eb 107 $answerrec))) {
108 error("Unable to insert answer for calculated question $question->id");
109 } else {
110 // notify("Answer inserted successfully for calculated question $question->id");
111 }
32a189d6 112 if (!insert_record('question_calculated', $calcrec)) {
516cf3eb 113 error("Unable to insert options calculared question $question->id");
114 } else {
115 // notify("Options inserted successfully for calculated question $question->id");
116 }
117 }
118 }
119
120 // Delete excessive records:
121 foreach ($oldanswers as $oldanswer) {
dc1f00de 122 if (!delete_records('question_answers', 'id', $oldanswer->id)) {
516cf3eb 123 error("Unable to delete old answers for calculated question $question->id");
124 } else {
125 // notify("Old answers deleted successfully for calculated question $question->id");
126 }
32a189d6 127 if (!delete_records('question_calculated', 'id', $oldanswer->calcid)) {
516cf3eb 128 error("Unable to delete old options for calculated question $question->id");
129 } else {
130 // notify("Old options deleted successfully for calculated question $question->id");
131 }
132 }
133
134 // Save units
135 $virtualqtype = $this->get_virtual_qtype();
136 $virtualqtype->save_numerical_units($question);
137
138 return true;
139 }
140
141 function create_runtime_question($question, $form) {
142 $question = parent::create_runtime_question($question, $form);
143 $question->options->answers = array();
144 foreach ($form->answers as $key => $answer) {
145 $a->answer = trim($form->answer[$key]);
146 $a->tolerance = $form->tolerance[$key];
147 $a->tolerancetype = $form->tolerancetype[$key];
148 $a->correctanswerlength = $form->correctanswerlength[$key];
149 $a->correctanswerformat = $form->correctanswerformat[$key];
150 $question->options->answers[] = clone($a);
151 }
152
153 return $question;
154 }
155
156 function validate_form($form) {
157 switch($form->wizardpage) {
158 case 'question':
159 $calculatedmessages = array();
160 if (empty($form->name)) {
161 $calculatedmessages[] = get_string('missingname', 'quiz');
162 }
163 if (empty($form->questiontext)) {
164 $calculatedmessages[] = get_string('missingquestiontext', 'quiz');
165 }
166 // Verify formulas
167 foreach ($form->answers as $key => $answer) {
168 if ('' === trim($answer)) {
169 $calculatedmessages[] =
170 get_string('missingformula', 'quiz');
171 }
172 if ($formulaerrors =
7518b645 173 qtype_calculated_find_formula_errors($answer)) {
516cf3eb 174 $calculatedmessages[] = $formulaerrors;
175 }
176 if (! isset($form->tolerance[$key])) {
177 $form->tolerance[$key] = 0.0;
178 }
179 if (! is_numeric($form->tolerance[$key])) {
180 $calculatedmessages[] =
181 get_string('tolerancemustbenumeric', 'quiz');
182 }
183 }
184
185 if (!empty($calculatedmessages)) {
186 $errorstring = "The following errors were found:<br />";
187 foreach ($calculatedmessages as $msg) {
188 $errorstring .= $msg . '<br />';
189 }
190 error($errorstring);
191 }
192
193 break;
194 default:
195 return parent::validate_form($form);
196 break;
197 }
198 return true;
199 }
200
201 /**
202 * Deletes question from the question-type specific tables
203 *
204 * @return boolean Success/Failure
205 * @param object $question The question being deleted
206 */
90c3f310 207 function delete_question($questionid) {
208 delete_records("question_calculated", "question", $questionid);
209 delete_records("question_numerical_units", "question", $questionid);
210 if ($datasets = get_records('question_datasets', 'question', $questionid)) {
211 foreach ($datasets as $dataset) {
d7bc7024 212 delete_records('question_dataset_definitions', 'id', $dataset->datasetdefinition);
213 delete_records('question_dataset_items', 'definition', $dataset->datasetdefinition);
90c3f310 214 }
215 }
216 delete_records("question_datasets", "question", $questionid);
516cf3eb 217 return true;
218 }
219
60b5ecd3 220 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
516cf3eb 221 // Substitute variables in questiontext before giving the data to the
222 // virtual type for printing
223 $virtualqtype = $this->get_virtual_qtype();
224 $unit = $virtualqtype->get_default_numerical_unit($question);
225
226 // We modify the question to look like a numerical question
227 $numericalquestion = clone($question);
228 $numericalquestion->options = clone($question->options);
229 foreach ($question->options->answers as $key => $answer) {
230 $numericalquestion->options->answers[$key] = clone($answer);
231 }
232 foreach ($numericalquestion->options->answers as $key => $answer) {
233 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
7518b645 234 $correctanswer = qtype_calculated_calculate_answer(
516cf3eb 235 $answer->answer, $state->options->dataset, $answer->tolerance,
236 $answer->tolerancetype, $answer->correctanswerlength,
237 $answer->correctanswerformat, $unit->unit);
238 $answer->answer = $correctanswer->answer;
239 }
240 $numericalquestion->questiontext = parent::substitute_variables(
92186abc 241 $numericalquestion->questiontext, $state->options->dataset);
60b5ecd3 242 $virtualqtype->print_question_formulation_and_controls($numericalquestion, $state, $cmoptions, $options);
516cf3eb 243 }
244
245 function grade_responses(&$question, &$state, $cmoptions) {
246 // Forward the grading to the virtual qtype
247
248 // We modify the question to look like a numerical question
249 $numericalquestion = clone($question);
250 $numericalquestion->options = clone($question->options);
251 foreach ($question->options->answers as $key => $answer) {
252 $numericalquestion->options->answers[$key] = clone($answer);
253 }
254 foreach ($numericalquestion->options->answers as $key => $answer) {
255 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
256 $answer->answer = $this->substitute_variables($answer->answer,
257 $state->options->dataset);
258 }
259 return parent::grade_responses($numericalquestion, $state, $cmoptions);
260 }
261
0a5b58af 262 function response_summary($question, $state, $length=80) {
31d21f22 263 // The actual response is the bit after the hyphen
264 return substr($state->answer, strpos($state->answer, '-')+1, $length);
265 }
266
516cf3eb 267 // ULPGC ecastro
268 function check_response(&$question, &$state) {
269 // Forward the checking to the virtual qtype
270 // We modify the question to look like a numerical question
271 $numericalquestion = clone($question);
272 $numericalquestion->options = clone($question->options);
273 foreach ($question->options->answers as $key => $answer) {
274 $numericalquestion->options->answers[$key] = clone($answer);
275 }
276 foreach ($numericalquestion->options->answers as $key => $answer) {
277 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
278 $answer->answer = $this->substitute_variables($answer->answer,
279 $state->options->dataset);
280 }
281 return parent::check_response($numericalquestion, $state);
282 }
283
284 // ULPGC ecastro
285 function get_actual_response(&$question, &$state) {
286 // Substitute variables in questiontext before giving the data to the
287 // virtual type
288 $virtualqtype = $this->get_virtual_qtype();
289 $unit = $virtualqtype->get_default_numerical_unit($question);
290
291 // We modify the question to look like a numerical question
292 $numericalquestion = clone($question);
293 $numericalquestion->options = clone($question->options);
294 foreach ($question->options->answers as $key => $answer) {
295 $numericalquestion->options->answers[$key] = clone($answer);
296 }
297 foreach ($numericalquestion->options->answers as $key => $answer) {
298 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
299 $answer->answer = $this->substitute_variables($answer->answer,
300 $state->options->dataset);
301 // apply_unit
302 }
303 $numericalquestion->questiontext = parent::substitute_variables(
304 $numericalquestion->questiontext, $state->options->dataset);
305 $responses = $virtualqtype->get_all_responses($numericalquestion, $state);
306 $response = reset($responses->responses);
307 $correct = $response->answer.' : ';
308
309 $responses = $virtualqtype->get_actual_response($numericalquestion, $state);
310
311 foreach ($responses as $key=>$response){
312 $responses[$key] = $correct.$response;
313 }
314
315 return $responses;
316 }
317
318 function create_virtual_qtype() {
319 global $CFG;
aaae75b0 320 require_once("$CFG->dirroot/question/type/numerical/questiontype.php");
32a189d6 321 return new question_numerical_qtype();
516cf3eb 322 }
323
324 function supports_dataset_item_generation() {
325 // Calcualted support generation of randomly distributed number data
326 return true;
327 }
60b5ecd3 328 function custom_generator_tools_part(&$mform, $idx, $j){
329
330 $minmaxgrp = array();
331 $minmaxgrp[] =& $mform->createElement('text', "calcmin[$idx]", get_string('calcmin', 'qtype_datasetdependent'), 'size="3"');
332 $minmaxgrp[] =& $mform->createElement('text', "calcmax[$idx]", get_string('calcmax', 'qtype_datasetdependent'), 'size="3"');
333 $mform->addGroup($minmaxgrp, 'minmaxgrp', get_string('minmax', 'qtype_datasetdependent'), ' - ', false);
a8d2a373 334 $mform->setType('calcmin', PARAM_NUMBER);
335 $mform->setType('calcmax', PARAM_NUMBER);
60b5ecd3 336
337 $precisionoptions = range(0, 10);
338 $mform->addElement('select', "calclength[$idx]", get_string('calclength', 'qtype_datasetdependent'), $precisionoptions);
339
340 $distriboptions = array('uniform' => get_string('uniform', 'qtype_datasetdependent'), 'loguniform' => get_string('loguniform', 'qtype_datasetdependent'));
341 $mform->addElement('select', "calcdistribution[$idx]", get_string('calcdistribution', 'qtype_datasetdependent'), $distriboptions);
342
343
60b5ecd3 344 }
345
346 function custom_generator_set_data($datasetdefs, $formdata){
347 $idx = 1;
348 foreach ($datasetdefs as $datasetdef){
349 if (ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$', $datasetdef->options, $regs)) {
350 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
351 $formdata["calcdistribution[$idx]"] = $regs[1];
352 $formdata["calcmin[$idx]"] = $regs[2];
353 $formdata["calcmax[$idx]"] = $regs[3];
354 $formdata["calclength[$idx]"] = $regs[4];
355 }
356 $idx++;
357 }
358 return $formdata;
359 }
516cf3eb 360
361 function custom_generator_tools($datasetdef) {
362 if (ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$',
363 $datasetdef->options, $regs)) {
364 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
365 for ($i = 0 ; $i<10 ; ++$i) {
366 $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
367 ? 'decimals'
368 : 'significantfigures'), 'quiz', $i);
369 }
09275894 370 return '<input type="submit" onclick="'
d2ce367f 371 . "getElementById('addform').regenerateddefid.value='$defid'; return true;"
516cf3eb 372 .'" value="'. get_string('generatevalue', 'quiz') . '"/><br/>'
373 . '<input type="text" size="3" name="calcmin[]" '
374 . " value=\"$regs[2]\"/> &amp; <input name=\"calcmax[]\" "
375 . ' type="text" size="3" value="' . $regs[3] .'"/> '
376 . choose_from_menu($lengthoptions, 'calclength[]',
377 $regs[4], // Selected
378 '', '', '', true) . '<br/>'
379 . choose_from_menu(array('uniform' => get_string('uniform', 'quiz'),
380 'loguniform' => get_string('loguniform', 'quiz')),
381 'calcdistribution[]',
382 $regs[1], // Selected
383 '', '', '', true);
384 } else {
385 return '';
386 }
387 }
388
60b5ecd3 389
516cf3eb 390 function update_dataset_options($datasetdefs, $form) {
391 // Do we have informatin about new options???
392 if (empty($form->definition) || empty($form->calcmin)
393 || empty($form->calcmax) || empty($form->calclength)
394 || empty($form->calcdistribution)) {
a8d2a373 395 // I guess not
516cf3eb 396
397 } else {
398 // Looks like we just could have some new information here
60b5ecd3 399 $uniquedefs = array_values(array_unique($form->definition));
400 foreach ($uniquedefs as $key => $defid) {
516cf3eb 401 if (isset($datasetdefs[$defid])
60b5ecd3 402 && is_numeric($form->calcmin[$key+1])
403 && is_numeric($form->calcmax[$key+1])
404 && is_numeric($form->calclength[$key+1])) {
405 switch ($form->calcdistribution[$key+1]) {
516cf3eb 406 case 'uniform': case 'loguniform':
407 $datasetdefs[$defid]->options =
60b5ecd3 408 $form->calcdistribution[$key+1] . ':'
409 . $form->calcmin[$key+1] . ':'
410 . $form->calcmax[$key+1] . ':'
411 . $form->calclength[$key+1];
516cf3eb 412 break;
413 default:
60b5ecd3 414 notify("Unexpected distribution ".$form->calcdistribution[$key+1]);
516cf3eb 415 }
416 }
417 }
418 }
419
420 // Look for empty options, on which we set default values
421 foreach ($datasetdefs as $defid => $def) {
422 if (empty($def->options)) {
423 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1';
424 }
425 }
426 return $datasetdefs;
427 }
428
60b5ecd3 429 function save_dataset_items($question, $fromform){
430 if (empty($question->options)) {
431 $this->get_question_options($question);
432 }
433 //get the old datasets for this question
434 $datasetdefs = $this->get_dataset_definitions($question->id, array());
435 // Handle generator options...
436 $olddatasetdefs = fullclone($datasetdefs);
437 $datasetdefs = $this->update_dataset_options($datasetdefs, $fromform);
438 $maxnumber = -1;
439 foreach ($datasetdefs as $defid => $datasetdef) {
440 if (isset($datasetdef->id)
441 && $datasetdef->options != $olddatasetdefs[$defid]->options) {
442 // Save the new value for options
443 update_record('question_dataset_definitions', $datasetdef);
444
445 }
446 // Get maxnumber
447 if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) {
448 $maxnumber = $datasetdef->itemcount;
449 }
450 }
451 // Handle adding and removing of dataset items
452 $i = 1;
a8d2a373 453 ksort($fromform->definition);
60b5ecd3 454 foreach ($fromform->definition as $key => $defid) {
455 //if the delete button has not been pressed then skip the datasetitems
456 //in the 'add item' part of the form.
457 if ((!isset($fromform->addbutton)) && ($i > (count($datasetdefs)*$maxnumber))) {
458 break;
459 }
460 $addeditem = new stdClass();
461 $addeditem->definition = $datasetdefs[$defid]->id;
462 $addeditem->value = $fromform->number[$i];
463 $addeditem->itemnumber = ceil($i / count($datasetdefs));
464
465 if ($fromform->itemid[$i]) {
466 // Reuse any previously used record
467 $addeditem->id = $fromform->itemid[$i];
468 if (!update_record('question_dataset_items', $addeditem)) {
469 error("Error: Unable to update dataset item");
470 }
471 } else {
472 if (!insert_record('question_dataset_items', $addeditem)) {
473 error("Error: Unable to insert dataset item");
474 }
475 }
476
477 $i++;
478 }
479 if ($maxnumber < $addeditem->itemnumber){
480 $maxnumber = $addeditem->itemnumber;
481 foreach ($datasetdefs as $key => $newdef) {
482 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
483 $newdef->itemcount = $maxnumber;
484 // Save the new value for options
485 update_record('question_dataset_definitions', $newdef);
486 }
487 }
488 }
489 if (isset($fromform->deletebutton)) {
490 // Simply decrease itemcount where == $maxnumber
491 foreach ($datasetdefs as $datasetdef) {
492 if ($datasetdef->itemcount == $maxnumber) {
493 $datasetdef->itemcount--;
494 if (!update_record('question_dataset_definitions',
495 $datasetdef)) {
496 error("Error: Unable to update itemcount");
497 }
498 }
499 }
500 --$maxnumber;
501 }
502 }
516cf3eb 503 function generate_dataset_item($options) {
504 if (!ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$',
505 $options, $regs)) {
506 // Unknown options...
507 return false;
508 }
509 if ($regs[1] == 'uniform') {
510 $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
511 return round($nbr, $regs[4]);
512
513 } else if ($regs[1] == 'loguniform') {
514 $log0 = log(abs($regs[2])); // It would have worked the other way to
515 $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
516
517 // Reformat according to the precision $regs[4]:
518
519 // Determine the format 0.[1-9][0-9]* for the nbr...
520 $p10 = 0;
521 while ($nbr < 1) {
522 --$p10;
523 $nbr *= 10;
524 }
525 while ($nbr >= 1) {
526 ++$p10;
527 $nbr /= 10;
528 }
529 // ... and have the nbr rounded off to the correct length
530 $nbr = round($nbr, $regs[4]);
531
532 // Have the nbr written on a suitable format,
533 // Either scientific or plain numeric
534 if (-2 > $p10 || 4 < $p10) {
535 // Use scientific format:
536 $eX = 'e'.--$p10;
537 $nbr *= 10;
538 if (1 == $regs[4]) {
539 $nbr = $nbr.$eX;
540 } else {
541 // Attach additional zeros at the end of $nbr,
542 $nbr .= (1==strlen($nbr) ? '.' : '')
543 . '00000000000000000000000000000000000000000x';
544 $nbr = substr($nbr, 0, $regs[4] +1).$eX;
545 }
546 } else {
547 // Stick to plain numeric format
548 $nbr *= "1e$p10";
549 if (0.1 <= $nbr / "1e$regs[4]") {
550 $nbr = $nbr;
551 } else {
552 // Could be an idea to add some zeros here
553 $nbr .= (ereg('^[0-9]*$', $nbr) ? '.' : '')
554 . '00000000000000000000000000000000000000000x';
555 $oklen = $regs[4] + ($p10 < 1 ? 2-$p10 : 1);
556 $nbr = substr($nbr, 0, $oklen);
557 }
558 }
559
560 // The larger of the values decide the sign in case the
561 // have equal different signs (which they really must not have)
562 if ($regs[2] + $regs[3] > 0) {
563 return $nbr;
564 } else {
565 return -$nbr;
566 }
567
568 } else {
569 error("The distribution $regs[1] caused problems");
570 }
571 return '';
572 }
573
574 function comment_header($question) {
575 //$this->get_question_options($question);
576 global $SESSION;
577 $strheader = '';
578 $delimiter = '';
60b5ecd3 579
580 $answers = $question->options->answers;
581
516cf3eb 582 foreach ($answers as $answer) {
583 if (is_string($answer)) {
584 $strheader .= $delimiter.$answer;
585 } else {
586 $strheader .= $delimiter.$answer->answer;
587 }
588 $delimiter = ',';
589 }
590 return $strheader;
591 }
592
593 function comment_on_datasetitems($question, $data, $number) {
594 /// Find a default unit:
32a189d6 595 if (!empty($question->id) && $unit = get_record('question_numerical_units',
516cf3eb 596 'question', $question->id, 'multiplier', 1.0)) {
597 $unit = $unit->unit;
598 } else {
599 $unit = '';
600 }
601
602 $answers = $question->options->answers;
60b5ecd3 603 $stranswers = '';
516cf3eb 604 $strmin = get_string('min', 'quiz');
605 $strmax = get_string('max', 'quiz');
606 $errors = '';
607 $delimiter = ': ';
608 $virtualqtype = $this->get_virtual_qtype();
609 foreach ($answers as $answer) {
60b5ecd3 610 $formula = $answer->answer;
611 foreach ($data as $name => $value) {
612 $formula = str_replace('{'.$name.'}', $value, $formula);
613 }
7518b645 614 $calculated = qtype_calculated_calculate_answer(
516cf3eb 615 $answer->answer, $data, $answer->tolerance,
616 $answer->tolerancetype, $answer->correctanswerlength,
617 $answer->correctanswerformat, $unit);
618 $calculated->tolerance = $answer->tolerance;
619 $calculated->tolerancetype = $answer->tolerancetype;
620 $calculated->correctanswerlength = $answer->correctanswerlength;
621 $calculated->correctanswerformat = $answer->correctanswerformat;
622 $virtualqtype->get_tolerance_interval($calculated);
623 if ($calculated->min === '') {
624 // This should mean that something is wrong
625 $errors .= " -$calculated->answer";
626 $stranswers .= $delimiter;
627 } else {
60b5ecd3 628 $stranswers .= $formula.' = '.$calculated->answer. '<br/>';
516cf3eb 629 $strmin .= $delimiter.$calculated->min;
630 $strmax .= $delimiter.$calculated->max;
631 }
632 }
60b5ecd3 633 return "$stranswers$strmin<br/>$strmax<br/>$errors";
516cf3eb 634 }
635
636 function tolerance_types() {
637 return array('1' => get_string('relative', 'quiz'),
638 '2' => get_string('nominal', 'quiz'),
639 '3' => get_string('geometric', 'quiz'));
640 }
641
642 function dataset_options($form, $name, $renameabledatasets=false) {
643 // Takes datasets from the parent implementation but
644 // filters options that are currently not accepted by calculated
645 // It also determines a default selection...
646 list($options, $selected) = parent::dataset_options($form, $name);
647 foreach ($options as $key => $whatever) {
648 if (!ereg('^'.LITERAL.'-', $key) && $key != '0') {
649 unset($options[$key]);
650 }
651 }
652 if (!$selected) {
653 $selected = LITERAL . "-0-$name"; // Default
654 }
655 return array($options, $selected);
656 }
657
658 function construct_dataset_menus($form, $mandatorydatasets,
659 $optionaldatasets) {
660 $datasetmenus = array();
661 foreach ($mandatorydatasets as $datasetname) {
662 if (!isset($datasetmenus[$datasetname])) {
663 list($options, $selected) =
664 $this->dataset_options($form, $datasetname);
665 unset($options['0']); // Mandatory...
666 $datasetmenus[$datasetname] = choose_from_menu ($options,
667 'dataset[]', $selected, '', '', "0", true);
668 }
669 }
670 foreach ($optionaldatasets as $datasetname) {
671 if (!isset($datasetmenus[$datasetname])) {
672 list($options, $selected) =
673 $this->dataset_options($form, $datasetname);
674 $datasetmenus[$datasetname] = choose_from_menu ($options,
675 'dataset[]', $selected, '', '', "0", true);
676 }
677 }
678 return $datasetmenus;
679 }
680
681 function get_correct_responses(&$question, &$state) {
682 $virtualqtype = $this->get_virtual_qtype();
683 $unit = $virtualqtype->get_default_numerical_unit($question);
684 foreach ($question->options->answers as $answer) {
685 if (((int) $answer->fraction) === 1) {
7518b645 686 $answernumerical = qtype_calculated_calculate_answer(
516cf3eb 687 $answer->answer, $state->options->dataset, $answer->tolerance,
688 $answer->tolerancetype, $answer->correctanswerlength,
689 $answer->correctanswerformat, $unit->unit);
690 return array('' => $answernumerical->answer);
691 }
692 }
693 return null;
694 }
695
696 function substitute_variables($str, $dataset) {
697 $formula = parent::substitute_variables($str, $dataset);
7518b645 698 if ($error = qtype_calculated_find_formula_errors($formula)) {
516cf3eb 699 return $error;
700 }
701 /// Calculate the correct answer
702 if (empty($formula)) {
703 $str = '';
704 } else {
705 eval('$str = '.$formula.';');
706 }
707 return $str;
708 }
92186abc 709
c5d94c41 710/// BACKUP FUNCTIONS ////////////////////////////
711
712 /*
713 * Backup the data in the question
714 *
715 * This is used in question/backuplib.php
716 */
717 function backup($bf,$preferences,$question,$level=6) {
718
719 $status = true;
720
721 $calculateds = get_records("question_calculated","question",$question,"id");
722 //If there are calculated-s
723 if ($calculateds) {
724 //Iterate over each calculateds
725 foreach ($calculateds as $calculated) {
726 $status = $status &&fwrite ($bf,start_tag("CALCULATED",$level,true));
727 //Print calculated contents
728 fwrite ($bf,full_tag("ANSWER",$level+1,false,$calculated->answer));
729 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$calculated->tolerance));
730 fwrite ($bf,full_tag("TOLERANCETYPE",$level+1,false,$calculated->tolerancetype));
731 fwrite ($bf,full_tag("CORRECTANSWERLENGTH",$level+1,false,$calculated->correctanswerlength));
732 fwrite ($bf,full_tag("CORRECTANSWERFORMAT",$level+1,false,$calculated->correctanswerformat));
733 //Now backup numerical_units
734 $status = question_backup_numerical_units($bf,$preferences,$question,7);
735 //Now backup required dataset definitions and items...
736 $status = question_backup_datasets($bf,$preferences,$question,7);
737 //End calculated data
738 $status = $status &&fwrite ($bf,end_tag("CALCULATED",$level,true));
739 }
740 //Now print question_answers
741 $status = question_backup_answers($bf,$preferences,$question);
742 }
743 return $status;
744 }
315559d3 745
746/// RESTORE FUNCTIONS /////////////////
747
748 /*
749 * Restores the data in the question
750 *
751 * This is used in question/restorelib.php
752 */
753 function restore($old_question_id,$new_question_id,$info,$restore) {
754
755 $status = true;
756
757 //Get the calculated-s array
758 $calculateds = $info['#']['CALCULATED'];
759
760 //Iterate over calculateds
761 for($i = 0; $i < sizeof($calculateds); $i++) {
762 $cal_info = $calculateds[$i];
763 //traverse_xmlize($cal_info); //Debug
764 //print_object ($GLOBALS['traverse_array']); //Debug
765 //$GLOBALS['traverse_array']=""; //Debug
766
767 //Now, build the question_calculated record structure
768 $calculated->question = $new_question_id;
769 $calculated->answer = backup_todb($cal_info['#']['ANSWER']['0']['#']);
770 $calculated->tolerance = backup_todb($cal_info['#']['TOLERANCE']['0']['#']);
771 $calculated->tolerancetype = backup_todb($cal_info['#']['TOLERANCETYPE']['0']['#']);
772 $calculated->correctanswerlength = backup_todb($cal_info['#']['CORRECTANSWERLENGTH']['0']['#']);
773 $calculated->correctanswerformat = backup_todb($cal_info['#']['CORRECTANSWERFORMAT']['0']['#']);
774
775 ////We have to recode the answer field
776 $answer = backup_getid($restore->backup_unique_code,"question_answers",$calculated->answer);
777 if ($answer) {
778 $calculated->answer = $answer->new_id;
779 }
780
781 //The structure is equal to the db, so insert the question_calculated
782 $newid = insert_record ("question_calculated",$calculated);
783
784 //Do some output
785 if (($i+1) % 50 == 0) {
786 if (!defined('RESTORE_SILENTLY')) {
787 echo ".";
788 if (($i+1) % 1000 == 0) {
789 echo "<br />";
790 }
791 }
792 backup_flush(300);
793 }
794
795 //Now restore numerical_units
796 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$cal_info,$restore);
797
798 //Now restore dataset_definitions
799 if ($status && $newid) {
800 $status = question_restore_dataset_definitions ($old_question_id,$new_question_id,$cal_info,$restore);
801 }
802
803 if (!$newid) {
804 $status = false;
805 }
806 }
807
808 return $status;
809 }
516cf3eb 810}
811//// END OF CLASS ////
812
813//////////////////////////////////////////////////////////////////////////
814//// INITIATION - Without this line the question type is not in use... ///
815//////////////////////////////////////////////////////////////////////////
a2156789 816question_register_questiontype(new question_calculated_qtype());
516cf3eb 817
7518b645 818function qtype_calculated_calculate_answer($formula, $individualdata,
516cf3eb 819 $tolerance, $tolerancetype, $answerlength, $answerformat='1', $unit='') {
820/// The return value has these properties:
821/// ->answer the correct answer
822/// ->min the lower bound for an acceptable response
823/// ->max the upper bound for an accetpable response
824
825 /// Exchange formula variables with the correct values...
f02c6f01 826 global $QTYPES;
dfa47f96 827 $answer = $QTYPES['calculated']->substitute_variables($formula, $individualdata);
516cf3eb 828 if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */
829 /*** Adjust to the correct number of decimals ***/
830
831 $calculated->answer = round($answer, $answerlength);
832
833 if ($answerlength) {
834 /* Try to include missing zeros at the end */
835
836 if (ereg('^(.*\\.)(.*)$', $calculated->answer, $regs)) {
837 $calculated->answer = $regs[1] . substr(
838 $regs[2] . '00000000000000000000000000000000000000000x',
839 0, $answerlength)
840 . $unit;
841 } else {
842 $calculated->answer .=
843 substr('.00000000000000000000000000000000000000000x',
844 0, $answerlength + 1) . $unit;
845 }
846 } else {
847 /* Attach unit */
848 $calculated->answer .= $unit;
849 }
850
851 } else if ($answer) { // Significant figures does only apply if the result is non-zero
852
853 // Convert to positive answer...
854 if ($answer < 0) {
855 $answer = -$answer;
856 $sign = '-';
857 } else {
858 $sign = '';
859 }
860
861 // Determine the format 0.[1-9][0-9]* for the answer...
862 $p10 = 0;
863 while ($answer < 1) {
864 --$p10;
865 $answer *= 10;
866 }
867 while ($answer >= 1) {
868 ++$p10;
869 $answer /= 10;
870 }
871 // ... and have the answer rounded of to the correct length
872 $answer = round($answer, $answerlength);
873
874 // Have the answer written on a suitable format,
875 // Either scientific or plain numeric
876 if (-2 > $p10 || 4 < $p10) {
877 // Use scientific format:
878 $eX = 'e'.--$p10;
879 $answer *= 10;
880 if (1 == $answerlength) {
881 $calculated->answer = $sign.$answer.$eX.$unit;
882 } else {
883 // Attach additional zeros at the end of $answer,
884 $answer .= (1==strlen($answer) ? '.' : '')
885 . '00000000000000000000000000000000000000000x';
886 $calculated->answer = $sign
887 .substr($answer, 0, $answerlength +1).$eX.$unit;
888 }
889 } else {
890 // Stick to plain numeric format
891 $answer *= "1e$p10";
892 if (0.1 <= $answer / "1e$answerlength") {
893 $calculated->answer = $sign.$answer.$unit;
894 } else {
895 // Could be an idea to add some zeros here
896 $answer .= (ereg('^[0-9]*$', $answer) ? '.' : '')
897 . '00000000000000000000000000000000000000000x';
898 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
899 $calculated->answer = $sign.substr($answer, 0, $oklen).$unit;
900 }
901 }
902
903 } else {
904 $calculated->answer = 0.0;
905 }
906
907 /// Return the result
908 return $calculated;
909}
910
911
7518b645 912function qtype_calculated_find_formula_errors($formula) {
516cf3eb 913/// Validates the formula submitted from the question edit page.
914/// Returns false if everything is alright.
915/// Otherwise it constructs an error message
916
917 // Strip away dataset names
918 while (ereg('\\{[[:alpha:]][^>} <{"\']*\\}', $formula, $regs)) {
919 $formula = str_replace($regs[0], '1', $formula);
920 }
921
922 // Strip away empty space and lowercase it
923 $formula = strtolower(str_replace(' ', '', $formula));
924
925 $safeoperatorchar = '-+/*%>:^~<?=&|!'; /* */
926 $operatorornumber = "[$safeoperatorchar.0-9eE]";
927
928
929 while (ereg("(^|[$safeoperatorchar,(])([a-z0-9_]*)\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)",
930 $formula, $regs)) {
931
932 switch ($regs[2]) {
933 // Simple parenthesis
934 case '':
c9026379 935 if ($regs[4] || strlen($regs[3])==0) {
516cf3eb 936 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
937 }
938 break;
939
940 // Zero argument functions
941 case 'pi':
942 if ($regs[3]) {
943 return get_string('functiontakesnoargs', 'quiz', $regs[2]);
944 }
945 break;
946
947 // Single argument functions (the most common case)
948 case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
949 case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
950 case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
951 case 'exp': case 'expm1': case 'floor': case 'is_finite':
952 case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
953 case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
954 case 'tan': case 'tanh':
955 if ($regs[4] || empty($regs[3])) {
956 return get_string('functiontakesonearg','quiz',$regs[2]);
957 }
958 break;
959
960 // Functions that take one or two arguments
961 case 'log': case 'round':
962 if ($regs[5] || empty($regs[3])) {
963 return get_string('functiontakesoneortwoargs','quiz',$regs[2]);
964 }
965 break;
966
967 // Functions that must have two arguments
968 case 'atan2': case 'fmod': case 'pow':
969 if ($regs[5] || empty($regs[4])) {
970 return get_string('functiontakestwoargs', 'quiz', $regs[2]);
971 }
972 break;
973
974 // Functions that take two or more arguments
975 case 'min': case 'max':
976 if (empty($regs[4])) {
977 return get_string('functiontakesatleasttwo','quiz',$regs[2]);
978 }
979 break;
980
981 default:
982 return get_string('unsupportedformulafunction','quiz',$regs[2]);
983 }
984
985 // Exchange the function call with '1' and then chack for
986 // another function call...
987 if ($regs[1]) {
988 // The function call is proceeded by an operator
989 $formula = str_replace($regs[0], $regs[1] . '1', $formula);
990 } else {
991 // The function call starts the formula
992 $formula = ereg_replace("^$regs[2]\\([^)]*\\)", '1', $formula);
993 }
994 }
995
996 if (ereg("[^$safeoperatorchar.0-9eE]+", $formula, $regs)) {
997 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
998 } else {
999 // Formula just might be valid
1000 return false;
1001 }
1002}
1003
1004function dump($obj) {
1005 echo "<pre>\n";
1006 var_dump($obj);
1007 echo "</pre><br />\n";
1008}
1009
1010?>