Automatic installer.php lang files by installer_builder (20070726)
[moodle-linuxchix.git] / question / type / calculated / questiontype.php
bloba19254f1450eb7c6e77e977fafe69f2485689418
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!');
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 $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)
377 $qtext = "";
378 $qtextremaining = $numericalquestion->questiontext ;
379 while (ereg('\{=([^[:space:]}]*)}', $qtextremaining, $regs1)) {
380 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
381 $qtext =$qtext.$qtextsplits[0];
382 $qtextremaining = $qtextsplits[1];
383 if (empty($regs1[1])) {
384 $str = '';
385 } else {
386 if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
387 $str=$formulaerrors ;
388 }else {
389 eval('$str = '.$regs1[1].';');
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);
415 // ULPGC ecastro
416 function check_response(&$question, &$state) {
417 // Forward the checking to the virtual qtype
418 // We modify the question to look like a numerical question
419 $numericalquestion = clone($question);
420 $numericalquestion->options = clone($question->options);
421 foreach ($question->options->answers as $key => $answer) {
422 $numericalquestion->options->answers[$key] = clone($answer);
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) ;
433 // ULPGC ecastro
434 function get_actual_response(&$question, &$state) {
435 // Substitute variables in questiontext before giving the data to the
436 // virtual type
437 $virtualqtype = $this->get_virtual_qtype();
438 $unit = $virtualqtype->get_default_numerical_unit($question);
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);
450 // apply_unit
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;
464 return $responses;
467 function create_virtual_qtype() {
468 global $CFG;
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
475 return true;
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){
496 $idx = 1;
497 foreach ($datasetdefs as $datasetdef){
498 if (ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$', $datasetdef->options, $regs)) {
499 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
500 $formdata["calcdistribution[$idx]"] = $regs[1];
501 $formdata["calcmin[$idx]"] = $regs[2];
502 $formdata["calcmax[$idx]"] = $regs[3];
503 $formdata["calclength[$idx]"] = $regs[4];
505 $idx++;
507 return $formdata;
510 function custom_generator_tools($datasetdef) {
511 if (ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$',
512 $datasetdef->options, $regs)) {
513 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
514 for ($i = 0 ; $i<10 ; ++$i) {
515 $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
516 ? 'decimals'
517 : 'significantfigures'), 'quiz', $i);
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]\"/> &amp; <input name=\"calcmax[]\" "
524 . ' type="text" size="3" value="' . $regs[3] .'"/> '
525 . choose_from_menu($lengthoptions, 'calclength[]',
526 $regs[4], // Selected
527 '', '', '', true) . '<br/>'
528 . choose_from_menu(array('uniform' => get_string('uniform', 'quiz'),
529 'loguniform' => get_string('loguniform', 'quiz')),
530 'calcdistribution[]',
531 $regs[1], // Selected
532 '', '', '', true);
533 } else {
534 return '';
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)) {
544 // I guess not
546 } else {
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];
561 break;
562 default:
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';
575 return $datasetdefs;
578 function save_dataset_items($question, $fromform){
579 // max datasets = 100 items
580 $max100 = 100 ;
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);
591 $maxnumber = -1;
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);
599 // Get maxnumber
600 if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) {
601 $maxnumber = $datasetdef->itemcount;
604 // Handle adding and removing of dataset items
605 $i = 1;
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))) {
611 break;
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");
624 } else {
625 if (!insert_record('question_dataset_items', $addeditem)) {
626 error("Error: Unable to insert dataset item");
630 $i++;
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
643 $numbertoadd =0;
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>";
662 } else {
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);
668 } else {
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");
676 }//for number added
677 }// datasetsdefs end
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',
696 $datasetdef)) {
697 error("Error: Unable to update itemcount");
703 function generate_dataset_item($options) {
704 if (!ereg('^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$',
705 $options, $regs)) {
706 // Unknown options...
707 return false;
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...
720 $p10 = 0;
721 while ($nbr < 1) {
722 --$p10;
723 $nbr *= 10;
725 while ($nbr >= 1) {
726 ++$p10;
727 $nbr /= 10;
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:
736 $eX = 'e'.--$p10;
737 $nbr *= 10;
738 if (1 == $regs[4]) {
739 $nbr = $nbr.$eX;
740 } else {
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;
746 } else {
747 // Stick to plain numeric format
748 $nbr *= "1e$p10";
749 if (0.1 <= $nbr / "1e$regs[4]") {
750 $nbr = $nbr;
751 } else {
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) {
763 return $nbr;
764 } else {
765 return -$nbr;
768 } else {
769 error("The distribution $regs[1] caused problems");
771 return '';
774 function comment_header($question) {
775 //$this->get_question_options($question);
776 $strheader = '';
777 $delimiter = '';
779 $answers = $question->options->answers;
781 foreach ($answers as $answer) {
782 if (is_string($answer)) {
783 $strheader .= $delimiter.$answer;
784 } else {
785 $strheader .= $delimiter.$answer->answer;
787 $delimiter = '<br/><br/>';
789 return $strheader;
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)) {
796 $unit = $unit->unit;
797 } else {
798 $unit = '';
801 $answers = $question->options->answers;
802 $stranswers = '';
803 $strmin = get_string('min', 'quiz');
804 $strmax = get_string('max', 'quiz');
805 $errors = '';
806 $delimiter = ': ';
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/>';
825 } else {
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]);
854 if (!$selected) {
855 if ($mandatory){
856 $selected = LITERAL . "-0-$name"; // Default
857 }else {
858 $selected = "0"; // Default
861 return array($options, $selected);
864 function construct_dataset_menus($form, $mandatorydatasets,
865 $optionaldatasets) {
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)){
890 $unit = $unit->unit;
891 } else {
892 $unit = '';
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);
903 return null;
906 function substitute_variables($str, $dataset) {
907 $formula = parent::substitute_variables($str, $dataset);
908 if ($error = qtype_calculated_find_formula_errors($formula)) {
909 return $error;
911 /// Calculate the correct answer
912 if (empty($formula)) {
913 $str = '';
914 } else {
915 eval('$str = '.$formula.';');
917 return $str;
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) {
926 global $CFG;
927 $datasetdefs = array();
928 $lnamemax = 30;
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
951 * these wild cards
954 function print_dataset_definitions_category($form) {
955 global $CFG;
956 $datasetdefs = array();
957 $lnamemax = 22;
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');
965 $text ='';
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) {
975 $sql1 = "SELECT q.*
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&nbsp;&nbsp;</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;
999 } else {
1000 $qu->name = '';
1002 $text .=" &nbsp;&nbsp; $qu->name <br/>";
1004 $text .="</td></tr>";
1006 $text .="</table>";
1007 }else{
1008 $text .=get_string('nosharedwildcard', 'qtype_calculated');
1010 return $text ;
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) {
1023 $status = true;
1025 $calculateds = get_records("question_calculated","question",$question,"id");
1026 //If there are calculated-s
1027 if ($calculateds) {
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);
1047 return $status;
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) {
1059 $status = true;
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);
1081 if ($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);
1088 //Do some output
1089 if (($i+1) % 50 == 0) {
1090 if (!defined('RESTORE_SILENTLY')) {
1091 echo ".";
1092 if (($i+1) % 1000 == 0) {
1093 echo "<br />";
1096 backup_flush(300);
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);
1107 if (!$newid) {
1108 $status = false;
1112 return $status;
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...
1130 global $QTYPES;
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',
1143 0, $answerlength)
1144 . $unit;
1145 } else {
1146 $calculated->answer .=
1147 substr('.00000000000000000000000000000000000000000x',
1148 0, $answerlength + 1) . $unit;
1150 } else {
1151 /* Attach 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...
1158 if ($answer < 0) {
1159 $answer = -$answer;
1160 $sign = '-';
1161 } else {
1162 $sign = '';
1165 // Determine the format 0.[1-9][0-9]* for the answer...
1166 $p10 = 0;
1167 while ($answer < 1) {
1168 --$p10;
1169 $answer *= 10;
1171 while ($answer >= 1) {
1172 ++$p10;
1173 $answer /= 10;
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:
1182 $eX = 'e'.--$p10;
1183 $answer *= 10;
1184 if (1 == $answerlength) {
1185 $calculated->answer = $sign.$answer.$eX.$unit;
1186 } else {
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;
1193 } else {
1194 // Stick to plain numeric format
1195 $answer *= "1e$p10";
1196 if (0.1 <= $answer / "1e$answerlength") {
1197 $calculated->answer = $sign.$answer.$unit;
1198 } else {
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;
1207 } else {
1208 $calculated->answer = 0.0;
1211 /// Return the result
1212 return $calculated;
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+)+)?)?)?\\)",
1233 $formula, $regs)) {
1235 switch ($regs[2]) {
1236 // Simple parenthesis
1237 case '':
1238 if ($regs[4] || strlen($regs[3])==0) {
1239 return get_string('illegalformulasyntax', 'quiz', $regs[0]);
1241 break;
1243 // Zero argument functions
1244 case 'pi':
1245 if ($regs[3]) {
1246 return get_string('functiontakesnoargs', 'quiz', $regs[2]);
1248 break;
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]);
1261 break;
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]);
1268 break;
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]);
1275 break;
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]);
1282 break;
1284 default:
1285 return get_string('unsupportedformulafunction','quiz',$regs[2]);
1288 // Exchange the function call with '1' and then chack for
1289 // another function call...
1290 if ($regs[1]) {
1291 // The function call is proceeded by an operator
1292 $formula = str_replace($regs[0], $regs[1] . '1', $formula);
1293 } else {
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]);
1301 } else {
1302 // Formula just might be valid
1303 return false;
1308 function dump($obj) {
1309 echo "<pre>\n";
1310 var_dump($obj);
1311 echo "</pre><br />\n";