Merge commit 'catalyst/MOODLE_19_STABLE' into mdl19-linuxchix
[moodle-linuxchix.git] / question / type / calculated / questiontype.php
blob1385d2fd1640ba3ab2053e63e91967fded7e1c04
1 <?php // $Id$
3 /////////////////
4 // CALCULATED ///
5 /////////////////
7 /// QUESTION TYPE CLASS //////////////////
9 require_once("$CFG->dirroot/question/type/datasetdependent/abstractqtype.php");
12 class question_calculated_qtype extends question_dataset_dependent_questiontype {
14 // Used by the function custom_generator_tools:
15 var $calcgenerateidhasbeenadded = false;
17 function name() {
18 return 'calculated';
21 function get_question_options(&$question) {
22 // First get the datasets and default options
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 for calculated question ' . $question->id . '!');
32 return false;
36 if(false === parent::get_question_options($question)) {
37 return false;
40 if (!$options = get_records('question_calculated', 'question', $question->id)) {
41 notify("No options were found for calculated question
42 #{$question->id}! Proceeding with defaults.");
43 // $options = new Array();
44 $options= new stdClass;
45 $options->tolerance = 0.01;
46 $options->tolerancetype = 1; // relative
47 $options->correctanswerlength = 2;
48 $options->correctanswerformat = 1; // decimals
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.
54 foreach ($question->options->answers as $key => $answer) {
55 $answer = &$question->options->answers[$key]; // for PHP 4.x
56 $answer->calcid = $options->id;
57 $answer->tolerance = $options->tolerance;
58 $answer->tolerancetype = $options->tolerancetype;
59 $answer->correctanswerlength = $options->correctanswerlength;
60 $answer->correctanswerformat = $options->correctanswerformat;
61 }*/
63 $virtualqtype = $this->get_virtual_qtype();
64 $virtualqtype->get_numerical_units($question);
66 if( isset($question->export_process)&&$question->export_process){
67 $question->options->datasets = $this->get_datasets_for_export($question);
69 return true;
72 function get_datasets_for_export(&$question){
73 $datasetdefs = array();
74 if (!empty($question->id)) {
75 global $CFG;
76 $sql = "SELECT i.*
77 FROM {$CFG->prefix}question_datasets d,
78 {$CFG->prefix}question_dataset_definitions i
79 WHERE d.question = '$question->id'
80 AND d.datasetdefinition = i.id
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';
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;
110 $def->number_of_items=$n ;
113 $datasetdefs["1-$r->category-$r->name"] = $def;
117 return $datasetdefs ;
120 function save_question_options($question) {
121 //$options = $question->subtypeoptions;
122 // Get old answers:
123 global $CFG;
125 // Get old versions of the objects
126 if (!$oldanswers = get_records('question_answers', 'question', $question->id, 'id ASC')) {
127 $oldanswers = array();
130 if (!$oldoptions = get_records('question_calculated', 'question', $question->id, 'answer ASC')) {
131 $oldoptions = array();
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;
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]);
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;
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;
164 // Set up the options object
165 if (!$options = array_shift($oldoptions)) {
166 $options = new stdClass;
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]);
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;
181 } else { // new options
182 if (! insert_record('question_calculated', $options)) {
183 $result->error = "Could not insert question calculated options!";
184 return $result;
189 // delete old answer records
190 if (!empty($oldanswers)) {
191 foreach($oldanswers as $oa) {
192 delete_records('question_answers', 'id', $oa->id);
196 // delete old answer records
197 if (!empty($oldoptions)) {
198 foreach($oldoptions as $oo) {
199 delete_records('question_calculated', 'id', $oo->id);
204 if( isset($question->import_process)&&$question->import_process){
205 $this->import_datasets($question);
207 // Report any problems.
208 if (!empty($result->notice)) {
209 return $result;
211 return true;
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' ;
242 }else { // no so create one
243 $datasetdef->category =$question->category ;
244 $todo='create' ;
247 if ( $todo=='create'){
248 if (!$datasetdef->id = insert_record(
249 'question_dataset_definitions', $datasetdef)) {
250 error("Unable to create dataset $defid");
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)) {
259 error("Unable to create relation to dataset $dataset->name $todo");
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 ;
267 if (!insert_record('question_dataset_items', $datasetitem)) {
268 error("Unable to insert dataset item $item->itemnumber with $item->value for $datasetdef->name");
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]);
280 $a->fraction = $form->fraction[$key];//new
281 $a->tolerance = $form->tolerance[$key];
282 $a->tolerancetype = $form->tolerancetype[$key];
283 $a->correctanswerlength = $form->correctanswerlength[$key];
284 $a->correctanswerformat = $form->correctanswerformat[$key];
285 $question->options->answers[] = clone($a);
288 return $question;
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');
298 if (empty($form->questiontext)) {
299 $calculatedmessages[] = get_string('missingquestiontext', 'quiz');
301 // Verify formulas
302 foreach ($form->answers as $key => $answer) {
303 if ('' === trim($answer)) {
304 $calculatedmessages[] =
305 get_string('missingformula', 'quiz');
307 if ($formulaerrors =
308 qtype_calculated_find_formula_errors($answer)) {
309 $calculatedmessages[] = $formulaerrors;
311 if (! isset($form->tolerance[$key])) {
312 $form->tolerance[$key] = 0.0;
314 if (! is_numeric($form->tolerance[$key])) {
315 $calculatedmessages[] =
316 get_string('tolerancemustbenumeric', 'quiz');
320 if (!empty($calculatedmessages)) {
321 $errorstring = "The following errors were found:<br />";
322 foreach ($calculatedmessages as $msg) {
323 $errorstring .= $msg . '<br />';
325 error($errorstring);
328 break;
329 default:
330 return parent::validate_form($form);
331 break;
333 return true;
337 * Deletes question from the question-type specific tables
339 * @return boolean Success/Failure
340 * @param object $question The question being deleted
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) {
347 delete_records('question_dataset_definitions', 'id', $dataset->datasetdefinition);
348 delete_records('question_dataset_items', 'definition', $dataset->datasetdefinition);
351 delete_records("question_datasets", "question", $questionid);
352 return true;
355 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
356 // Substitute variables in questiontext before giving the data to the
357 // virtual type for printing
358 $virtualqtype = $this->get_virtual_qtype();
359 if($unit = $virtualqtype->get_default_numerical_unit($question)){
360 $unit = $unit->unit;
361 } else {
362 $unit = '';
364 // We modify the question to look like a numerical question
365 $numericalquestion = fullclone($question);
366 foreach ($numericalquestion->options->answers as $key => $answer) {
367 $answer = fullclone($numericalquestion->options->answers[$key]);
368 $numericalquestion->options->answers[$key]->answer = $this->substitute_variables($answer->answer,
369 $state->options->dataset);
371 $numericalquestion->questiontext = parent::substitute_variables(
372 $numericalquestion->questiontext, $state->options->dataset);
373 //evaluate the equations i.e {=5+4)
374 $qtext = "";
375 $qtextremaining = $numericalquestion->questiontext ;
376 while (ereg('\{=([^[:space:]}]*)}', $qtextremaining, $regs1)) {
377 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
378 $qtext =$qtext.$qtextsplits[0];
379 $qtextremaining = $qtextsplits[1];
380 if (empty($regs1[1])) {
381 $str = '';
382 } else {
383 if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
384 $str=$formulaerrors ;
385 }else {
386 eval('$str = '.$regs1[1].';');
389 $qtext = $qtext.$str ;
391 $numericalquestion->questiontext = $qtext.$qtextremaining ; // end replace equations
392 $virtualqtype->print_question_formulation_and_controls($numericalquestion, $state, $cmoptions, $options);
394 function grade_responses(&$question, &$state, $cmoptions) {
395 // Forward the grading to the virtual qtype
396 // We modify the question to look like a numerical question
397 $numericalquestion = fullclone($question);
398 foreach ($numericalquestion->options->answers as $key => $answer) {
399 $answer = $numericalquestion->options->answers[$key]->answer; // for PHP 4.x
400 $numericalquestion->options->answers[$key]->answer = $this->substitute_variables($answer,
401 $state->options->dataset);
403 $virtualqtype = $this->get_virtual_qtype();
404 return $virtualqtype->grade_responses($numericalquestion, $state, $cmoptions) ;
407 function response_summary($question, $state, $length=80) {
408 // The actual response is the bit after the hyphen
409 return substr($state->answer, strpos($state->answer, '-')+1, $length);
412 // ULPGC ecastro
413 function check_response(&$question, &$state) {
414 // Forward the checking to the virtual qtype
415 // We modify the question to look like a numerical question
416 $numericalquestion = clone($question);
417 $numericalquestion->options = clone($question->options);
418 foreach ($question->options->answers as $key => $answer) {
419 $numericalquestion->options->answers[$key] = clone($answer);
421 foreach ($numericalquestion->options->answers as $key => $answer) {
422 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
423 $answer->answer = $this->substitute_variables($answer->answer,
424 $state->options->dataset);
426 $virtualqtype = $this->get_virtual_qtype();
427 return $virtualqtype->check_response($numericalquestion, $state) ;
430 // ULPGC ecastro
431 function get_actual_response(&$question, &$state) {
432 // Substitute variables in questiontext before giving the data to the
433 // virtual type
434 $virtualqtype = $this->get_virtual_qtype();
435 $unit = $virtualqtype->get_default_numerical_unit($question);
437 // We modify the question to look like a numerical question
438 $numericalquestion = clone($question);
439 $numericalquestion->options = clone($question->options);
440 foreach ($question->options->answers as $key => $answer) {
441 $numericalquestion->options->answers[$key] = clone($answer);
443 foreach ($numericalquestion->options->answers as $key => $answer) {
444 $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
445 $answer->answer = $this->substitute_variables($answer->answer,
446 $state->options->dataset);
447 // apply_unit
449 $numericalquestion->questiontext = $this->substitute_variables(
450 $numericalquestion->questiontext, $state->options->dataset);
451 $responses = $virtualqtype->get_all_responses($numericalquestion, $state);
452 $response = reset($responses->responses);
453 $correct = $response->answer.' : ';
455 $responses = $virtualqtype->get_actual_response($numericalquestion, $state);
457 foreach ($responses as $key=>$response){
458 $responses[$key] = $correct.$response;
461 return $responses;
464 function create_virtual_qtype() {
465 global $CFG;
466 require_once("$CFG->dirroot/question/type/numerical/questiontype.php");
467 return new question_numerical_qtype();
470 function supports_dataset_item_generation() {
471 // Calcualted support generation of randomly distributed number data
472 return true;
474 function custom_generator_tools_part(&$mform, $idx, $j){
476 $minmaxgrp = array();
477 $minmaxgrp[] =& $mform->createElement('text', "calcmin[$idx]", get_string('calcmin', 'qtype_datasetdependent'));
478 $minmaxgrp[] =& $mform->createElement('text', "calcmax[$idx]", get_string('calcmax', 'qtype_datasetdependent'));
479 $mform->addGroup($minmaxgrp, 'minmaxgrp', get_string('minmax', 'qtype_datasetdependent'), ' - ', false);
480 $mform->setType("calcmin[$idx]", PARAM_NUMBER);
481 $mform->setType("calcmax[$idx]", PARAM_NUMBER);
483 $precisionoptions = range(0, 10);
484 $mform->addElement('select', "calclength[$idx]", get_string('calclength', 'qtype_datasetdependent'), $precisionoptions);
486 $distriboptions = array('uniform' => get_string('uniform', 'qtype_datasetdependent'), 'loguniform' => get_string('loguniform', 'qtype_datasetdependent'));
487 $mform->addElement('select', "calcdistribution[$idx]", get_string('calcdistribution', 'qtype_datasetdependent'), $distriboptions);
492 function custom_generator_set_data($datasetdefs, $formdata){
493 $idx = 1;
494 foreach ($datasetdefs as $datasetdef){
495 if (ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$', $datasetdef->options, $regs)) {
496 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
497 $formdata["calcdistribution[$idx]"] = $regs[1];
498 $formdata["calcmin[$idx]"] = $regs[2];
499 $formdata["calcmax[$idx]"] = $regs[3];
500 $formdata["calclength[$idx]"] = $regs[4];
502 $idx++;
504 return $formdata;
507 function custom_generator_tools($datasetdef) {
508 if (ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$',
509 $datasetdef->options, $regs)) {
510 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
511 for ($i = 0 ; $i<10 ; ++$i) {
512 $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
513 ? 'decimals'
514 : 'significantfigures'), 'quiz', $i);
516 return '<input type="submit" onclick="'
517 . "getElementById('addform').regenerateddefid.value='$defid'; return true;"
518 .'" value="'. get_string('generatevalue', 'quiz') . '"/><br/>'
519 . '<input type="text" size="3" name="calcmin[]" '
520 . " value=\"$regs[2]\"/> &amp; <input name=\"calcmax[]\" "
521 . ' type="text" size="3" value="' . $regs[3] .'"/> '
522 . choose_from_menu($lengthoptions, 'calclength[]',
523 $regs[4], // Selected
524 '', '', '', true) . '<br/>'
525 . choose_from_menu(array('uniform' => get_string('uniform', 'quiz'),
526 'loguniform' => get_string('loguniform', 'quiz')),
527 'calcdistribution[]',
528 $regs[1], // Selected
529 '', '', '', true);
530 } else {
531 return '';
536 function update_dataset_options($datasetdefs, $form) {
537 // Do we have informatin about new options???
538 if (empty($form->definition) || empty($form->calcmin)
539 || empty($form->calcmax) || empty($form->calclength)
540 || empty($form->calcdistribution)) {
541 // I guess not
543 } else {
544 // Looks like we just could have some new information here
545 $uniquedefs = array_values(array_unique($form->definition));
546 foreach ($uniquedefs as $key => $defid) {
547 if (isset($datasetdefs[$defid])
548 && is_numeric($form->calcmin[$key+1])
549 && is_numeric($form->calcmax[$key+1])
550 && is_numeric($form->calclength[$key+1])) {
551 switch ($form->calcdistribution[$key+1]) {
552 case 'uniform': case 'loguniform':
553 $datasetdefs[$defid]->options =
554 $form->calcdistribution[$key+1] . ':'
555 . $form->calcmin[$key+1] . ':'
556 . $form->calcmax[$key+1] . ':'
557 . $form->calclength[$key+1];
558 break;
559 default:
560 notify("Unexpected distribution ".$form->calcdistribution[$key+1]);
566 // Look for empty options, on which we set default values
567 foreach ($datasetdefs as $defid => $def) {
568 if (empty($def->options)) {
569 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1';
572 return $datasetdefs;
575 function save_dataset_items($question, $fromform){
576 global $CFG ;
577 // max datasets = 100 items
578 $max100 = 100 ;
579 if(isset($fromform->nextpageparam["forceregeneration"])) {
580 $regenerate = $fromform->nextpageparam["forceregeneration"];
581 }else{
582 $regenerate = 0 ;
584 if (empty($question->options)) {
585 $this->get_question_options($question);
587 //get the old datasets for this question
588 $datasetdefs = $this->get_dataset_definitions($question->id, array());
590 // Handle generator options...
591 $olddatasetdefs = fullclone($datasetdefs);
592 $datasetdefs = $this->update_dataset_options($datasetdefs, $fromform);
593 $maxnumber = -1;
594 foreach ($datasetdefs as $defid => $datasetdef) {
595 if (isset($datasetdef->id)
596 && $datasetdef->options != $olddatasetdefs[$defid]->options) {
597 // Save the new value for options
598 update_record('question_dataset_definitions', $datasetdef);
601 // Get maxnumber
602 if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) {
603 $maxnumber = $datasetdef->itemcount;
606 // Handle adding and removing of dataset items
607 $i = 1;
608 ksort($fromform->definition);
609 foreach ($fromform->definition as $key => $defid) {
610 //if the delete button has not been pressed then skip the datasetitems
611 //in the 'add item' part of the form.
612 if ((!isset($fromform->addbutton)) && ($i > (count($datasetdefs)*$maxnumber))) {
613 break;
615 $addeditem = new stdClass();
616 $addeditem->definition = $datasetdefs[$defid]->id;
617 $addeditem->value = $fromform->number[$i];
618 $addeditem->itemnumber = ceil($i / count($datasetdefs));
620 if ($fromform->itemid[$i]) {
621 // Reuse any previously used record
622 $addeditem->id = $fromform->itemid[$i];
623 if (!update_record('question_dataset_items', $addeditem)) {
624 error("Error: Unable to update dataset item");
626 } else {
627 if (!insert_record('question_dataset_items', $addeditem)) {
628 error("Error: Unable to insert dataset item");
632 $i++;
634 if ($maxnumber < $addeditem->itemnumber){
635 $maxnumber = $addeditem->itemnumber;
636 foreach ($datasetdefs as $key => $newdef) {
637 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
638 $newdef->itemcount = $maxnumber;
639 // Save the new value for options
640 update_record('question_dataset_definitions', $newdef);
644 // adding supplementary items
645 $numbertoadd =0;
646 if (isset($fromform->addbutton) && $fromform->selectadd > 1 && $maxnumber < $max100 ) {
647 $numbertoadd =$fromform->selectadd-1 ;
648 if ( $max100 - $maxnumber < $numbertoadd ) {
649 $numbertoadd = $max100 - $maxnumber ;
651 //add the other items.
652 // Generate a new dataset item (or reuse an old one)
653 foreach ($datasetdefs as $defid => $datasetdef) {
654 if (isset($datasetdef->id)) {
655 $datasetdefs[$defid]->items = get_records_sql( // Use number as key!!
656 " SELECT itemnumber, definition, id, value
657 FROM {$CFG->prefix}question_dataset_items
658 WHERE definition = $datasetdef->id ORDER BY itemnumber");
660 // echo "<pre>"; print_r($datasetdefs[$defid]->items);
661 for ($numberadded =$maxnumber+1 ; $numberadded <= $maxnumber+$numbertoadd ; $numberadded++){
662 if (isset($datasetdefs[$defid]->items[$numberadded]) && ! $regenerate ){
663 // echo "<p>Reuse an previously used record".$numberadded."id".$datasetdef->id."</p>";
664 } else {
665 $datasetitem = new stdClass;
666 $datasetitem->definition = $datasetdef->id ;
667 $datasetitem->itemnumber = $numberadded;
668 if ($this->supports_dataset_item_generation()) {
669 $datasetitem->value = $this->generate_dataset_item($datasetdef->options);
670 } else {
671 $datasetitem->value = '';
673 if (!insert_record('question_dataset_items', $datasetitem)) {
674 error("Error: Unable to insert new dataset item");
677 }//for number added
678 }// datasetsdefs end
679 $maxnumber += $numbertoadd ;
680 foreach ($datasetdefs as $key => $newdef) {
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);
689 if (isset($fromform->deletebutton)) {
690 if(isset($fromform->selectdelete)) $newmaxnumber = $maxnumber-$fromform->selectdelete ;
691 else $newmaxnumber = $maxnumber-1 ;
692 if ($newmaxnumber < 0 ) $newmaxnumber = 0 ;
693 foreach ($datasetdefs as $datasetdef) {
694 if ($datasetdef->itemcount == $maxnumber) {
695 $datasetdef->itemcount= $newmaxnumber ;
696 if (!update_record('question_dataset_definitions',
697 $datasetdef)) {
698 error("Error: Unable to update itemcount");
704 function generate_dataset_item($options) {
706 if (!ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$',
707 $options, $regs)) {
708 // Unknown options...
709 return false;
711 if ($regs[1] == 'uniform') {
712 $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
713 return sprintf("%.".$regs[4]."f",$nbr);
715 } else if ($regs[1] == 'loguniform') {
716 $log0 = log(abs($regs[2])); // It would have worked the other way to
717 $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
718 return sprintf("%.".$regs[4]."f",$nbr);
720 } else {
721 error("The distribution $regs[1] caused problems");
723 return '';
726 function comment_header($question) {
727 //$this->get_question_options($question);
728 $strheader = '';
729 $delimiter = '';
731 $answers = $question->options->answers;
733 foreach ($answers as $answer) {
734 if (is_string($answer)) {
735 $strheader .= $delimiter.$answer;
736 } else {
737 $strheader .= $delimiter.$answer->answer;
739 $delimiter = '<br/><br/><br/>';
741 return $strheader;
744 function comment_on_datasetitems($question, $data, $number) {
745 /// Find a default unit:
746 if (!empty($question->id) && $unit = get_record('question_numerical_units',
747 'question', $question->id, 'multiplier', 1.0)) {
748 $unit = $unit->unit;
749 } else {
750 $unit = '';
753 $answers = fullclone($question->options->answers);
754 $stranswers = '';
755 $strmin = get_string('min', 'quiz');
756 $strmax = get_string('max', 'quiz');
757 $errors = '';
758 $delimiter = ': ';
759 $virtualqtype = $this->get_virtual_qtype();
760 foreach ($answers as $answer) {
761 $formula = $answer->answer;
762 foreach ($data as $name => $value) {
763 $formula = str_replace('{'.$name.'}', $value, $formula);
765 $formattedanswer = qtype_calculated_calculate_answer(
766 $answer->answer, $data, $answer->tolerance,
767 $answer->tolerancetype, $answer->correctanswerlength,
768 $answer->correctanswerformat, $unit);
769 eval('$answer->answer = '.$formula.';') ;
770 $virtualqtype->get_tolerance_interval($answer);
771 if ($answer->min === '') {
772 // This should mean that something is wrong
773 $stranswers .= " -$formattedanswer->answer".'<br/><br/>';
774 } else {
775 $stranswers .= $formula.' = '.$formattedanswer->answer.'<br/>' ;
776 $stranswers .= $strmin. $delimiter.$answer->min.'---';
777 $stranswers .= $strmax.$delimiter.$answer->max;
778 $stranswers .='<br/>';
779 $correcttrue->correct = $formattedanswer->answer ;
780 $correcttrue->true = $answer->answer ;
781 if ($formattedanswer->answer < $answer->min || $formattedanswer->answer > $answer->max){
782 $stranswers .=get_string('trueansweroutsidelimits','qtype_calculated',$correcttrue);//<span class="error">ERROR True answer '..' outside limits</span>';
783 } else {
784 $stranswers .=get_string('trueanswerinsidelimits','qtype_calculated',$correcttrue);//' True answer :'.$calculated->trueanswer.' inside limits';
786 $stranswers .='<br/>';
789 return "$stranswers";
792 function tolerance_types() {
793 return array('1' => get_string('relative', 'quiz'),
794 '2' => get_string('nominal', 'quiz'),
795 '3' => get_string('geometric', 'quiz'));
798 function dataset_options($form, $name, $mandatory=true,$renameabledatasets=false) {
799 // Takes datasets from the parent implementation but
800 // filters options that are currently not accepted by calculated
801 // It also determines a default selection...
802 //$renameabledatasets not implemented anmywhere
803 list($options, $selected) = parent::dataset_options($form, $name,'','qtype_calculated');
804 // list($options, $selected) = $this->dataset_optionsa($form, $name);
806 foreach ($options as $key => $whatever) {
807 if (!ereg('^'.LITERAL.'-', $key) && $key != '0') {
808 unset($options[$key]);
811 if (!$selected) {
812 if ($mandatory){
813 $selected = LITERAL . "-0-$name"; // Default
814 }else {
815 $selected = "0"; // Default
818 return array($options, $selected);
821 function construct_dataset_menus($form, $mandatorydatasets,
822 $optionaldatasets) {
823 $datasetmenus = array();
824 foreach ($mandatorydatasets as $datasetname) {
825 if (!isset($datasetmenus[$datasetname])) {
826 list($options, $selected) =
827 $this->dataset_options($form, $datasetname);
828 unset($options['0']); // Mandatory...
829 $datasetmenus[$datasetname] = choose_from_menu ($options,
830 'dataset[]', $selected, '', '', "0", true);
833 foreach ($optionaldatasets as $datasetname) {
834 if (!isset($datasetmenus[$datasetname])) {
835 list($options, $selected) =
836 $this->dataset_options($form, $datasetname);
837 $datasetmenus[$datasetname] = choose_from_menu ($options,
838 'dataset[]', $selected, '', '', "0", true);
841 return $datasetmenus;
844 function print_question_grading_details(&$question, &$state, &$cmoptions, &$options) {
845 $virtualqtype = $this->get_virtual_qtype();
846 $virtualqtype->print_question_grading_details($question, $state, $cmoptions, $options) ;
849 function get_correct_responses(&$question, &$state) {
850 $virtualqtype = $this->get_virtual_qtype();
851 if($unit = $virtualqtype->get_default_numerical_unit($question)){
852 $unit = $unit->unit;
853 } else {
854 $unit = '';
856 foreach ($question->options->answers as $answer) {
857 if (((int) $answer->fraction) === 1) {
858 $answernumerical = qtype_calculated_calculate_answer(
859 $answer->answer, $state->options->dataset, $answer->tolerance,
860 $answer->tolerancetype, $answer->correctanswerlength,
861 $answer->correctanswerformat, $unit);
862 return array('' => $answernumerical->answer);
865 return null;
868 function substitute_variables($str, $dataset) {
869 $formula = parent::substitute_variables($str, $dataset);
870 if ($error = qtype_calculated_find_formula_errors($formula)) {
871 return $error;
873 /// Calculate the correct answer
874 if (empty($formula)) {
875 $str = '';
876 } else {
877 eval('$str = '.$formula.';');
879 return $str;
883 * This function retrieve the item count of the available category shareable
884 * wild cards that is added as a comment displayed when a wild card with
885 * the same name is displayed in datasetdefinitions_form.php
887 function get_dataset_definitions_category($form) {
888 global $CFG;
889 $datasetdefs = array();
890 $lnamemax = 30;
891 if (!empty($form->category)) {
892 $sql = "SELECT i.*,d.*
893 FROM {$CFG->prefix}question_datasets d,
894 {$CFG->prefix}question_dataset_definitions i
895 WHERE i.id = d.datasetdefinition
896 AND i.category = '$form->category'
899 if ($records = get_records_sql($sql)) {
900 foreach ($records as $r) {
901 if ( !isset ($datasetdefs["$r->name"])) $datasetdefs["$r->name"] = $r->itemcount;
905 return $datasetdefs ;
909 * This function build a table showing the available category shareable
910 * wild cards, their name, their definition (Min, Max, Decimal) , the item count
911 * and the name of the question where they are used.
912 * This table is intended to be add before the question text to help the user use
913 * these wild cards
916 function print_dataset_definitions_category($form) {
917 global $CFG;
918 $datasetdefs = array();
919 $lnamemax = 22;
920 $namestr =get_string('name', 'quiz');
921 $minstr=get_string('min', 'quiz');
922 $maxstr=get_string('max', 'quiz');
923 $rangeofvaluestr=get_string('minmax','qtype_datasetdependent');
924 $questionusingstr = get_string('usedinquestion','qtype_calculated');
925 $itemscountstr = get_string('itemscount','qtype_datasetdependent');
926 $text ='';
927 if (!empty($form->category)) {
928 list($category) = explode(',', $form->category);
929 $sql = "SELECT i.*,d.*
930 FROM {$CFG->prefix}question_datasets d,
931 {$CFG->prefix}question_dataset_definitions i
932 WHERE i.id = d.datasetdefinition
933 AND i.category = $category;
935 if ($records = get_records_sql($sql)) {
936 foreach ($records as $r) {
937 $sql1 = "SELECT q.*
938 FROM {$CFG->prefix}question q
939 WHERE q.id = $r->question
941 if ( !isset ($datasetdefs["$r->type-$r->category-$r->name"])){
942 $datasetdefs["$r->type-$r->category-$r->name"]= $r;
944 if ($questionb = get_records_sql($sql1)) {
945 $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question]->name =$questionb[$r->question]->name ;
950 if (!empty ($datasetdefs)){
952 $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>";
953 foreach ($datasetdefs as $datasetdef){
954 list($distribution, $min, $max,$dec) = explode(':', $datasetdef->options, 4);
955 $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\">";
956 foreach ($datasetdef->questions as $qu) {
957 //limit the name length displayed
958 if (!empty($qu->name)) {
959 $qu->name = (strlen($qu->name) > $lnamemax) ?
960 substr($qu->name, 0, $lnamemax).'...' : $qu->name;
961 } else {
962 $qu->name = '';
964 $text .=" &nbsp;&nbsp; $qu->name <br/>";
966 $text .="</td></tr>";
968 $text .="</table>";
969 }else{
970 $text .=get_string('nosharedwildcard', 'qtype_calculated');
972 return $text ;
976 /// BACKUP FUNCTIONS ////////////////////////////
979 * Backup the data in the question
981 * This is used in question/backuplib.php
983 function backup($bf,$preferences,$question,$level=6) {
985 $status = true;
987 $calculateds = get_records("question_calculated","question",$question,"id");
988 //If there are calculated-s
989 if ($calculateds) {
990 //Iterate over each calculateds
991 foreach ($calculateds as $calculated) {
992 $status = $status &&fwrite ($bf,start_tag("CALCULATED",$level,true));
993 //Print calculated contents
994 fwrite ($bf,full_tag("ANSWER",$level+1,false,$calculated->answer));
995 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$calculated->tolerance));
996 fwrite ($bf,full_tag("TOLERANCETYPE",$level+1,false,$calculated->tolerancetype));
997 fwrite ($bf,full_tag("CORRECTANSWERLENGTH",$level+1,false,$calculated->correctanswerlength));
998 fwrite ($bf,full_tag("CORRECTANSWERFORMAT",$level+1,false,$calculated->correctanswerformat));
999 //Now backup numerical_units
1000 $status = question_backup_numerical_units($bf,$preferences,$question,7);
1001 //Now backup required dataset definitions and items...
1002 $status = question_backup_datasets($bf,$preferences,$question,7);
1003 //End calculated data
1004 $status = $status &&fwrite ($bf,end_tag("CALCULATED",$level,true));
1006 //Now print question_answers
1007 $status = question_backup_answers($bf,$preferences,$question);
1009 return $status;
1012 /// RESTORE FUNCTIONS /////////////////
1015 * Restores the data in the question
1017 * This is used in question/restorelib.php
1019 function restore($old_question_id,$new_question_id,$info,$restore) {
1021 $status = true;
1023 //Get the calculated-s array
1024 $calculateds = $info['#']['CALCULATED'];
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
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']['#']);
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;
1047 //The structure is equal to the db, so insert the question_calculated
1048 $newid = insert_record ("question_calculated",$calculated);
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 />";
1058 backup_flush(300);
1061 //Now restore numerical_units
1062 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$cal_info,$restore);
1064 //Now restore dataset_definitions
1065 if ($status && $newid) {
1066 $status = question_restore_dataset_definitions ($old_question_id,$new_question_id,$cal_info,$restore);
1069 if (!$newid) {
1070 $status = false;
1074 return $status;
1078 * Runs all the code required to set up and save an essay question for testing purposes.
1079 * Alternate DB table prefix may be used to facilitate data deletion.
1081 function generate_test($name, $courseid = null) {
1082 list($form, $question) = parent::generate_test($name, $courseid);
1083 $form->feedback = 1;
1084 $form->multiplier = array(1, 1);
1085 $form->shuffleanswers = 1;
1086 $form->noanswers = 1;
1087 $form->qtype ='calculated';
1088 $question->qtype ='calculated';
1089 $form->answers = array('{a} + {b}');
1090 $form->fraction = array(1);
1091 $form->tolerance = array(0.01);
1092 $form->tolerancetype = array(1);
1093 $form->correctanswerlength = array(2);
1094 $form->correctanswerformat = array(1);
1095 $form->questiontext = "What is {a} + {b}?";
1097 if ($courseid) {
1098 $course = get_record('course', 'id', $courseid);
1101 $new_question = $this->save_question($question, $form, $course);
1103 $dataset_form = new stdClass();
1104 $dataset_form->nextpageparam["forceregeneration"]= 1;
1105 $dataset_form->calcmin = array(1 => 1.0, 2 => 1.0);
1106 $dataset_form->calcmax = array(1 => 10.0, 2 => 10.0);
1107 $dataset_form->calclength = array(1 => 1, 2 => 1);
1108 $dataset_form->number = array(1 => 5.4 , 2 => 4.9);
1109 $dataset_form->itemid = array(1 => '' , 2 => '');
1110 $dataset_form->calcdistribution = array(1 => 'uniform', 2 => 'uniform');
1111 $dataset_form->definition = array(1 => "1-0-a",
1112 2 => "1-0-b");
1113 $dataset_form->nextpageparam = array('forceregeneration' => false);
1114 $dataset_form->addbutton = 1;
1115 $dataset_form->selectadd = 1;
1116 $dataset_form->courseid = $courseid;
1117 $dataset_form->cmid = 0;
1118 $dataset_form->id = $new_question->id;
1119 $this->save_dataset_items($new_question, $dataset_form);
1121 return $new_question;
1124 //// END OF CLASS ////
1126 //////////////////////////////////////////////////////////////////////////
1127 //// INITIATION - Without this line the question type is not in use... ///
1128 //////////////////////////////////////////////////////////////////////////
1129 question_register_questiontype(new question_calculated_qtype());
1131 function qtype_calculated_calculate_answer($formula, $individualdata,
1132 $tolerance, $tolerancetype, $answerlength, $answerformat='1', $unit='') {
1133 /// The return value has these properties:
1134 /// ->answer the correct answer
1135 /// ->min the lower bound for an acceptable response
1136 /// ->max the upper bound for an accetpable response
1138 /// Exchange formula variables with the correct values...
1139 global $QTYPES;
1140 $answer = $QTYPES['calculated']->substitute_variables($formula, $individualdata);
1141 if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */
1142 /*** Adjust to the correct number of decimals ***/
1143 if (stripos($answer,'e')>0 ){
1144 $answerlengthadd = strlen($answer)-stripos($answer,'e');
1145 }else {
1146 $answerlengthadd = 0 ;
1148 $calculated->answer = round(floatval($answer), $answerlength+$answerlengthadd);
1150 if ($answerlength) {
1151 /* Try to include missing zeros at the end */
1153 if (ereg('^(.*\\.)(.*)$', $calculated->answer, $regs)) {
1154 $calculated->answer = $regs[1] . substr(
1155 $regs[2] . '00000000000000000000000000000000000000000x',
1156 0, $answerlength)
1157 . $unit;
1158 } else {
1159 $calculated->answer .=
1160 substr('.00000000000000000000000000000000000000000x',
1161 0, $answerlength + 1) . $unit;
1163 } else {
1164 /* Attach unit */
1165 $calculated->answer .= $unit;
1168 } else if ($answer) { // Significant figures does only apply if the result is non-zero
1170 // Convert to positive answer...
1171 if ($answer < 0) {
1172 $answer = -$answer;
1173 $sign = '-';
1174 } else {
1175 $sign = '';
1178 // Determine the format 0.[1-9][0-9]* for the answer...
1179 $p10 = 0;
1180 while ($answer < 1) {
1181 --$p10;
1182 $answer *= 10;
1184 while ($answer >= 1) {
1185 ++$p10;
1186 $answer /= 10;
1188 // ... and have the answer rounded of to the correct length
1189 $answer = round($answer, $answerlength);
1191 // Have the answer written on a suitable format,
1192 // Either scientific or plain numeric
1193 if (-2 > $p10 || 4 < $p10) {
1194 // Use scientific format:
1195 $eX = 'e'.--$p10;
1196 $answer *= 10;
1197 if (1 == $answerlength) {
1198 $calculated->answer = $sign.$answer.$eX.$unit;
1199 } else {
1200 // Attach additional zeros at the end of $answer,
1201 $answer .= (1==strlen($answer) ? '.' : '')
1202 . '00000000000000000000000000000000000000000x';
1203 $calculated->answer = $sign
1204 .substr($answer, 0, $answerlength +1).$eX.$unit;
1206 } else {
1207 // Stick to plain numeric format
1208 $answer *= "1e$p10";
1209 if (0.1 <= $answer / "1e$answerlength") {
1210 $calculated->answer = $sign.$answer.$unit;
1211 } else {
1212 // Could be an idea to add some zeros here
1213 $answer .= (ereg('^[0-9]*$', $answer) ? '.' : '')
1214 . '00000000000000000000000000000000000000000x';
1215 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
1216 $calculated->answer = $sign.substr($answer, 0, $oklen).$unit;
1220 } else {
1221 $calculated->answer = 0.0;
1224 /// Return the result
1225 return $calculated;
1229 function qtype_calculated_find_formula_errors($formula) {
1230 /// Validates the formula submitted from the question edit page.
1231 /// Returns false if everything is alright.
1232 /// Otherwise it constructs an error message
1233 // Strip away dataset names
1234 while (ereg('\\{[[:alpha:]][^>} <{"\']*\\}', $formula, $regs)) {
1235 $formula = str_replace($regs[0], '1', $formula);
1238 // Strip away empty space and lowercase it
1239 $formula = strtolower(str_replace(' ', '', $formula));
1241 $safeoperatorchar = '-+/*%>:^~<?=&|!'; /* */
1242 $operatorornumber = "[$safeoperatorchar.0-9eE]";
1245 while (ereg("(^|[$safeoperatorchar,(])([a-z0-9_]*)\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)",
1246 $formula, $regs)) {
1248 switch ($regs[2]) {
1249 // Simple parenthesis
1250 case '':
1251 if ($regs[4] || strlen($regs[3])==0) {
1252 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
1254 break;
1256 // Zero argument functions
1257 case 'pi':
1258 if ($regs[3]) {
1259 return get_string('functiontakesnoargs', 'quiz', $regs[2]);
1261 break;
1263 // Single argument functions (the most common case)
1264 case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
1265 case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
1266 case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
1267 case 'exp': case 'expm1': case 'floor': case 'is_finite':
1268 case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
1269 case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
1270 case 'tan': case 'tanh':
1271 if ($regs[4] || empty($regs[3])) {
1272 return get_string('functiontakesonearg','quiz',$regs[2]);
1274 break;
1276 // Functions that take one or two arguments
1277 case 'log': case 'round':
1278 if ($regs[5] || empty($regs[3])) {
1279 return get_string('functiontakesoneortwoargs','quiz',$regs[2]);
1281 break;
1283 // Functions that must have two arguments
1284 case 'atan2': case 'fmod': case 'pow':
1285 if ($regs[5] || empty($regs[4])) {
1286 return get_string('functiontakestwoargs', 'quiz', $regs[2]);
1288 break;
1290 // Functions that take two or more arguments
1291 case 'min': case 'max':
1292 if (empty($regs[4])) {
1293 return get_string('functiontakesatleasttwo','quiz',$regs[2]);
1295 break;
1297 default:
1298 return get_string('unsupportedformulafunction','quiz',$regs[2]);
1301 // Exchange the function call with '1' and then chack for
1302 // another function call...
1303 if ($regs[1]) {
1304 // The function call is proceeded by an operator
1305 $formula = str_replace($regs[0], $regs[1] . '1', $formula);
1306 } else {
1307 // The function call starts the formula
1308 $formula = ereg_replace("^$regs[2]\\([^)]*\\)", '1', $formula);
1312 if (ereg("[^$safeoperatorchar.0-9eE]+", $formula, $regs)) {
1313 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
1314 } else {
1315 // Formula just might be valid
1316 return false;
1321 function dump($obj) {
1322 echo "<pre>\n";
1323 var_dump($obj);
1324 echo "</pre><br />\n";