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