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