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