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!');
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 $correctanswer = qtype_calculated_calculate_answer(
369 $answer->answer
, $state->options
->dataset
, $answer->tolerance
,
370 $answer->tolerancetype
, $answer->correctanswerlength
,
371 $answer->correctanswerformat
, $unit);
372 $numericalquestion->options
->answers
[$key]->answer
= $correctanswer->answer
;
374 $numericalquestion->questiontext
= parent
::substitute_variables(
375 $numericalquestion->questiontext
, $state->options
->dataset
);
376 //evaluate the equations i.e {=5+4)
378 $qtextremaining = $numericalquestion->questiontext
;
379 while (ereg('\{=([^[:space:]}]*)}', $qtextremaining, $regs1)) {
380 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
381 $qtext =$qtext.$qtextsplits[0];
382 $qtextremaining = $qtextsplits[1];
383 if (empty($regs1[1])) {
386 if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
387 $str=$formulaerrors ;
389 eval('$str = '.$regs1[1].';');
392 $qtext = $qtext.$str ;
394 $numericalquestion->questiontext
= $qtext.$qtextremaining ; // end replace equations
395 $virtualqtype->print_question_formulation_and_controls($numericalquestion, $state, $cmoptions, $options);
397 function grade_responses(&$question, &$state, $cmoptions) {
398 // Forward the grading to the virtual qtype
399 // We modify the question to look like a numerical question
400 $numericalquestion = fullclone($question);
401 foreach ($numericalquestion->options
->answers
as $key => $answer) {
402 $answer = $numericalquestion->options
->answers
[$key]->answer
; // for PHP 4.x
403 $numericalquestion->options
->answers
[$key]->answer
= $this->substitute_variables($answer,
404 $state->options
->dataset
);
406 $virtualqtype = $this->get_virtual_qtype();
407 return $virtualqtype->grade_responses($numericalquestion, $state, $cmoptions) ;
410 function response_summary($question, $state, $length=80) {
411 // The actual response is the bit after the hyphen
412 return substr($state->answer
, strpos($state->answer
, '-')+
1, $length);
416 function check_response(&$question, &$state) {
417 // Forward the checking to the virtual qtype
418 // We modify the question to look like a numerical question
419 $numericalquestion = clone($question);
420 $numericalquestion->options
= clone($question->options
);
421 foreach ($question->options
->answers
as $key => $answer) {
422 $numericalquestion->options
->answers
[$key] = clone($answer);
424 foreach ($numericalquestion->options
->answers
as $key => $answer) {
425 $answer = &$numericalquestion->options
->answers
[$key]; // for PHP 4.x
426 $answer->answer
= $this->substitute_variables($answer->answer
,
427 $state->options
->dataset
);
429 $virtualqtype = $this->get_virtual_qtype();
430 return $virtualqtype->check_response($numericalquestion, $state) ;
434 function get_actual_response(&$question, &$state) {
435 // Substitute variables in questiontext before giving the data to the
437 $virtualqtype = $this->get_virtual_qtype();
438 $unit = $virtualqtype->get_default_numerical_unit($question);
440 // We modify the question to look like a numerical question
441 $numericalquestion = clone($question);
442 $numericalquestion->options
= clone($question->options
);
443 foreach ($question->options
->answers
as $key => $answer) {
444 $numericalquestion->options
->answers
[$key] = clone($answer);
446 foreach ($numericalquestion->options
->answers
as $key => $answer) {
447 $answer = &$numericalquestion->options
->answers
[$key]; // for PHP 4.x
448 $answer->answer
= $this->substitute_variables($answer->answer
,
449 $state->options
->dataset
);
452 $numericalquestion->questiontext
= $this->substitute_variables(
453 $numericalquestion->questiontext
, $state->options
->dataset
);
454 $responses = $virtualqtype->get_all_responses($numericalquestion, $state);
455 $response = reset($responses->responses
);
456 $correct = $response->answer
.' : ';
458 $responses = $virtualqtype->get_actual_response($numericalquestion, $state);
460 foreach ($responses as $key=>$response){
461 $responses[$key] = $correct.$response;
467 function create_virtual_qtype() {
469 require_once("$CFG->dirroot/question/type/numerical/questiontype.php");
470 return new question_numerical_qtype();
473 function supports_dataset_item_generation() {
474 // Calcualted support generation of randomly distributed number data
477 function custom_generator_tools_part(&$mform, $idx, $j){
479 $minmaxgrp = array();
480 $minmaxgrp[] =& $mform->createElement('text', "calcmin[$idx]", get_string('calcmin', 'qtype_datasetdependent'), 'size="3"');
481 $minmaxgrp[] =& $mform->createElement('text', "calcmax[$idx]", get_string('calcmax', 'qtype_datasetdependent'), 'size="3"');
482 $mform->addGroup($minmaxgrp, 'minmaxgrp', get_string('minmax', 'qtype_datasetdependent'), ' - ', false);
483 $mform->setType('calcmin', PARAM_NUMBER
);
484 $mform->setType('calcmax', PARAM_NUMBER
);
486 $precisionoptions = range(0, 10);
487 $mform->addElement('select', "calclength[$idx]", get_string('calclength', 'qtype_datasetdependent'), $precisionoptions);
489 $distriboptions = array('uniform' => get_string('uniform', 'qtype_datasetdependent'), 'loguniform' => get_string('loguniform', 'qtype_datasetdependent'));
490 $mform->addElement('select', "calcdistribution[$idx]", get_string('calcdistribution', 'qtype_datasetdependent'), $distriboptions);
495 function custom_generator_set_data($datasetdefs, $formdata){
497 foreach ($datasetdefs as $datasetdef){
498 if (ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$', $datasetdef->options
, $regs)) {
499 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
500 $formdata["calcdistribution[$idx]"] = $regs[1];
501 $formdata["calcmin[$idx]"] = $regs[2];
502 $formdata["calcmax[$idx]"] = $regs[3];
503 $formdata["calclength[$idx]"] = $regs[4];
510 function custom_generator_tools($datasetdef) {
511 if (ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$',
512 $datasetdef->options
, $regs)) {
513 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
514 for ($i = 0 ; $i<10 ; ++
$i) {
515 $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
517 : 'significantfigures'), 'quiz', $i);
519 return '<input type="submit" onclick="'
520 . "getElementById('addform').regenerateddefid.value='$defid'; return true;"
521 .'" value="'. get_string('generatevalue', 'quiz') . '"/><br/>'
522 . '<input type="text" size="3" name="calcmin[]" '
523 . " value=\"$regs[2]\"/> & <input name=\"calcmax[]\" "
524 . ' type="text" size="3" value="' . $regs[3] .'"/> '
525 . choose_from_menu($lengthoptions, 'calclength[]',
526 $regs[4], // Selected
527 '', '', '', true) . '<br/>'
528 . choose_from_menu(array('uniform' => get_string('uniform', 'quiz'),
529 'loguniform' => get_string('loguniform', 'quiz')),
530 'calcdistribution[]',
531 $regs[1], // Selected
539 function update_dataset_options($datasetdefs, $form) {
540 // Do we have informatin about new options???
541 if (empty($form->definition
) ||
empty($form->calcmin
)
542 ||
empty($form->calcmax
) ||
empty($form->calclength
)
543 ||
empty($form->calcdistribution
)) {
547 // Looks like we just could have some new information here
548 $uniquedefs = array_values(array_unique($form->definition
));
549 foreach ($uniquedefs as $key => $defid) {
550 if (isset($datasetdefs[$defid])
551 && is_numeric($form->calcmin
[$key+
1])
552 && is_numeric($form->calcmax
[$key+
1])
553 && is_numeric($form->calclength
[$key+
1])) {
554 switch ($form->calcdistribution
[$key+
1]) {
555 case 'uniform': case 'loguniform':
556 $datasetdefs[$defid]->options
=
557 $form->calcdistribution
[$key+
1] . ':'
558 . $form->calcmin
[$key+
1] . ':'
559 . $form->calcmax
[$key+
1] . ':'
560 . $form->calclength
[$key+
1];
563 notify("Unexpected distribution ".$form->calcdistribution
[$key+
1]);
569 // Look for empty options, on which we set default values
570 foreach ($datasetdefs as $defid => $def) {
571 if (empty($def->options
)) {
572 $datasetdefs[$defid]->options
= 'uniform:1.0:10.0:1';
578 function save_dataset_items($question, $fromform){
579 // max datasets = 100 items
581 $regenerate = optional_param('forceregeneration', 0, PARAM_BOOL
);
582 // echo "<pre>"; print_r($fromform);
583 if (empty($question->options
)) {
584 $this->get_question_options($question);
586 //get the old datasets for this question
587 $datasetdefs = $this->get_dataset_definitions($question->id
, array());
588 // Handle generator options...
589 $olddatasetdefs = fullclone($datasetdefs);
590 $datasetdefs = $this->update_dataset_options($datasetdefs, $fromform);
592 foreach ($datasetdefs as $defid => $datasetdef) {
593 if (isset($datasetdef->id
)
594 && $datasetdef->options
!= $olddatasetdefs[$defid]->options
) {
595 // Save the new value for options
596 update_record('question_dataset_definitions', $datasetdef);
600 if ($maxnumber == -1 ||
$datasetdef->itemcount
< $maxnumber) {
601 $maxnumber = $datasetdef->itemcount
;
604 // Handle adding and removing of dataset items
606 ksort($fromform->definition
);
607 foreach ($fromform->definition
as $key => $defid) {
608 //if the delete button has not been pressed then skip the datasetitems
609 //in the 'add item' part of the form.
610 if ((!isset($fromform->addbutton
)) && ($i > (count($datasetdefs)*$maxnumber))) {
613 $addeditem = new stdClass();
614 $addeditem->definition
= $datasetdefs[$defid]->id
;
615 $addeditem->value
= $fromform->number
[$i];
616 $addeditem->itemnumber
= ceil($i / count($datasetdefs));
618 if ($fromform->itemid
[$i]) {
619 // Reuse any previously used record
620 $addeditem->id
= $fromform->itemid
[$i];
621 if (!update_record('question_dataset_items', $addeditem)) {
622 error("Error: Unable to update dataset item");
625 if (!insert_record('question_dataset_items', $addeditem)) {
626 error("Error: Unable to insert dataset item");
632 if ($maxnumber < $addeditem->itemnumber
){
633 $maxnumber = $addeditem->itemnumber
;
634 foreach ($datasetdefs as $key => $newdef) {
635 if (isset($newdef->id
) && $newdef->itemcount
<= $maxnumber) {
636 $newdef->itemcount
= $maxnumber;
637 // Save the new value for options
638 update_record('question_dataset_definitions', $newdef);
642 // adding supplementary items
644 if (isset($fromform->addbutton
) && $fromform->selectadd
> 1 && $maxnumber < $max100 ) {
645 $numbertoadd =$fromform->selectadd
-1 ;
646 if ( $max100 - $maxnumber < $numbertoadd ) {
647 $numbertoadd = $max100 - $maxnumber ;
649 //add the other items.
650 // Generate a new dataset item (or reuse an old one)
651 foreach ($datasetdefs as $defid => $datasetdef) {
652 if (isset($datasetdef->id
)) {
653 $datasetdefs[$defid]->items
= get_records_sql( // Use number as key!!
654 " SELECT itemnumber, definition, id, value
655 FROM {$CFG->prefix}question_dataset_items
656 WHERE definition = $datasetdef->id ORDER BY itemnumber");
658 // echo "<pre>"; print_r($datasetdefs[$defid]->items);
659 for ($numberadded =$maxnumber+
1 ; $numberadded <= $maxnumber+
$numbertoadd ; $numberadded++
){
660 if (isset($datasetdefs[$defid]->items
[$numberadded]) && ! $regenerate ){
661 // echo "<p>Reuse an previously used record".$numberadded."id".$datasetdef->id."</p>";
663 $datasetitem = new stdClass
;
664 $datasetitem->definition
= $datasetdef->id
;
665 $datasetitem->itemnumber
= $numberadded;
666 if ($this->supports_dataset_item_generation()) {
667 $datasetitem->value
= $this->generate_dataset_item($datasetdef->options
);
669 $datasetitem->value
= '';
671 //pp echo "<pre>"; print_r( $datasetitem );
672 if (!insert_record('question_dataset_items', $datasetitem)) {
673 error("Error: Unable to insert new dataset item");
678 $maxnumber +
= $numbertoadd ;
679 foreach ($datasetdefs as $key => $newdef) {
680 if (isset($newdef->id
) && $newdef->itemcount
<= $maxnumber) {
681 $newdef->itemcount
= $maxnumber;
682 // Save the new value for options
683 update_record('question_dataset_definitions', $newdef);
688 if (isset($fromform->deletebutton
)) {
689 if(isset($fromform->selectdelete
)) $newmaxnumber = $maxnumber-$fromform->selectdelete
;
690 else $newmaxnumber = $maxnumber-1 ;
691 if ($newmaxnumber < 0 ) $newmaxnumber = 0 ;
692 foreach ($datasetdefs as $datasetdef) {
693 if ($datasetdef->itemcount
== $maxnumber) {
694 $datasetdef->itemcount
= $newmaxnumber ;
695 if (!update_record('question_dataset_definitions',
697 error("Error: Unable to update itemcount");
703 function generate_dataset_item($options) {
704 if (!ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$',
706 // Unknown options...
709 if ($regs[1] == 'uniform') {
710 $nbr = $regs[2] +
($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
711 return round($nbr, $regs[4]);
713 } else if ($regs[1] == 'loguniform') {
714 $log0 = log(abs($regs[2])); // It would have worked the other way to
715 $nbr = exp($log0 +
(log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
717 // Reformat according to the precision $regs[4]:
719 // Determine the format 0.[1-9][0-9]* for the nbr...
729 // ... and have the nbr rounded off to the correct length
730 $nbr = round($nbr, $regs[4]);
732 // Have the nbr written on a suitable format,
733 // Either scientific or plain numeric
734 if (-2 > $p10 ||
4 < $p10) {
735 // Use scientific format:
741 // Attach additional zeros at the end of $nbr,
742 $nbr .= (1==strlen($nbr) ?
'.' : '')
743 . '00000000000000000000000000000000000000000x';
744 $nbr = substr($nbr, 0, $regs[4] +
1).$eX;
747 // Stick to plain numeric format
749 if (0.1 <= $nbr / "1e$regs[4]") {
752 // Could be an idea to add some zeros here
753 $nbr .= (ereg('^[0-9]*$', $nbr) ?
'.' : '')
754 . '00000000000000000000000000000000000000000x';
755 $oklen = $regs[4] +
($p10 < 1 ?
2-$p10 : 1);
756 $nbr = substr($nbr, 0, $oklen);
760 // The larger of the values decide the sign in case the
761 // have equal different signs (which they really must not have)
762 if ($regs[2] +
$regs[3] > 0) {
769 error("The distribution $regs[1] caused problems");
774 function comment_header($question) {
775 //$this->get_question_options($question);
779 $answers = $question->options
->answers
;
781 foreach ($answers as $answer) {
782 if (is_string($answer)) {
783 $strheader .= $delimiter.$answer;
785 $strheader .= $delimiter.$answer->answer
;
787 $delimiter = '<br/><br/>';
792 function comment_on_datasetitems($question, $data, $number) {
793 /// Find a default unit:
794 if (!empty($question->id
) && $unit = get_record('question_numerical_units',
795 'question', $question->id
, 'multiplier', 1.0)) {
801 $answers = $question->options
->answers
;
803 $strmin = get_string('min', 'quiz');
804 $strmax = get_string('max', 'quiz');
807 $virtualqtype = $this->get_virtual_qtype();
808 foreach ($answers as $answer) {
809 $formula = $answer->answer
;
810 foreach ($data as $name => $value) {
811 $formula = str_replace('{'.$name.'}', $value, $formula);
813 $calculated = qtype_calculated_calculate_answer(
814 $answer->answer
, $data, $answer->tolerance
,
815 $answer->tolerancetype
, $answer->correctanswerlength
,
816 $answer->correctanswerformat
, $unit);
817 $calculated->tolerance
= $answer->tolerance
;
818 $calculated->tolerancetype
= $answer->tolerancetype
;
819 $calculated->correctanswerlength
= $answer->correctanswerlength
;
820 $calculated->correctanswerformat
= $answer->correctanswerformat
;
821 $virtualqtype->get_tolerance_interval($calculated);
822 if ($calculated->min
=== '') {
823 // This should mean that something is wrong
824 $stranswers .= " -$calculated->answer".'<br/><br/>';
826 $stranswers .= $formula.' = '.$calculated->answer
.'<br/>' ;
827 $stranswers .= $strmin. $delimiter.$calculated->min
.'---';
828 $stranswers .= $strmax.$delimiter.$calculated->max
;
829 $stranswers .='<br/>';
832 return "$stranswers";
835 function tolerance_types() {
836 return array('1' => get_string('relative', 'quiz'),
837 '2' => get_string('nominal', 'quiz'),
838 '3' => get_string('geometric', 'quiz'));
841 function dataset_options($form, $name, $mandatory=true,$renameabledatasets=false) {
842 // Takes datasets from the parent implementation but
843 // filters options that are currently not accepted by calculated
844 // It also determines a default selection...
845 //$renameabledatasets not implemented anmywhere
846 list($options, $selected) = parent
::dataset_options($form, $name,'','qtype_calculated');
847 // list($options, $selected) = $this->dataset_optionsa($form, $name);
849 foreach ($options as $key => $whatever) {
850 if (!ereg('^'.LITERAL
.'-', $key) && $key != '0') {
851 unset($options[$key]);
856 $selected = LITERAL
. "-0-$name"; // Default
858 $selected = "0"; // Default
861 return array($options, $selected);
864 function construct_dataset_menus($form, $mandatorydatasets,
866 $datasetmenus = array();
867 foreach ($mandatorydatasets as $datasetname) {
868 if (!isset($datasetmenus[$datasetname])) {
869 list($options, $selected) =
870 $this->dataset_options($form, $datasetname);
871 unset($options['0']); // Mandatory...
872 $datasetmenus[$datasetname] = choose_from_menu ($options,
873 'dataset[]', $selected, '', '', "0", true);
876 foreach ($optionaldatasets as $datasetname) {
877 if (!isset($datasetmenus[$datasetname])) {
878 list($options, $selected) =
879 $this->dataset_options($form, $datasetname);
880 $datasetmenus[$datasetname] = choose_from_menu ($options,
881 'dataset[]', $selected, '', '', "0", true);
884 return $datasetmenus;
887 function get_correct_responses(&$question, &$state) {
888 $virtualqtype = $this->get_virtual_qtype();
889 if($unit = $virtualqtype->get_default_numerical_unit($question)){
894 foreach ($question->options
->answers
as $answer) {
895 if (((int) $answer->fraction
) === 1) {
896 $answernumerical = qtype_calculated_calculate_answer(
897 $answer->answer
, $state->options
->dataset
, $answer->tolerance
,
898 $answer->tolerancetype
, $answer->correctanswerlength
,
899 $answer->correctanswerformat
, $unit);
900 return array('' => $answernumerical->answer
);
906 function substitute_variables($str, $dataset) {
907 $formula = parent
::substitute_variables($str, $dataset);
908 if ($error = qtype_calculated_find_formula_errors($formula)) {
911 /// Calculate the correct answer
912 if (empty($formula)) {
915 eval('$str = '.$formula.';');
921 * This function retrieve the item count of the available category shareable
922 * wild cards that is added as a comment displayed when a wild card with
923 * the same name is displayed in datasetdefinitions_form.php
925 function get_dataset_definitions_category($form) {
927 $datasetdefs = array();
929 if (!empty($form->category
)) {
930 $sql = "SELECT i.*,d.*
931 FROM {$CFG->prefix}question_datasets d,
932 {$CFG->prefix}question_dataset_definitions i
933 WHERE i.id = d.datasetdefinition
934 AND i.category = '$form->category'
937 if ($records = get_records_sql($sql)) {
938 foreach ($records as $r) {
939 if ( !isset ($datasetdefs["$r->name"])) $datasetdefs["$r->name"] = $r->itemcount
;
943 return $datasetdefs ;
947 * This function build a table showing the available category shareable
948 * wild cards, their name, their definition (Min, Max, Decimal) , the item count
949 * and the name of the question where they are used.
950 * This table is intended to be add before the question text to help the user use
954 function print_dataset_definitions_category($form) {
956 $datasetdefs = array();
958 $namestr =get_string('name', 'quiz');
959 $minstr=get_string('min', 'quiz');
960 $maxstr=get_string('max', 'quiz');
961 $rangeofvaluestr=get_string('minmax','qtype_datasetdependent');
962 $questionusingstr = get_string('usedinquestion','qtype_calculated');
963 $wildcardstr = get_string('wildcard', 'qtype_calculated');
964 $itemscountstr = get_string('itemscount','qtype_datasetdependent');
966 if (!empty($form->category
)) {
967 $sql = "SELECT i.*,d.*
968 FROM {$CFG->prefix}question_datasets d,
969 {$CFG->prefix}question_dataset_definitions i
970 WHERE i.id = d.datasetdefinition
971 AND i.category = '$form->category';
973 if ($records = get_records_sql($sql)) {
974 foreach ($records as $r) {
976 FROM {$CFG->prefix}question q
977 WHERE q.id = $r->question
979 if ( !isset ($datasetdefs["$r->type-$r->category-$r->name"])){
980 $datasetdefs["$r->type-$r->category-$r->name"]= $r;
982 if ($questionb = get_records_sql($sql1)) {
983 $datasetdefs["$r->type-$r->category-$r->name"]->questions
[$r->question
]->name
=$questionb[$r->question
]->name
;
988 if (!empty ($datasetdefs)){
990 $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>";
991 foreach ($datasetdefs as $datasetdef){
992 list($distribution, $min, $max,$dec) = explode(':', $datasetdef->options
, 4);
993 $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\">";
994 foreach ($datasetdef->questions
as $qu) {
995 //limit the name length displayed
996 if (!empty($qu->name
)) {
997 $qu->name
= (strlen($qu->name
) > $lnamemax) ?
998 substr($qu->name
, 0, $lnamemax).'...' : $qu->name
;
1002 $text .=" $qu->name <br/>";
1004 $text .="</td></tr>";
1008 $text .=get_string('nosharedwildcard', 'qtype_calculated');
1014 /// BACKUP FUNCTIONS ////////////////////////////
1017 * Backup the data in the question
1019 * This is used in question/backuplib.php
1021 function backup($bf,$preferences,$question,$level=6) {
1025 $calculateds = get_records("question_calculated","question",$question,"id");
1026 //If there are calculated-s
1028 //Iterate over each calculateds
1029 foreach ($calculateds as $calculated) {
1030 $status = $status &&fwrite ($bf,start_tag("CALCULATED",$level,true));
1031 //Print calculated contents
1032 fwrite ($bf,full_tag("ANSWER",$level+
1,false,$calculated->answer
));
1033 fwrite ($bf,full_tag("TOLERANCE",$level+
1,false,$calculated->tolerance
));
1034 fwrite ($bf,full_tag("TOLERANCETYPE",$level+
1,false,$calculated->tolerancetype
));
1035 fwrite ($bf,full_tag("CORRECTANSWERLENGTH",$level+
1,false,$calculated->correctanswerlength
));
1036 fwrite ($bf,full_tag("CORRECTANSWERFORMAT",$level+
1,false,$calculated->correctanswerformat
));
1037 //Now backup numerical_units
1038 $status = question_backup_numerical_units($bf,$preferences,$question,7);
1039 //Now backup required dataset definitions and items...
1040 $status = question_backup_datasets($bf,$preferences,$question,7);
1041 //End calculated data
1042 $status = $status &&fwrite ($bf,end_tag("CALCULATED",$level,true));
1044 //Now print question_answers
1045 $status = question_backup_answers($bf,$preferences,$question);
1050 /// RESTORE FUNCTIONS /////////////////
1053 * Restores the data in the question
1055 * This is used in question/restorelib.php
1057 function restore($old_question_id,$new_question_id,$info,$restore) {
1061 //Get the calculated-s array
1062 $calculateds = $info['#']['CALCULATED'];
1064 //Iterate over calculateds
1065 for($i = 0; $i < sizeof($calculateds); $i++
) {
1066 $cal_info = $calculateds[$i];
1067 //traverse_xmlize($cal_info); //Debug
1068 //print_object ($GLOBALS['traverse_array']); //Debug
1069 //$GLOBALS['traverse_array']=""; //Debug
1071 //Now, build the question_calculated record structure
1072 $calculated->question
= $new_question_id;
1073 $calculated->answer
= backup_todb($cal_info['#']['ANSWER']['0']['#']);
1074 $calculated->tolerance
= backup_todb($cal_info['#']['TOLERANCE']['0']['#']);
1075 $calculated->tolerancetype
= backup_todb($cal_info['#']['TOLERANCETYPE']['0']['#']);
1076 $calculated->correctanswerlength
= backup_todb($cal_info['#']['CORRECTANSWERLENGTH']['0']['#']);
1077 $calculated->correctanswerformat
= backup_todb($cal_info['#']['CORRECTANSWERFORMAT']['0']['#']);
1079 ////We have to recode the answer field
1080 $answer = backup_getid($restore->backup_unique_code
,"question_answers",$calculated->answer
);
1082 $calculated->answer
= $answer->new_id
;
1085 //The structure is equal to the db, so insert the question_calculated
1086 $newid = insert_record ("question_calculated",$calculated);
1089 if (($i+
1) %
50 == 0) {
1090 if (!defined('RESTORE_SILENTLY')) {
1092 if (($i+
1) %
1000 == 0) {
1099 //Now restore numerical_units
1100 $status = question_restore_numerical_units ($old_question_id,$new_question_id,$cal_info,$restore);
1102 //Now restore dataset_definitions
1103 if ($status && $newid) {
1104 $status = question_restore_dataset_definitions ($old_question_id,$new_question_id,$cal_info,$restore);
1115 //// END OF CLASS ////
1117 //////////////////////////////////////////////////////////////////////////
1118 //// INITIATION - Without this line the question type is not in use... ///
1119 //////////////////////////////////////////////////////////////////////////
1120 question_register_questiontype(new question_calculated_qtype());
1122 function qtype_calculated_calculate_answer($formula, $individualdata,
1123 $tolerance, $tolerancetype, $answerlength, $answerformat='1', $unit='') {
1124 /// The return value has these properties:
1125 /// ->answer the correct answer
1126 /// ->min the lower bound for an acceptable response
1127 /// ->max the upper bound for an accetpable response
1129 /// Exchange formula variables with the correct values...
1131 $answer = $QTYPES['calculated']->substitute_variables($formula, $individualdata);
1132 if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */
1133 /*** Adjust to the correct number of decimals ***/
1135 $calculated->answer
= round($answer, $answerlength);
1137 if ($answerlength) {
1138 /* Try to include missing zeros at the end */
1140 if (ereg('^(.*\\.)(.*)$', $calculated->answer
, $regs)) {
1141 $calculated->answer
= $regs[1] . substr(
1142 $regs[2] . '00000000000000000000000000000000000000000x',
1146 $calculated->answer
.=
1147 substr('.00000000000000000000000000000000000000000x',
1148 0, $answerlength +
1) . $unit;
1152 $calculated->answer
.= $unit;
1155 } else if ($answer) { // Significant figures does only apply if the result is non-zero
1157 // Convert to positive answer...
1165 // Determine the format 0.[1-9][0-9]* for the answer...
1167 while ($answer < 1) {
1171 while ($answer >= 1) {
1175 // ... and have the answer rounded of to the correct length
1176 $answer = round($answer, $answerlength);
1178 // Have the answer written on a suitable format,
1179 // Either scientific or plain numeric
1180 if (-2 > $p10 ||
4 < $p10) {
1181 // Use scientific format:
1184 if (1 == $answerlength) {
1185 $calculated->answer
= $sign.$answer.$eX.$unit;
1187 // Attach additional zeros at the end of $answer,
1188 $answer .= (1==strlen($answer) ?
'.' : '')
1189 . '00000000000000000000000000000000000000000x';
1190 $calculated->answer
= $sign
1191 .substr($answer, 0, $answerlength +
1).$eX.$unit;
1194 // Stick to plain numeric format
1195 $answer *= "1e$p10";
1196 if (0.1 <= $answer / "1e$answerlength") {
1197 $calculated->answer
= $sign.$answer.$unit;
1199 // Could be an idea to add some zeros here
1200 $answer .= (ereg('^[0-9]*$', $answer) ?
'.' : '')
1201 . '00000000000000000000000000000000000000000x';
1202 $oklen = $answerlength +
($p10 < 1 ?
2-$p10 : 1);
1203 $calculated->answer
= $sign.substr($answer, 0, $oklen).$unit;
1208 $calculated->answer
= 0.0;
1211 /// Return the result
1216 function qtype_calculated_find_formula_errors($formula) {
1217 /// Validates the formula submitted from the question edit page.
1218 /// Returns false if everything is alright.
1219 /// Otherwise it constructs an error message
1220 // Strip away dataset names
1221 while (ereg('\\{[[:alpha:]][^>} <{"\']*\\}', $formula, $regs)) {
1222 $formula = str_replace($regs[0], '1', $formula);
1225 // Strip away empty space and lowercase it
1226 $formula = strtolower(str_replace(' ', '', $formula));
1228 $safeoperatorchar = '-+/*%>:^~<?=&|!'; /* */
1229 $operatorornumber = "[$safeoperatorchar.0-9eE]";
1232 while (ereg("(^|[$safeoperatorchar,(])([a-z0-9_]*)\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)",
1236 // Simple parenthesis
1238 if ($regs[4] ||
strlen($regs[3])==0) {
1239 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
1243 // Zero argument functions
1246 return get_string('functiontakesnoargs', 'quiz', $regs[2]);
1250 // Single argument functions (the most common case)
1251 case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
1252 case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
1253 case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
1254 case 'exp': case 'expm1': case 'floor': case 'is_finite':
1255 case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
1256 case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
1257 case 'tan': case 'tanh':
1258 if ($regs[4] ||
empty($regs[3])) {
1259 return get_string('functiontakesonearg','quiz',$regs[2]);
1263 // Functions that take one or two arguments
1264 case 'log': case 'round':
1265 if ($regs[5] ||
empty($regs[3])) {
1266 return get_string('functiontakesoneortwoargs','quiz',$regs[2]);
1270 // Functions that must have two arguments
1271 case 'atan2': case 'fmod': case 'pow':
1272 if ($regs[5] ||
empty($regs[4])) {
1273 return get_string('functiontakestwoargs', 'quiz', $regs[2]);
1277 // Functions that take two or more arguments
1278 case 'min': case 'max':
1279 if (empty($regs[4])) {
1280 return get_string('functiontakesatleasttwo','quiz',$regs[2]);
1285 return get_string('unsupportedformulafunction','quiz',$regs[2]);
1288 // Exchange the function call with '1' and then chack for
1289 // another function call...
1291 // The function call is proceeded by an operator
1292 $formula = str_replace($regs[0], $regs[1] . '1', $formula);
1294 // The function call starts the formula
1295 $formula = ereg_replace("^$regs[2]\\([^)]*\\)", '1', $formula);
1299 if (ereg("[^$safeoperatorchar.0-9eE]+", $formula, $regs)) {
1300 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
1302 // Formula just might be valid
1308 function dump($obj) {
1311 echo "</pre><br />\n";