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