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;
21 function get_question_options(&$question) {
22 // First get the datasets and default options
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
. '!');
36 if(false === parent::get_question_options($question)) {
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;
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);
72 function get_datasets_for_export(&$question){
73 $datasetdefs = array();
74 if (!empty($question->id
)) {
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) {
85 if ($def->category
=='0'){
86 $def->status
='private';
88 $def->status
='shared';
90 $def->type
='calculated' ;
91 list($distribution, $min, $max,$dec) = explode(':', $def->options
, 4);
92 $def->distribution
=$distribution;
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)){
104 foreach( $items as $ii){
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;
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();
135 $virtualqtype = $this->get_virtual_qtype();
136 $result = $virtualqtype->save_numerical_units($question);
137 if (isset($result->error
)) {
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)";
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!";
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]);
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)";
181 } else { // new options
182 if (! insert_record('question_calculated', $options)) {
183 $result->error
= "Could not insert question calculated options!";
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
)) {
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;
226 }else if ($dataset->status
=='shared' ){
227 if ($sharedatasetdefs = get_records_select(
228 'question_dataset_definitions',
230 AND name = '$dataset->name'
231 AND category = '$question->category'
233 )) { // so there is at least one
234 $sharedatasetdef = array_shift($sharedatasetdefs);
235 if ( $sharedatasetdef->options
== $datasetdef->options
){// identical so use it
237 $datasetdef =$sharedatasetdef ;
238 } else { // different so create a private one
239 $datasetdef->category
= 0;
242 }else { // no so create one
243 $datasetdef->category
=$question->category
;
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',
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);
291 function validate_form($form) {
292 switch($form->wizardpage
) {
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');
302 foreach ($form->answers
as $key => $answer) {
303 if ('' === trim($answer)) {
304 $calculatedmessages[] =
305 get_string('missingformula', 'quiz');
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 />';
330 return parent
::validate_form($form);
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);
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)){
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)
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])) {
383 if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
384 $str=$formulaerrors ;
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);
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) ;
431 function get_actual_response(&$question, &$state) {
432 // Substitute variables in questiontext before giving the data to the
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
);
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;
464 function create_virtual_qtype() {
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
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){
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];
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'
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]\"/> & <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
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
)) {
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];
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';
575 function save_dataset_items($question, $fromform){
577 // max datasets = 100 items
579 if(isset($fromform->nextpageparam
["forceregeneration"])) {
580 $regenerate = $fromform->nextpageparam
["forceregeneration"];
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);
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);
602 if ($maxnumber == -1 ||
$datasetdef->itemcount
< $maxnumber) {
603 $maxnumber = $datasetdef->itemcount
;
606 // Handle adding and removing of dataset items
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))) {
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");
627 if (!insert_record('question_dataset_items', $addeditem)) {
628 error("Error: Unable to insert dataset item");
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
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>";
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
);
671 $datasetitem->value
= '';
673 if (!insert_record('question_dataset_items', $datasetitem)) {
674 error("Error: Unable to insert new dataset item");
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',
698 error("Error: Unable to update itemcount");
704 function generate_dataset_item($options) {
706 if (!ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$',
708 // Unknown options...
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);
721 error("The distribution $regs[1] caused problems");
726 function comment_header($question) {
727 //$this->get_question_options($question);
731 $answers = $question->options
->answers
;
733 foreach ($answers as $answer) {
734 if (is_string($answer)) {
735 $strheader .= $delimiter.$answer;
737 $strheader .= $delimiter.$answer->answer
;
739 $delimiter = '<br/><br/><br/>';
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)) {
753 $answers = fullclone($question->options
->answers
);
755 $strmin = get_string('min', 'quiz');
756 $strmax = get_string('max', 'quiz');
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/>';
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>';
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]);
813 $selected = LITERAL
. "-0-$name"; // Default
815 $selected = "0"; // Default
818 return array($options, $selected);
821 function construct_dataset_menus($form, $mandatorydatasets,
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)){
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
);
868 function substitute_variables($str, $dataset) {
869 $formula = parent
::substitute_variables($str, $dataset);
870 if ($error = qtype_calculated_find_formula_errors($formula)) {
873 /// Calculate the correct answer
874 if (empty($formula)) {
877 eval('$str = '.$formula.';');
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) {
889 $datasetdefs = array();
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
916 function print_dataset_definitions_category($form) {
918 $datasetdefs = array();
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');
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) {
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 </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
;
964 $text .=" $qu->name <br/>";
966 $text .="</td></tr>";
970 $text .=get_string('nosharedwildcard', 'qtype_calculated');
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) {
987 $calculateds = get_records("question_calculated","question",$question,"id");
988 //If there are calculated-s
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);
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) {
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
);
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);
1051 if (($i+
1) %
50 == 0) {
1052 if (!defined('RESTORE_SILENTLY')) {
1054 if (($i+
1) %
1000 == 0) {
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);
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}?";
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",
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...
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');
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',
1159 $calculated->answer
.=
1160 substr('.00000000000000000000000000000000000000000x',
1161 0, $answerlength +
1) . $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...
1178 // Determine the format 0.[1-9][0-9]* for the answer...
1180 while ($answer < 1) {
1184 while ($answer >= 1) {
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:
1197 if (1 == $answerlength) {
1198 $calculated->answer
= $sign.$answer.$eX.$unit;
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;
1207 // Stick to plain numeric format
1208 $answer *= "1e$p10";
1209 if (0.1 <= $answer / "1e$answerlength") {
1210 $calculated->answer
= $sign.$answer.$unit;
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;
1221 $calculated->answer
= 0.0;
1224 /// Return the result
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+)+)?)?)?\\)",
1249 // Simple parenthesis
1251 if ($regs[4] ||
strlen($regs[3])==0) {
1252 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
1256 // Zero argument functions
1259 return get_string('functiontakesnoargs', 'quiz', $regs[2]);
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]);
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]);
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]);
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]);
1298 return get_string('unsupportedformulafunction','quiz',$regs[2]);
1301 // Exchange the function call with '1' and then chack for
1302 // another function call...
1304 // The function call is proceeded by an operator
1305 $formula = str_replace($regs[0], $regs[1] . '1', $formula);
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]);
1315 // Formula just might be valid
1321 function dump($obj) {
1324 echo "</pre><br />\n";