Moving quiz-independent question scripts to their new location. In a following commit...
[moodle.git] / question / questiontypes / calculated / questiontype.php
CommitLineData
516cf3eb 1<?php // $Id$
2
3/////////////////
4/// CALCULATED ///
5/////////////////
6
7/// QUESTION TYPE CLASS //////////////////
8
9require_once("$CFG->dirroot/mod/quiz/questiontypes/datasetdependent/abstractqtype.php");
10
11class quiz_calculated_qtype extends quiz_dataset_dependent_questiontype {
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
26 if (!$options = get_record('quiz_calculated', 'question', $question->id)) {
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
61 FROM {$CFG->prefix}quiz_answers a,
62 {$CFG->prefix}quiz_calculated c
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;
92 if (!update_record('quiz_answers', $answerrec)) {
93 error("Unable to update answer for calculated question #{$question->id}!");
94 } else {
95 // notify("Answer updated successfully for calculated question $question->name");
96 }
97 if (!update_record('quiz_calculated', $calcrec)) {
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);
105 if (!($calcrec->answer = insert_record('quiz_answers',
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 }
111 if (!insert_record('quiz_calculated', $calcrec)) {
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) {
121 if (!delete_records('quiz_answers', 'id', $oldanswer->id)) {
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 }
126 if (!delete_records('quiz_calculated', 'id', $oldanswer->calcid)) {
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 =
172 quiz_qtype_calculated_find_formula_errors($answer)) {
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 */
206 function delete_question($question) {
207 delete_records("quiz_calculated", "question", $question->id);
208 delete_records("quiz_numerical_units", "question", $question->id);
209 delete_records("quiz_question_datasets", "question", $question->id);
210 //TODO: delete entries from the quiz_dataset_items and quiz_dataset_definitions tables
211 return true;
212 }
213
214 function print_question_formulation_and_controls(&$question, &$state, $cmoptions,
215 $options) {
216 // Substitute variables in questiontext before giving the data to the
217 // virtual type for printing
218 $virtualqtype = $this->get_virtual_qtype();
219 $unit = $virtualqtype->get_default_numerical_unit($question);
220
221 // We modify the question to look like a numerical question
222 $numericalquestion = clone($question);
223 $numericalquestion->options = clone($question->options);
224 foreach ($question->options->answers as $key => $answer) {
225 $numericalquestion->options->answers[$key] = clone($answer);
226 }
227 foreach ($numericalquestion->options->answers as $key => $answer) {
228 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
229 $correctanswer = quiz_qtype_calculated_calculate_answer(
230 $answer->answer, $state->options->dataset, $answer->tolerance,
231 $answer->tolerancetype, $answer->correctanswerlength,
232 $answer->correctanswerformat, $unit->unit);
233 $answer->answer = $correctanswer->answer;
234 }
235 $numericalquestion->questiontext = parent::substitute_variables(
236 $numericalquestion->questiontext, $state->options->dataset);
237 $virtualqtype->print_question_formulation_and_controls($numericalquestion,
238 $state, $cmoptions, $options);
239 }
240
241 function grade_responses(&$question, &$state, $cmoptions) {
242 // Forward the grading to the virtual qtype
243
244 // We modify the question to look like a numerical question
245 $numericalquestion = clone($question);
246 $numericalquestion->options = clone($question->options);
247 foreach ($question->options->answers as $key => $answer) {
248 $numericalquestion->options->answers[$key] = clone($answer);
249 }
250 foreach ($numericalquestion->options->answers as $key => $answer) {
251 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
252 $answer->answer = $this->substitute_variables($answer->answer,
253 $state->options->dataset);
254 }
255 return parent::grade_responses($numericalquestion, $state, $cmoptions);
256 }
257
258 // ULPGC ecastro
259 function check_response(&$question, &$state) {
260 // Forward the checking to the virtual qtype
261 // We modify the question to look like a numerical question
262 $numericalquestion = clone($question);
263 $numericalquestion->options = clone($question->options);
264 foreach ($question->options->answers as $key => $answer) {
265 $numericalquestion->options->answers[$key] = clone($answer);
266 }
267 foreach ($numericalquestion->options->answers as $key => $answer) {
268 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
269 $answer->answer = $this->substitute_variables($answer->answer,
270 $state->options->dataset);
271 }
272 return parent::check_response($numericalquestion, $state);
273 }
274
275 // ULPGC ecastro
276 function get_actual_response(&$question, &$state) {
277 // Substitute variables in questiontext before giving the data to the
278 // virtual type
279 $virtualqtype = $this->get_virtual_qtype();
280 $unit = $virtualqtype->get_default_numerical_unit($question);
281
282 // We modify the question to look like a numerical question
283 $numericalquestion = clone($question);
284 $numericalquestion->options = clone($question->options);
285 foreach ($question->options->answers as $key => $answer) {
286 $numericalquestion->options->answers[$key] = clone($answer);
287 }
288 foreach ($numericalquestion->options->answers as $key => $answer) {
289 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
290 $answer->answer = $this->substitute_variables($answer->answer,
291 $state->options->dataset);
292 // apply_unit
293 }
294 $numericalquestion->questiontext = parent::substitute_variables(
295 $numericalquestion->questiontext, $state->options->dataset);
296 $responses = $virtualqtype->get_all_responses($numericalquestion, $state);
297 $response = reset($responses->responses);
298 $correct = $response->answer.' : ';
299
300 $responses = $virtualqtype->get_actual_response($numericalquestion, $state);
301
302 foreach ($responses as $key=>$response){
303 $responses[$key] = $correct.$response;
304 }
305
306 return $responses;
307 }
308
309 function create_virtual_qtype() {
310 global $CFG;
311 require_once("$CFG->dirroot/mod/quiz/questiontypes/numerical/questiontype.php");
312 return new quiz_numerical_qtype();
313 }
314
315 function supports_dataset_item_generation() {
316 // Calcualted support generation of randomly distributed number data
317 return true;
318 }
319
320 function custom_generator_tools($datasetdef) {
321 if (ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$',
322 $datasetdef->options, $regs)) {
323 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
324 for ($i = 0 ; $i<10 ; ++$i) {
325 $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
326 ? 'decimals'
327 : 'significantfigures'), 'quiz', $i);
328 }
329 return '<input type="submit" onClick="'
330 . "document.addform.regenerateddefid.value='$defid'; return true;"
331 .'" value="'. get_string('generatevalue', 'quiz') . '"/><br/>'
332 . '<input type="text" size="3" name="calcmin[]" '
333 . " value=\"$regs[2]\"/> &amp; <input name=\"calcmax[]\" "
334 . ' type="text" size="3" value="' . $regs[3] .'"/> '
335 . choose_from_menu($lengthoptions, 'calclength[]',
336 $regs[4], // Selected
337 '', '', '', true) . '<br/>'
338 . choose_from_menu(array('uniform' => get_string('uniform', 'quiz'),
339 'loguniform' => get_string('loguniform', 'quiz')),
340 'calcdistribution[]',
341 $regs[1], // Selected
342 '', '', '', true);
343 } else {
344 return '';
345 }
346 }
347
348 function update_dataset_options($datasetdefs, $form) {
349 // Do we have informatin about new options???
350 if (empty($form->definition) || empty($form->calcmin)
351 || empty($form->calcmax) || empty($form->calclength)
352 || empty($form->calcdistribution)) {
353 // I gues not:
354
355 } else {
356 // Looks like we just could have some new information here
357 foreach ($form->definition as $key => $defid) {
358 if (isset($datasetdefs[$defid])
359 && is_numeric($form->calcmin[$key])
360 && is_numeric($form->calcmax[$key])
361 && is_numeric($form->calclength[$key])) {
362 switch ($form->calcdistribution[$key]) {
363 case 'uniform': case 'loguniform':
364 $datasetdefs[$defid]->options =
365 $form->calcdistribution[$key] . ':'
366 . $form->calcmin[$key] . ':'
367 . $form->calcmax[$key] . ':'
368 . $form->calclength[$key];
369 break;
370 default:
371 notify("Unexpected distribution $form->calcdistribution[$key]");
372 }
373 }
374 }
375 }
376
377 // Look for empty options, on which we set default values
378 foreach ($datasetdefs as $defid => $def) {
379 if (empty($def->options)) {
380 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1';
381 }
382 }
383 return $datasetdefs;
384 }
385
386 function generate_dataset_item($options) {
387 if (!ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$',
388 $options, $regs)) {
389 // Unknown options...
390 return false;
391 }
392 if ($regs[1] == 'uniform') {
393 $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
394 return round($nbr, $regs[4]);
395
396 } else if ($regs[1] == 'loguniform') {
397 $log0 = log(abs($regs[2])); // It would have worked the other way to
398 $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
399
400 // Reformat according to the precision $regs[4]:
401
402 // Determine the format 0.[1-9][0-9]* for the nbr...
403 $p10 = 0;
404 while ($nbr < 1) {
405 --$p10;
406 $nbr *= 10;
407 }
408 while ($nbr >= 1) {
409 ++$p10;
410 $nbr /= 10;
411 }
412 // ... and have the nbr rounded off to the correct length
413 $nbr = round($nbr, $regs[4]);
414
415 // Have the nbr written on a suitable format,
416 // Either scientific or plain numeric
417 if (-2 > $p10 || 4 < $p10) {
418 // Use scientific format:
419 $eX = 'e'.--$p10;
420 $nbr *= 10;
421 if (1 == $regs[4]) {
422 $nbr = $nbr.$eX;
423 } else {
424 // Attach additional zeros at the end of $nbr,
425 $nbr .= (1==strlen($nbr) ? '.' : '')
426 . '00000000000000000000000000000000000000000x';
427 $nbr = substr($nbr, 0, $regs[4] +1).$eX;
428 }
429 } else {
430 // Stick to plain numeric format
431 $nbr *= "1e$p10";
432 if (0.1 <= $nbr / "1e$regs[4]") {
433 $nbr = $nbr;
434 } else {
435 // Could be an idea to add some zeros here
436 $nbr .= (ereg('^[0-9]*$', $nbr) ? '.' : '')
437 . '00000000000000000000000000000000000000000x';
438 $oklen = $regs[4] + ($p10 < 1 ? 2-$p10 : 1);
439 $nbr = substr($nbr, 0, $oklen);
440 }
441 }
442
443 // The larger of the values decide the sign in case the
444 // have equal different signs (which they really must not have)
445 if ($regs[2] + $regs[3] > 0) {
446 return $nbr;
447 } else {
448 return -$nbr;
449 }
450
451 } else {
452 error("The distribution $regs[1] caused problems");
453 }
454 return '';
455 }
456
457 function comment_header($question) {
458 //$this->get_question_options($question);
459 global $SESSION;
460 $strheader = '';
461 $delimiter = '';
462 if (empty($question->id)) {
463 $answers = $SESSION->datasetdependent->questionform->answers;
464 } else {
465 $answers = $question->options->answers;
466 }
467 foreach ($answers as $answer) {
468 if (is_string($answer)) {
469 $strheader .= $delimiter.$answer;
470 } else {
471 $strheader .= $delimiter.$answer->answer;
472 }
473 $delimiter = ',';
474 }
475 return $strheader;
476 }
477
478 function comment_on_datasetitems($question, $data, $number) {
479 /// Find a default unit:
480 if (!empty($question->id) && $unit = get_record('quiz_numerical_units',
481 'question', $question->id, 'multiplier', 1.0)) {
482 $unit = $unit->unit;
483 } else {
484 $unit = '';
485 }
486
487 $answers = $question->options->answers;
488 $stranswers = get_string('answer', 'quiz');
489 $strmin = get_string('min', 'quiz');
490 $strmax = get_string('max', 'quiz');
491 $errors = '';
492 $delimiter = ': ';
493 $virtualqtype = $this->get_virtual_qtype();
494 foreach ($answers as $answer) {
495 $calculated = quiz_qtype_calculated_calculate_answer(
496 $answer->answer, $data, $answer->tolerance,
497 $answer->tolerancetype, $answer->correctanswerlength,
498 $answer->correctanswerformat, $unit);
499 $calculated->tolerance = $answer->tolerance;
500 $calculated->tolerancetype = $answer->tolerancetype;
501 $calculated->correctanswerlength = $answer->correctanswerlength;
502 $calculated->correctanswerformat = $answer->correctanswerformat;
503 $virtualqtype->get_tolerance_interval($calculated);
504 if ($calculated->min === '') {
505 // This should mean that something is wrong
506 $errors .= " -$calculated->answer";
507 $stranswers .= $delimiter;
508 } else {
509 $stranswers .= $delimiter.$calculated->answer;
510 $strmin .= $delimiter.$calculated->min;
511 $strmax .= $delimiter.$calculated->max;
512 }
513 }
514 return "$stranswers<br/>$strmin<br/>$strmax<br/>$errors";
515 }
516
517 function tolerance_types() {
518 return array('1' => get_string('relative', 'quiz'),
519 '2' => get_string('nominal', 'quiz'),
520 '3' => get_string('geometric', 'quiz'));
521 }
522
523 function dataset_options($form, $name, $renameabledatasets=false) {
524 // Takes datasets from the parent implementation but
525 // filters options that are currently not accepted by calculated
526 // It also determines a default selection...
527 list($options, $selected) = parent::dataset_options($form, $name);
528 foreach ($options as $key => $whatever) {
529 if (!ereg('^'.LITERAL.'-', $key) && $key != '0') {
530 unset($options[$key]);
531 }
532 }
533 if (!$selected) {
534 $selected = LITERAL . "-0-$name"; // Default
535 }
536 return array($options, $selected);
537 }
538
539 function construct_dataset_menus($form, $mandatorydatasets,
540 $optionaldatasets) {
541 $datasetmenus = array();
542 foreach ($mandatorydatasets as $datasetname) {
543 if (!isset($datasetmenus[$datasetname])) {
544 list($options, $selected) =
545 $this->dataset_options($form, $datasetname);
546 unset($options['0']); // Mandatory...
547 $datasetmenus[$datasetname] = choose_from_menu ($options,
548 'dataset[]', $selected, '', '', "0", true);
549 }
550 }
551 foreach ($optionaldatasets as $datasetname) {
552 if (!isset($datasetmenus[$datasetname])) {
553 list($options, $selected) =
554 $this->dataset_options($form, $datasetname);
555 $datasetmenus[$datasetname] = choose_from_menu ($options,
556 'dataset[]', $selected, '', '', "0", true);
557 }
558 }
559 return $datasetmenus;
560 }
561
562 function get_correct_responses(&$question, &$state) {
563 $virtualqtype = $this->get_virtual_qtype();
564 $unit = $virtualqtype->get_default_numerical_unit($question);
565 foreach ($question->options->answers as $answer) {
566 if (((int) $answer->fraction) === 1) {
567 $answernumerical = quiz_qtype_calculated_calculate_answer(
568 $answer->answer, $state->options->dataset, $answer->tolerance,
569 $answer->tolerancetype, $answer->correctanswerlength,
570 $answer->correctanswerformat, $unit->unit);
571 return array('' => $answernumerical->answer);
572 }
573 }
574 return null;
575 }
576
577 function substitute_variables($str, $dataset) {
578 $formula = parent::substitute_variables($str, $dataset);
579 if ($error = quiz_qtype_calculated_find_formula_errors($formula)) {
580 return $error;
581 }
582 /// Calculate the correct answer
583 if (empty($formula)) {
584 $str = '';
585 } else {
586 eval('$str = '.$formula.';');
587 }
588 return $str;
589 }
590}
591//// END OF CLASS ////
592
593//////////////////////////////////////////////////////////////////////////
594//// INITIATION - Without this line the question type is not in use... ///
595//////////////////////////////////////////////////////////////////////////
596$QUIZ_QTYPES[CALCULATED]= new quiz_calculated_qtype();
597
598function quiz_qtype_calculated_calculate_answer($formula, $individualdata,
599 $tolerance, $tolerancetype, $answerlength, $answerformat='1', $unit='') {
600/// The return value has these properties:
601/// ->answer the correct answer
602/// ->min the lower bound for an acceptable response
603/// ->max the upper bound for an accetpable response
604
605 /// Exchange formula variables with the correct values...
606 global $QUIZ_QTYPES;
607 $answer = $QUIZ_QTYPES[CALCULATED]->substitute_variables($formula, $individualdata);
608 if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */
609 /*** Adjust to the correct number of decimals ***/
610
611 $calculated->answer = round($answer, $answerlength);
612
613 if ($answerlength) {
614 /* Try to include missing zeros at the end */
615
616 if (ereg('^(.*\\.)(.*)$', $calculated->answer, $regs)) {
617 $calculated->answer = $regs[1] . substr(
618 $regs[2] . '00000000000000000000000000000000000000000x',
619 0, $answerlength)
620 . $unit;
621 } else {
622 $calculated->answer .=
623 substr('.00000000000000000000000000000000000000000x',
624 0, $answerlength + 1) . $unit;
625 }
626 } else {
627 /* Attach unit */
628 $calculated->answer .= $unit;
629 }
630
631 } else if ($answer) { // Significant figures does only apply if the result is non-zero
632
633 // Convert to positive answer...
634 if ($answer < 0) {
635 $answer = -$answer;
636 $sign = '-';
637 } else {
638 $sign = '';
639 }
640
641 // Determine the format 0.[1-9][0-9]* for the answer...
642 $p10 = 0;
643 while ($answer < 1) {
644 --$p10;
645 $answer *= 10;
646 }
647 while ($answer >= 1) {
648 ++$p10;
649 $answer /= 10;
650 }
651 // ... and have the answer rounded of to the correct length
652 $answer = round($answer, $answerlength);
653
654 // Have the answer written on a suitable format,
655 // Either scientific or plain numeric
656 if (-2 > $p10 || 4 < $p10) {
657 // Use scientific format:
658 $eX = 'e'.--$p10;
659 $answer *= 10;
660 if (1 == $answerlength) {
661 $calculated->answer = $sign.$answer.$eX.$unit;
662 } else {
663 // Attach additional zeros at the end of $answer,
664 $answer .= (1==strlen($answer) ? '.' : '')
665 . '00000000000000000000000000000000000000000x';
666 $calculated->answer = $sign
667 .substr($answer, 0, $answerlength +1).$eX.$unit;
668 }
669 } else {
670 // Stick to plain numeric format
671 $answer *= "1e$p10";
672 if (0.1 <= $answer / "1e$answerlength") {
673 $calculated->answer = $sign.$answer.$unit;
674 } else {
675 // Could be an idea to add some zeros here
676 $answer .= (ereg('^[0-9]*$', $answer) ? '.' : '')
677 . '00000000000000000000000000000000000000000x';
678 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
679 $calculated->answer = $sign.substr($answer, 0, $oklen).$unit;
680 }
681 }
682
683 } else {
684 $calculated->answer = 0.0;
685 }
686
687 /// Return the result
688 return $calculated;
689}
690
691
692function quiz_qtype_calculated_find_formula_errors($formula) {
693/// Validates the formula submitted from the question edit page.
694/// Returns false if everything is alright.
695/// Otherwise it constructs an error message
696
697 // Strip away dataset names
698 while (ereg('\\{[[:alpha:]][^>} <{"\']*\\}', $formula, $regs)) {
699 $formula = str_replace($regs[0], '1', $formula);
700 }
701
702 // Strip away empty space and lowercase it
703 $formula = strtolower(str_replace(' ', '', $formula));
704
705 $safeoperatorchar = '-+/*%>:^~<?=&|!'; /* */
706 $operatorornumber = "[$safeoperatorchar.0-9eE]";
707
708
709 while (ereg("(^|[$safeoperatorchar,(])([a-z0-9_]*)\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)",
710 $formula, $regs)) {
711
712 switch ($regs[2]) {
713 // Simple parenthesis
714 case '':
715 if ($regs[4] || empty($regs[3])) {
716 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
717 }
718 break;
719
720 // Zero argument functions
721 case 'pi':
722 if ($regs[3]) {
723 return get_string('functiontakesnoargs', 'quiz', $regs[2]);
724 }
725 break;
726
727 // Single argument functions (the most common case)
728 case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
729 case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
730 case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
731 case 'exp': case 'expm1': case 'floor': case 'is_finite':
732 case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
733 case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
734 case 'tan': case 'tanh':
735 if ($regs[4] || empty($regs[3])) {
736 return get_string('functiontakesonearg','quiz',$regs[2]);
737 }
738 break;
739
740 // Functions that take one or two arguments
741 case 'log': case 'round':
742 if ($regs[5] || empty($regs[3])) {
743 return get_string('functiontakesoneortwoargs','quiz',$regs[2]);
744 }
745 break;
746
747 // Functions that must have two arguments
748 case 'atan2': case 'fmod': case 'pow':
749 if ($regs[5] || empty($regs[4])) {
750 return get_string('functiontakestwoargs', 'quiz', $regs[2]);
751 }
752 break;
753
754 // Functions that take two or more arguments
755 case 'min': case 'max':
756 if (empty($regs[4])) {
757 return get_string('functiontakesatleasttwo','quiz',$regs[2]);
758 }
759 break;
760
761 default:
762 return get_string('unsupportedformulafunction','quiz',$regs[2]);
763 }
764
765 // Exchange the function call with '1' and then chack for
766 // another function call...
767 if ($regs[1]) {
768 // The function call is proceeded by an operator
769 $formula = str_replace($regs[0], $regs[1] . '1', $formula);
770 } else {
771 // The function call starts the formula
772 $formula = ereg_replace("^$regs[2]\\([^)]*\\)", '1', $formula);
773 }
774 }
775
776 if (ereg("[^$safeoperatorchar.0-9eE]+", $formula, $regs)) {
777 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
778 } else {
779 // Formula just might be valid
780 return false;
781 }
782}
783
784function dump($obj) {
785 echo "<pre>\n";
786 var_dump($obj);
787 echo "</pre><br />\n";
788}
789
790?>