ea43f1ebaec31647a4de5b784c8404668bac56b2
[moodle.git] / question / type / calculated / questiontype.php
1 <?php
3 /////////////////
4 // CALCULATED ///
5 /////////////////
7 /// QUESTION TYPE CLASS //////////////////
11 class question_calculated_qtype extends default_questiontype {
13     // Used by the function custom_generator_tools:
14     var $calcgenerateidhasbeenadded = false;
15     public $virtualqtype = false;
17     function name() {
18         return 'calculated';
19     }
21     function has_wildcards_in_responses($question, $subqid) {
22         return true;
23     }
25     function requires_qtypes() {
26         return array('numerical');
27     }
29     function get_question_options(&$question) {
30         // First get the datasets and default options
31         // the code is used for calculated, calculatedsimple and calculatedmulti qtypes
32          global $CFG, $DB, $OUTPUT, $QTYPES;
33         if (!$question->options = $DB->get_record('question_calculated_options', array('question' => $question->id))) {
34           //  echo $OUTPUT->notification('Error: Missing question options for calculated question'.$question->id.'!');
35           //  return false;
36             $question->options->synchronize = 0;
37             $question->options->single = 0; //$question->single;
38             $question->options->answernumbering = 'abc';
39             $question->options->shuffleanswers = 0 ;
40             $question->options->correctfeedback = '';
41             $question->options->partiallycorrectfeedback = '';
42             $question->options->incorrectfeedback = '';
44         }
46         if (!$question->options->answers = $DB->get_records_sql(
47                                 "SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.correctanswerformat " .
48                                 "FROM {question_answers} a, " .
49                                 "     {question_calculated} c " .
50                                 "WHERE a.question = ? " .
51                                 "AND   a.id = c.answer ".
52                                 "ORDER BY a.id ASC", array($question->id))) {
53             echo $OUTPUT->notification('Error: Missing question answer for calculated question ' . $question->id . '!');
54             return false;
55         }
57         if ( $this->get_virtual_qtype() ==  $QTYPES['numerical']){
58             $QTYPES['numerical']->get_numerical_units($question);
59             $QTYPES['numerical']->get_numerical_options($question);
60         }
62         if( isset($question->export_process)&&$question->export_process){
63             $question->options->datasets = $this->get_datasets_for_export($question);
64         }
65         return true;
66     }
68     function get_datasets_for_export(&$question){
69         global $DB;
70         $datasetdefs = array();
71         if (!empty($question->id)) {
72             global $CFG;
73             $sql = "SELECT i.*
74                     FROM {question_datasets} d,
75                          {question_dataset_definitions} i
76                     WHERE d.question = ?
77                     AND   d.datasetdefinition = i.id
78                    ";
79             if ($records = $DB->get_records_sql($sql, array($question->id))) {
80                 foreach ($records as $r) {
81                     $def = $r ;
82                     if ($def->category=='0'){
83                         $def->status='private';
84                     } else {
85                         $def->status='shared';
86                     }
87                     $def->type ='calculated' ;
88                     list($distribution, $min, $max,$dec) = explode(':', $def->options, 4);
89                     $def->distribution=$distribution;
90                     $def->minimum=$min;
91                     $def->maximum=$max;
92                     $def->decimals=$dec ;
93                      if ($def->itemcount > 0 ) {
94                         // get the datasetitems
95                         $def->items = array();
96                         if ($items = $this->get_database_dataset_items($def->id)){
97                             $n = 0;
98                             foreach( $items as $ii){
99                                 $n++;
100                                 $def->items[$n] = new stdClass;
101                                 $def->items[$n]->itemnumber=$ii->itemnumber;
102                                 $def->items[$n]->value=$ii->value;
103                            }
104                            $def->number_of_items=$n ;
105                         }
106                     }
107                     $datasetdefs["1-$r->category-$r->name"] = $def;
108                 }
109             }
110         }
111         return $datasetdefs ;
112     }
114     function save_question_options($question) {
115         //$options = $question->subtypeoptions;
116         // Get old answers:
117         // the code is used for calculated, calculatedsimple and calculatedmulti qtypes
118         global $CFG, $DB, $QTYPES ;
119         if (isset($question->answer) && !isset($question->answers)) {
120             $question->answers = $question->answer;
121         }
122         // calculated options
123         $update = true ;
124         $options = $DB->get_record("question_calculated_options", array("question" => $question->id));
125         if (!$options) {
126             $update = false;
127             $options = new stdClass;
128             $options->question = $question->id;
129         }
130         // as used only by calculated
131         if(isset($question->synchronize)){
132         $options->synchronize = $question->synchronize;
133         }else {
134             $options->synchronize = 0 ;
135         }
136         $options->single = 0; //$question->single;
137         $options->answernumbering =  $question->answernumbering;
138         $options->shuffleanswers = $question->shuffleanswers;
139         $options->correctfeedback = trim($question->correctfeedback);
140         $options->partiallycorrectfeedback = trim($question->partiallycorrectfeedback);
141         $options->incorrectfeedback = trim($question->incorrectfeedback);
142         if ($update) {
143             if (!$DB->update_record("question_calculated_options", $options)) {
144                 $result->error = "Could not update calculated question options! (id=$options->id)";
145                 return $result;
146             }
147         } else {
148             if (!$DB->insert_record("question_calculated_options", $options)) {
149                 $result->error = "Could not insert calculated question options!";
150                 return $result;
151             }
152         }
154         // Get old versions of the objects
155         if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) {
156             $oldanswers = array();
157         }
159         if (!$oldoptions = $DB->get_records('question_calculated', array('question' => $question->id), 'answer ASC')) {
160             $oldoptions = array();
161         }
163         // Save the units.
164         $virtualqtype = $this->get_virtual_qtype();
165         $result = $virtualqtype->save_numerical_units($question);
166         if (isset($result->error)) {
167             return $result;
168         } else {
169             $units = &$result->units;
170         }
171         // Insert all the new answers
172         if (isset($question->answer) && !isset($question->answers)) {
173             $question->answers=$question->answer;
174         }
175         foreach ($question->answers as $key => $dataanswer) {
176             if (  trim($dataanswer) != '' ) {
177                 $answer = new stdClass;
178                 $answer->question = $question->id;
179                 $answer->answer = trim($dataanswer);
180                 $answer->fraction = $question->fraction[$key];
181                 $answer->feedback = trim($question->feedback[$key]);
183                 if ($oldanswer = array_shift($oldanswers)) {  // Existing answer, so reuse it
184                     $answer->id = $oldanswer->id;
185                     $DB->update_record("question_answers", $answer);
186                 } else { // This is a completely new answer
187                     $answer->id = $DB->insert_record("question_answers", $answer);
188                 }
190                 // Set up the options object
191                 if (!$options = array_shift($oldoptions)) {
192                     $options = new stdClass;
193                 }
194                 $options->question  = $question->id;
195                 $options->answer    = $answer->id;
196                 $options->tolerance = trim($question->tolerance[$key]);
197                 $options->tolerancetype  = trim($question->tolerancetype[$key]);
198                 $options->correctanswerlength  = trim($question->correctanswerlength[$key]);
199                 $options->correctanswerformat  = trim($question->correctanswerformat[$key]);
201                 // Save options
202                 if (isset($options->id)) { // reusing existing record
203                     $DB->update_record('question_calculated', $options);
204                 } else { // new options
205                     $DB->insert_record('question_calculated', $options);
206                 }
207             }
208         }
209         // delete old answer records
210         if (!empty($oldanswers)) {
211             foreach($oldanswers as $oa) {
212                 $DB->delete_records('question_answers', array('id' => $oa->id));
213             }
214         }
216         // delete old answer records
217         if (!empty($oldoptions)) {
218             foreach($oldoptions as $oo) {
219                 $DB->delete_records('question_calculated', array('id' => $oo->id));
220             }
221         }
222         $result = $QTYPES['numerical']->save_numerical_options($question);
223         if (isset($result->error)) {
224             return $result;
225         }
228         if( isset($question->import_process)&&$question->import_process){
229             $this->import_datasets($question);
230          }
231         // Report any problems.
232         if (!empty($result->notice)) {
233             return $result;
234         }
235         return true;
236     }
238     function import_datasets($question){
239         global $DB;
240         $n = count($question->dataset);
241         foreach ($question->dataset as $dataset) {
242             // name, type, option,
243             $datasetdef = new stdClass();
244             $datasetdef->name = $dataset->name;
245             $datasetdef->type = 1 ;
246             $datasetdef->options =  $dataset->distribution.':'.$dataset->min.':'.$dataset->max.':'.$dataset->length;
247             $datasetdef->itemcount=$dataset->itemcount;
248             if ( $dataset->status =='private'){
249                 $datasetdef->category = 0;
250                 $todo='create' ;
251             }else if ($dataset->status =='shared' ){
252                 if ($sharedatasetdefs = $DB->get_records_select(
253                         'question_dataset_definitions',
254                         "type = '1'
255                         AND name = ?
256                         AND category = ?
257                         ORDER BY id DESC ", array($dataset->name, $question->category)
258                        )) { // so there is at least one
259                     $sharedatasetdef = array_shift($sharedatasetdefs);
260                     if ( $sharedatasetdef->options ==  $datasetdef->options ){// identical so use it
261                         $todo='useit' ;
262                         $datasetdef =$sharedatasetdef ;
263                     } else { // different so create a private one
264                         $datasetdef->category = 0;
265                         $todo='create' ;
266                     }
267                 }else { // no so create one
268                     $datasetdef->category =$question->category ;
269                     $todo='create' ;
270                }
271             }
272             if (  $todo=='create'){
273                 $datasetdef->id = $DB->insert_record( 'question_dataset_definitions', $datasetdef);
274            }
275            // Create relation to the dataset:
276            $questiondataset = new stdClass;
277            $questiondataset->question = $question->id;
278            $questiondataset->datasetdefinition = $datasetdef->id;
279             $DB->insert_record('question_datasets', $questiondataset);
280             if ($todo=='create'){ // add the items
281                 foreach ($dataset->datasetitem as $dataitem ){
282                     $datasetitem = new stdClass;
283                     $datasetitem->definition=$datasetdef->id ;
284                     $datasetitem->itemnumber = $dataitem->itemnumber ;
285                     $datasetitem->value = $dataitem->value ;
286                     $DB->insert_record('question_dataset_items', $datasetitem);
287                 }
288             }
289         }
290     }
292     function restore_session_and_responses(&$question, &$state) {
293         global $OUTPUT;
294         if (!preg_match('~^dataset([0-9]+)[^-]*-(.*)$~',
295                 $state->responses[''], $regs)) {
296             echo $OUTPUT->notification("Wrongly formatted raw response answer " .
297                    "{$state->responses['']}! Could not restore session for " .
298                    " question #{$question->id}.");
299             $state->options->datasetitem = 1;
300             $state->options->dataset = array();
301             $state->responses = array('' => '');
302             return false;
303         }
305         // Restore the chosen dataset
306         $state->options->datasetitem = $regs[1];
307         $state->options->dataset =
308          $this->pick_question_dataset($question,$state->options->datasetitem);
309         $state->responses = array('' => $regs[2]);
310         $virtualqtype = $this->get_virtual_qtype();
311         return $virtualqtype->restore_session_and_responses($question, $state);
312     }
314     function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
315         // Find out how many datasets are available
316         global $CFG, $DB, $QTYPES, $OUTPUT;
317         if(!$maxnumber = (int)$DB->get_field_sql(
318                             "SELECT MIN(a.itemcount)
319                             FROM {question_dataset_definitions} a,
320                                  {question_datasets} b
321                             WHERE b.question = ?
322                             AND   a.id = b.datasetdefinition", array($question->id))) {
323             print_error('cannotgetdsforquestion', 'question', '', $question->id);
324         }
325                     $sql = "SELECT i.*
326                     FROM {question_datasets} d,
327                          {question_dataset_definitions} i
328                     WHERE d.question = ?
329                     AND   d.datasetdefinition = i.id
330                     AND   i.category != 0
331                    ";
332         if (!$question->options->synchronize || !$records = $DB->get_records_sql($sql, array($question->id))) {
333             $synchronize_calculated  =  false ;
334         }else {
335            // i.e records is true so test coherence
336            $coherence = true ;
337                 $a = new stdClass ;
338                 $a->qid = $question->id ;
339                 $a->qcat = $question->category ;
340            foreach($records as $def ){
341                 if ($def->category != $question->category){
342                     $a->name = $def->name;
343                     $a->sharedcat = $def->category ;
344                     $coherence = false ;
345                     break;
346                 }
347             }
348             if(!$coherence){
349                          echo $OUTPUT->notification(get_string('nocoherencequestionsdatyasetcategory','qtype_calculated',$a));
350           }
351             $synchronize_calculated  = true ;
352         }
354         // Choose a random dataset
355         // maxnumber sould not be breater than 100
356         if ($maxnumber > CALCULATEDQUESTIONMAXITEMNUMBER ){
357             $maxnumber = CALCULATEDQUESTIONMAXITEMNUMBER ;
358         }
359         if ( $synchronize_calculated === false ) {
360             $state->options->datasetitem = rand(1, $maxnumber);
361         }else{
362             $state->options->datasetitem = intval( $maxnumber * substr($attempt->timestart,-2) /100 ) ;
363             if ($state->options->datasetitem < 1) {
364                 $state->options->datasetitem =1 ;
365             } else if ($state->options->datasetitem > $maxnumber){
366                 $state->options->datasetitem = $maxnumber ;
367             }
369         };
370         $state->options->dataset =
371          $this->pick_question_dataset($question,$state->options->datasetitem);
372                     $virtualqtype = $this->get_virtual_qtype( );
373             return $virtualqtype->create_session_and_responses($question, $state, $cmoptions, $attempt);
374         }
378     function save_session_and_responses(&$question, &$state) {
379         global $DB;
380         $responses = 'dataset'.$state->options->datasetitem.'-' ;
381         // regular numeric type
382          if(isset($state->responses['unit']) && isset($question->options->units[$state->responses['unit']])){
383             $responses .= $state->responses['answer'].'|||||'.$question->options->units[$state->responses['unit']]->unit;
384         }else if(isset($state->responses['unit'])){
385             $responses .= $state->responses['answer'].'|||||'.$state->responses['unit'] ;
386         }else {
387             $responses .= $state->responses['answer'].'|||||';
388         }
390         // Set the legacy answer field
391         if (!$DB->set_field('question_states', 'answer', $responses, array('id'=> $state->id))) {
392             return false;
393         }
394         return true;
395     }
397     function create_runtime_question($question, $form) {
398         $question = parent::create_runtime_question($question, $form);
399         $question->options->answers = array();
400         foreach ($form->answers as $key => $answer) {
401             $a->answer              = trim($form->answer[$key]);
402             $a->fraction              = $form->fraction[$key];//new
403            $a->tolerance           = $form->tolerance[$key];
404             $a->tolerancetype       = $form->tolerancetype[$key];
405             $a->correctanswerlength = $form->correctanswerlength[$key];
406             $a->correctanswerformat = $form->correctanswerformat[$key];
407             $question->options->answers[] = clone($a);
408         }
410         return $question;
411     }
413     function validate_form($form) {
414         switch($form->wizardpage) {
415             case 'question':
416                 $calculatedmessages = array();
417                 if (empty($form->name)) {
418                     $calculatedmessages[] = get_string('missingname', 'quiz');
419                 }
420                 if (empty($form->questiontext)) {
421                     $calculatedmessages[] = get_string('missingquestiontext', 'quiz');
422                 }
423                 // Verify formulas
424                 foreach ($form->answers as $key => $answer) {
425                     if ('' === trim($answer)) {
426                         $calculatedmessages[] =
427                             get_string('missingformula', 'quiz');
428                     }
429                     if ($formulaerrors =
430                      qtype_calculated_find_formula_errors($answer)) {
431                         $calculatedmessages[] = $formulaerrors;
432                     }
433                     if (! isset($form->tolerance[$key])) {
434                         $form->tolerance[$key] = 0.0;
435                     }
436                     if (! is_numeric($form->tolerance[$key])) {
437                         $calculatedmessages[] =
438                             get_string('tolerancemustbenumeric', 'quiz');
439                     }
440                 }
442                 if (!empty($calculatedmessages)) {
443                     $errorstring = "The following errors were found:<br />";
444                     foreach ($calculatedmessages as $msg) {
445                         $errorstring .= $msg . '<br />';
446                     }
447                     print_error($errorstring);
448                 }
450                 break;
451             default:
452                 return parent::validate_form($form);
453                 break;
454         }
455         return true;
456     }
457     function finished_edit_wizard(&$form) {
458         return isset($form->backtoquiz);
459     }
460     // This gets called by editquestion.php after the standard question is saved
461     function print_next_wizard_page(&$question, &$form, $course) {
462         global $CFG, $USER, $SESSION, $COURSE;
464         // Catch invalid navigation & reloads
465         if (empty($question->id) && empty($SESSION->calculated)) {
466             redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired.', 3);
467         }
469         // See where we're coming from
470         switch($form->wizardpage) {
471             case 'question':
472                 require("$CFG->dirroot/question/type/calculated/datasetdefinitions.php");
473                 break;
474             case 'datasetdefinitions':
475             case 'datasetitems':
476                 require("$CFG->dirroot/question/type/calculated/datasetitems.php");
477                 break;
478             default:
479                 print_error('invalidwizardpage', 'question');
480                 break;
481         }
482     }
484     // This gets called by question2.php after the standard question is saved
485     function &next_wizard_form($submiturl, $question, $wizardnow){
486         global $CFG, $SESSION, $COURSE;
488         // Catch invalid navigation & reloads
489         if (empty($question->id) && empty($SESSION->calculated)) {
490             redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired. Cannot get next wizard form.', 3);
491         }
492         if (empty($question->id)){
493             $question =& $SESSION->calculated->questionform;
494         }
496         // See where we're coming from
497         switch($wizardnow) {
498             case 'datasetdefinitions':
499                 require("$CFG->dirroot/question/type/calculated/datasetdefinitions_form.php");
500                 $mform = new question_dataset_dependent_definitions_form("$submiturl?wizardnow=datasetdefinitions", $question);
501                 break;
502             case 'datasetitems':
503                 require("$CFG->dirroot/question/type/calculated/datasetitems_form.php");
504                 $regenerate = optional_param('forceregeneration', 0, PARAM_BOOL);
505                 $mform = new question_dataset_dependent_items_form("$submiturl?wizardnow=datasetitems", $question, $regenerate);
506                 break;
507             default:
508                 print_error('invalidwizardpage', 'question');
509                 break;
510         }
512         return $mform;
513     }
515     /**
516      * This method should be overriden if you want to include a special heading or some other
517      * html on a question editing page besides the question editing form.
518      *
519      * @param question_edit_form $mform a child of question_edit_form
520      * @param object $question
521      * @param string $wizardnow is '' for first page.
522      */
523     function display_question_editing_page(&$mform, $question, $wizardnow){
524         global $OUTPUT ;
525         switch ($wizardnow){
526             case '':
527                 //on first page default display is fine
528                 parent::display_question_editing_page($mform, $question, $wizardnow);
529                 return;
530                 break;
531             case 'datasetdefinitions':
532                  echo $OUTPUT->heading_with_help(get_string("choosedatasetproperties", "qtype_calculated"), 'questiondatasets', 'qtype_calculated');
533                 break;
534             case 'datasetitems':
535                echo $OUTPUT->heading_with_help(get_string("editdatasets", "qtype_calculated"), 'questiondatasets', 'qtype_calculated');
536                 break;
537         }
540         $mform->display();
542     }
544      /**
545      * This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php
546      * so that they can be saved
547      * using the function save_dataset_definitions($form)
548      *  when creating a new calculated question or
549      *  whenediting an already existing calculated question
550      * or by  function save_as_new_dataset_definitions($form, $initialid)
551      *  when saving as new an already existing calculated question
552      *
553      * @param object $form
554      * @param int $questionfromid default = '0'
555      */
556     function preparedatasets(&$form , $questionfromid='0'){
557         // the dataset names present in the edit_question_form and edit_calculated_form are retrieved
558         $possibledatasets = $this->find_dataset_names($form->questiontext);
559         $mandatorydatasets = array();
560             foreach ($form->answers as $answer) {
561                 $mandatorydatasets += $this->find_dataset_names($answer);
562             }
563         // if there are identical datasetdefs already saved in the original question.
564         // either when editing a question or saving as new
565         // they are retrieved using $questionfromid
566         if ($questionfromid!='0'){
567             $form->id = $questionfromid ;
568         }
569         $datasets = array();
570         $key = 0 ;
571         // always prepare the mandatorydatasets present in the answers
572         // the $options are not used here
573         foreach ($mandatorydatasets as $datasetname) {
574             if (!isset($datasets[$datasetname])) {
575                 list($options, $selected) =
576                         $this->dataset_options($form, $datasetname);
577                 $datasets[$datasetname]='';
578                  $form->dataset[$key]=$selected ;
579                 $key++;
580             }
581         }
582         // do not prepare possibledatasets when creating a question
583         // they will defined and stored with datasetdefinitions_form.php
584         // the $options are not used here
585         if ($questionfromid!='0'){
587         foreach ($possibledatasets as $datasetname) {
588             if (!isset($datasets[$datasetname])) {
589                 list($options, $selected) =
590                         $this->dataset_options($form, $datasetname,false);
591                 $datasets[$datasetname]='';
592                  $form->dataset[$key]=$selected ;
593                 $key++;
594             }
595         }
596         }
597      return $datasets ;
598      }
599     function addnamecategory(&$question){
600         global $DB;
601                             $categorydatasetdefs = $DB->get_records_sql(
602                 "SELECT  a.*
603                    FROM {question_datasets} b,
604                         {question_dataset_definitions} a
605                   WHERE a.id = b.datasetdefinition
606                     AND a.type = '1'
607                     AND a.category != 0
608                     AND b.question = ? ORDER BY a.name ",array($question->id));
609         $questionname = $question->name ;
610         $regs= array();
611         if(preg_match('~#\{([^[:space:]]*)#~',$questionname , $regs)){
612                $questionname = str_replace($regs[0], '', $questionname);
613         };
615         if (!empty($categorydatasetdefs)){ // there is at least one with the same name
616             $questionname  ="#".$questionname;
617             foreach($categorydatasetdefs as $def) {
618                 if(strlen("{$def->name}")+strlen($questionname) < 250 ){
619                     $questionname = '{'.$def->name.'}'
620                     .$questionname;
621                 }
622             }
623             $questionname ="#".$questionname;
624         }
625          if (!$DB->set_field('question', 'name', $questionname, array("id" => $question->id))) {
626             return false ;
627         }
628     }
630     /**
631     * this version save the available data at the different steps of the question editing process
632     * without using global $SESSION as storage between steps
633     * at the first step $wizardnow = 'question'
634     *  when creating a new question
635     *  when modifying a question
636     *  when copying as a new question
637     *  the general parameters and answers are saved using parent::save_question
638     *  then the datasets are prepared and saved
639     * at the second step $wizardnow = 'datasetdefinitions'
640     *  the datadefs final type are defined as private, category or not a datadef
641     * at the third step $wizardnow = 'datasetitems'
642     *  the datadefs parameters and the data items are created or defined
643     *
644     * @param object question
645     * @param object $form
646     * @param int $course
647     * @param PARAM_ALPHA $wizardnow should be added as we are coming from question2.php
648     */
649     function save_question($question, $form, $course) {
650         global $DB;
651         $wizardnow =  optional_param('wizardnow', '', PARAM_ALPHA);
652         $id = optional_param('id', 0, PARAM_INT); // question id
653         // in case 'question'
654         // for a new question $form->id is empty
655         // when saving as new question
656         //   $question->id = 0, $form is $data from question2.php
657         //   and $data->makecopy is defined as $data->id is the initial question id
658         // edit case. If it is a new question we don't necessarily need to
659         // return a valid question object
661         // See where we're coming from
662         switch($wizardnow) {
663             case '' :
664             case 'question': // coming from the first page, creating the second
665                 if (empty($form->id)) { // for a new question $form->id is empty
666                     $question = parent::save_question($question, $form, $course);
667                    //prepare the datasets using default $questionfromid
668                     $this->preparedatasets($form);
669                    $form->id = $question->id;
670                    $this->save_dataset_definitions($form);
671                     if(isset($form->synchronize) && $form->synchronize == 2 ){
672                         $this->addnamecategory($question);
673                     }
674                 } else if (!empty($form->makecopy)){
675                    $questionfromid =  $form->id ;
676                    $question = parent::save_question($question, $form, $course);
677                     //prepare the datasets
678                     $this->preparedatasets($form,$questionfromid);
679                     $form->id = $question->id;
680                     $this->save_as_new_dataset_definitions($form,$questionfromid );
681                     if(isset($form->synchronize) && $form->synchronize == 2 ){
682                         $this->addnamecategory($question);
683                     }
684                 }  else {// editing a question
685                     $question = parent::save_question($question, $form, $course);
686                     //prepare the datasets
687                     $this->preparedatasets($form,$question->id);
688                     $form->id = $question->id;
689                     $this->save_dataset_definitions($form);
690                     if(isset($form->synchronize) && $form->synchronize == 2 ){
691                         $this->addnamecategory($question);
692                     }
693                 }
694                 break;
695             case 'datasetdefinitions':
696                 // calculated options
697                 // it cannot go here without having done the first page
698                 // so the question_calculated_options should exist
699                 // only need to update the synchronize field
700                 if(isset($form->synchronize) ){
701                     $options_synchronize = $form->synchronize ;
702                 }else {
703                     $options_synchronize = 0 ;
704                 }
705                 if (!$DB->set_field('question_calculated_options', 'synchronize', $options_synchronize, array("question" => $question->id))) {
706                     return false;
707                 }
708                     if(isset($form->synchronize) && $form->synchronize == 2 ){
709                         $this->addnamecategory($question);
710                     }
712                 $this->save_dataset_definitions($form);
713                 break;
714             case 'datasetitems':
715                 $this->save_dataset_items($question, $form);
716                 $this->save_question_calculated($question, $form);
717                 break;
718             default:
719                 print_error('invalidwizardpage', 'question');
720                 break;
721         }
722         return $question;
723     }
724     /**
725     * Deletes question from the question-type specific tables
726     *
727     * @return boolean Success/Failure
728     * @param object $question  The question being deleted
729     */
730     function delete_question($questionid) {
731         global $DB;
733         $DB->delete_records("question_calculated", array("question" => $questionid));
734         $DB->delete_records("question_calculated_options", array("question" => $questionid));
735         $DB->delete_records("question_numerical_units", array("question" => $questionid));
736         if ($datasets = $DB->get_records('question_datasets', array('question' => $questionid))) {
737             foreach ($datasets as $dataset) {
738                 if (!$DB->get_records_select(
739                         'question_datasets',
740                         "question != ?
741                         AND datasetdefinition = ? ", array($questionid, $dataset->datasetdefinition))){
742                     $DB->delete_records('question_dataset_definitions', array('id' => $dataset->datasetdefinition));
743                     $DB->delete_records('question_dataset_items', array('definition' => $dataset->datasetdefinition));
744                 }
745             }
746         }
747         $DB->delete_records("question_datasets", array("question" => $questionid));
748         return true;
749     }
750     function test_response(&$question, &$state, $answer) {
751           $virtualqtype = $this->get_virtual_qtype();
752         return $virtualqtype->test_response($question, $state, $answer);
754     }
755     function compare_responses(&$question, $state, $teststate) {
757                  $virtualqtype = $this->get_virtual_qtype();
758         return $virtualqtype->compare_responses($question, $state, $teststate);
759     }
761     function convert_answers (&$question, &$state){
762             foreach ($question->options->answers as $key => $answer) {
763                 $answer = fullclone($question->options->answers[$key]);
764                 $question->options->answers[$key]->answer = $this->substitute_variables_and_eval($answer->answer,
765                 $state->options->dataset);
766             }
767         }
768     function convert_questiontext (&$question, &$state){
769         $tolerancemax =0.01;
770          $tolerancetypemax = 1 ;
771          $correctanswerlengthmax = 2 ;
772          $correctanswerformatmax = 1 ;
773          $tolerancemaxset = false ;
774         foreach ($question->options->answers as $key => $answer) {
775              if($answer->fraction == 1.0 && !$tolerancemaxset){
776                 $tolerancemax = $answer->tolerance;
777                 $tolerancetypemax = $answer->tolerancetype ;
778                 $correctanswerlengthmax = $answer->correctanswerlength;
779                 $correctanswerformatmax =$answer->correctanswerformat;
780                 $tolerancemaxset = true ;
781             }
782         }
783         $question->questiontext = $this->substitute_variables(
784         $question->questiontext, $state->options->dataset);
785         //evaluate the equations i.e {=5+4)
786         $qtext = "";
787         $qtextremaining = $question->questiontext ;
788         while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
789       //  while  (preg_match('~\{=|%=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
790             $qtextsplits = explode($regs1[0], $qtextremaining, 2);
791             $qtext =$qtext.$qtextsplits[0];
792             $qtextremaining = $qtextsplits[1];
793             if (empty($regs1[1])) {
794                     $str = '';
795                 } else {
796                     if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
797                         $str=$formulaerrors ;
798                     }else {
799                        eval('$str = '.$regs1[1].';');
800                        $texteval= qtype_calculated_calculate_answer(
801                      $str, $state->options->dataset, $tolerancemax,
802                      $tolerancetypemax, $correctanswerlengthmax,
803                         $correctanswerformatmax, '');
804                         $str = $texteval->answer;
806                         ;
807                     }
808                 }
809                 $qtext = $qtext.$str ;
810         }
811         $question->questiontext = $qtext.$qtextremaining ; // end replace equations
812      }
814     function get_default_numerical_unit($question,$virtualqtype){
815             if($unit = $virtualqtype->get_default_numerical_unit($question)){
816                  $unit = $unit->unit;
817             } else {
818                 $unit = '';
819             }
820             return $unit ;
822     }
823     function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
824         // Substitute variables in questiontext before giving the data to the
825         // virtual type for printing
826         $virtualqtype = $this->get_virtual_qtype();
827         // why $unit as it is not use
828         $unit = $this-> get_default_numerical_unit($question,$virtualqtype);
829         // We modify the question to look like a numerical question
830         $numericalquestion = fullclone($question);
831             $this->convert_answers($numericalquestion, $state);
832             $this->convert_questiontext($numericalquestion, $state);
833  /*        $tolerancemax =0.01;
834          $tolerancetypemax = 1 ;
835          $correctanswerlengthmax = 2 ;
836          $correctanswerformatmax = 1 ;
837          $tolerancemaxset = false ;
838         foreach ($numericalquestion->options->answers as $key => $answer) {
839              if($answer->fraction == 1.0 && !$tolerancemaxset){
840                 $tolerancemax = $answer->tolerance;
841                 $tolerancetypemax = $answer->tolerancetype ;
842                 $correctanswerlengthmax = $answer->correctanswerlength;
843                 $correctanswerformatmax =$answer->correctanswerformat;
844                 $tolerancemaxset = true ;
845             }
846         }
848         $numericalquestion->questiontext = $this->substitute_variables(
849         $numericalquestion->questiontext, $state->options->dataset);
850         //evaluate the equations i.e {=5+4)
851         $qtext = "";
852         $qtextremaining = $numericalquestion->questiontext ;
853         while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
854       //  while  (preg_match('~\{=|%=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
855             $qtextsplits = explode($regs1[0], $qtextremaining, 2);
856             $qtext =$qtext.$qtextsplits[0];
857             $qtextremaining = $qtextsplits[1];
858             if (empty($regs1[1])) {
859                     $str = '';
860                 } else {
861                     if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
862                         $str=$formulaerrors ;
863                     }else {
864                        eval('$str = '.$regs1[1].';');
865                        $texteval= qtype_calculated_calculate_answer(
866                      $str, $state->options->dataset, $tolerancemax,
867                      $tolerancetypemax, $correctanswerlengthmax,
868                         $correctanswerformatmax, '');
869                         $str = $texteval->answer;
871                         ;
872                     }
873                 }
874                 $qtext = $qtext.$str ;
875         }
876         $numericalquestion->questiontext = $qtext.$qtextremaining ; // end replace equations
877         */
879         $virtualqtype->print_question_formulation_and_controls($numericalquestion, $state, $cmoptions, $options);
880     }
881     function grade_responses(&$question, &$state, $cmoptions) {
882         // Forward the grading to the virtual qtype
883         // We modify the question to look like a numerical question
884         $numericalquestion = fullclone($question);
885        foreach ($numericalquestion->options->answers as $key => $answer) {
886             $answer = $numericalquestion->options->answers[$key]->answer; // for PHP 4.x
887           $numericalquestion->options->answers[$key]->answer = $this->substitute_variables_and_eval($answer,
888              $state->options->dataset);
889        }
890          $virtualqtype = $this->get_virtual_qtype();
891         return $virtualqtype->grade_responses($numericalquestion, $state, $cmoptions) ;
892     }
895     // ULPGC ecastro
896     function check_response(&$question, &$state) {
897         // Forward the checking to the virtual qtype
898         // We modify the question to look like a numerical question
899         $numericalquestion = clone($question);
900         $numericalquestion->options = clone($question->options);
901         foreach ($question->options->answers as $key => $answer) {
902             $numericalquestion->options->answers[$key] = clone($answer);
903         }
904         foreach ($numericalquestion->options->answers as $key => $answer) {
905             $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
906             $answer->answer = $this->substitute_variables_and_eval($answer->answer,
907              $state->options->dataset);
908         }
909         $virtualqtype = $this->get_virtual_qtype();
910         return $virtualqtype->check_response($numericalquestion, $state) ;
911     }
913     // ULPGC ecastro
914     function get_actual_response(&$question, &$state) {
915         // Substitute variables in questiontext before giving the data to the
916         // virtual type
917         $virtualqtype = $this->get_virtual_qtype();
918         $unit = $virtualqtype->get_default_numerical_unit($question);
920         // We modify the question to look like a numerical question
921         $numericalquestion = clone($question);
922         $numericalquestion->options = clone($question->options);
923         foreach ($question->options->answers as $key => $answer) {
924             $numericalquestion->options->answers[$key] = clone($answer);
925         }
926         foreach ($numericalquestion->options->answers as $key => $answer) {
927             $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x
928             $answer->answer = $this->substitute_variables_and_eval($answer->answer,
929              $state->options->dataset);
930             // apply_unit
931         }
932         $numericalquestion->questiontext = $this->substitute_variables_and_eval(
933                                   $numericalquestion->questiontext, $state->options->dataset);
934         $responses = $virtualqtype->get_all_responses($numericalquestion, $state);
935         $response = reset($responses->responses);
936         $correct = $response->answer.' : ';
938         $responses = $virtualqtype->get_actual_response($numericalquestion, $state);
940         foreach ($responses as $key=>$response){
941             $responses[$key] = $correct.$response;
942         }
944         return $responses;
945     }
947     function create_virtual_qtype() {
948         global $CFG;
949             require_once("$CFG->dirroot/question/type/numerical/questiontype.php");
950             return new question_numerical_qtype();
951     }
953     function supports_dataset_item_generation() {
954     // Calcualted support generation of randomly distributed number data
955         return true;
956     }
957     function custom_generator_tools_part(&$mform, $idx, $j){
959         $minmaxgrp = array();
960         $minmaxgrp[] =& $mform->createElement('text', "calcmin[$idx]", get_string('calcmin', 'qtype_calculated'));
961         $minmaxgrp[] =& $mform->createElement('text', "calcmax[$idx]", get_string('calcmax', 'qtype_calculated'));
962         $mform->addGroup($minmaxgrp, 'minmaxgrp', get_string('minmax', 'qtype_calculated'), ' - ', false);
963         $mform->setType("calcmin[$idx]", PARAM_NUMBER);
964         $mform->setType("calcmax[$idx]", PARAM_NUMBER);
966         $precisionoptions = range(0, 10);
967         $mform->addElement('select', "calclength[$idx]", get_string('calclength', 'qtype_calculated'), $precisionoptions);
969         $distriboptions = array('uniform' => get_string('uniform', 'qtype_calculated'), 'loguniform' => get_string('loguniform', 'qtype_calculated'));
970         $mform->addElement('select', "calcdistribution[$idx]", get_string('calcdistribution', 'qtype_calculated'), $distriboptions);
973     }
975     function custom_generator_set_data($datasetdefs, $formdata){
976         $idx = 1;
977         foreach ($datasetdefs as $datasetdef){
978             if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~', $datasetdef->options, $regs)) {
979                 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
980                 $formdata["calcdistribution[$idx]"] = $regs[1];
981                 $formdata["calcmin[$idx]"] = $regs[2];
982                 $formdata["calcmax[$idx]"] = $regs[3];
983                 $formdata["calclength[$idx]"] = $regs[4];
984             }
985             $idx++;
986         }
987         return $formdata;
988     }
990     function custom_generator_tools($datasetdef) {
991         global $OUTPUT;
992         if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
993                 $datasetdef->options, $regs)) {
994             $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
995             for ($i = 0 ; $i<10 ; ++$i) {
996                 $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
997                                                 ? 'decimals'
998                                                 : 'significantfigures'), 'quiz', $i);
999             }
1000             $menu1 = html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
1002             $options = array('uniform' => get_string('uniform', 'quiz'), 'loguniform' => get_string('loguniform', 'quiz'));
1003             $menu2 = html_writer::select($options, 'calcdistribution[]', $regs[1], null);
1004             return '<input type="submit" onclick="'
1005                     . "getElementById('addform').regenerateddefid.value='$defid'; return true;"
1006                     .'" value="'. get_string('generatevalue', 'quiz') . '"/><br/>'
1007                     . '<input type="text" size="3" name="calcmin[]" '
1008                     . " value=\"$regs[2]\"/> &amp; <input name=\"calcmax[]\" "
1009                     . ' type="text" size="3" value="' . $regs[3] .'"/> '
1010                     . $menu1 . '<br/>'
1011                     . $menu2;
1012         } else {
1013             return '';
1014         }
1015     }
1018     function update_dataset_options($datasetdefs, $form) {
1019         // Do we have informatin about new options???
1020         if (empty($form->definition) || empty($form->calcmin)
1021                 || empty($form->calcmax) || empty($form->calclength)
1022                 || empty($form->calcdistribution)) {
1023             // I guess not
1025         } else {
1026             // Looks like we just could have some new information here
1027             $uniquedefs = array_values(array_unique($form->definition));
1028             foreach ($uniquedefs as $key => $defid) {
1029                 if (isset($datasetdefs[$defid])
1030                         && is_numeric($form->calcmin[$key+1])
1031                         && is_numeric($form->calcmax[$key+1])
1032                         && is_numeric($form->calclength[$key+1])) {
1033                     switch     ($form->calcdistribution[$key+1]) {
1034                         case 'uniform': case 'loguniform':
1035                             $datasetdefs[$defid]->options =
1036                                     $form->calcdistribution[$key+1] . ':'
1037                                     . $form->calcmin[$key+1] . ':'
1038                                     . $form->calcmax[$key+1] . ':'
1039                                     . $form->calclength[$key+1];
1040                             break;
1041                         default:
1042                             echo $OUTPUT->notification("Unexpected distribution ".$form->calcdistribution[$key+1]);
1043                     }
1044                 }
1045             }
1046         }
1048         // Look for empty options, on which we set default values
1049         foreach ($datasetdefs as $defid => $def) {
1050             if (empty($def->options)) {
1051                 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1';
1052             }
1053         }
1054         return $datasetdefs;
1055     }
1057     function save_question_calculated($question, $fromform){
1058         global $DB;
1060         foreach ($question->options->answers as $key => $answer) {
1061             if ($options = $DB->get_record('question_calculated', array('answer' => $key))) {
1062                 $options->tolerance = trim($fromform->tolerance[$key]);
1063                 $options->tolerancetype  = trim($fromform->tolerancetype[$key]);
1064                 $options->correctanswerlength  = trim($fromform->correctanswerlength[$key]);
1065                 $options->correctanswerformat  = trim($fromform->correctanswerformat[$key]);
1066                 $DB->update_record('question_calculated', $options);
1067             }
1068         }
1069     }
1071                 /**
1072                 * This function get the dataset items using id as unique parameter and return an
1073                 * array with itemnumber as index sorted ascendant
1074                 * If the multiple records with the same itemnumber exist, only the newest one
1075                 * i.e with the greatest id is used, the others are ignored but not deleted.
1076                 * MDL-19210
1077                 */
1078     function get_database_dataset_items($definition){
1079         global $CFG, $DB;
1080                 $databasedataitems = $DB->get_records_sql( // Use number as key!!
1081                         " SELECT id , itemnumber, definition,  value
1082                           FROM {question_dataset_items}
1083                           WHERE definition = $definition order by id DESC ", array($definition));
1084         $dataitems = Array();
1085         foreach($databasedataitems as $id => $dataitem  ){
1086         if (!isset($dataitems[$dataitem->itemnumber])){
1087                     $dataitems[$dataitem->itemnumber] = $dataitem ;
1088                   }else {
1089                         // deleting the unused records could be added here
1090                   }
1091         }
1092         ksort($dataitems);
1093         return $dataitems ;
1094     }
1096     function save_dataset_items($question, $fromform){
1097         global $CFG, $DB;
1098         // max datasets = 100 items
1099         $max100 = CALCULATEDQUESTIONMAXITEMNUMBER ;
1100         $synchronize = false ;
1101         if(isset($fromform->nextpageparam["forceregeneration"])) {
1102             $regenerate = $fromform->nextpageparam["forceregeneration"];
1103         }else{
1104             $regenerate = 0 ;
1105         }
1106         if (empty($question->options)) {
1107             $this->get_question_options($question);
1108         }
1109         if(!empty($question->options->synchronize)){
1110             $synchronize = true ;
1111         }
1114         //get the old datasets for this question
1115         $datasetdefs = $this->get_dataset_definitions($question->id, array());
1116         // Handle generator options...
1117         $olddatasetdefs = fullclone($datasetdefs);
1118         $datasetdefs = $this->update_dataset_options($datasetdefs, $fromform);
1119         $maxnumber = -1;
1120         foreach ($datasetdefs as $defid => $datasetdef) {
1121             if (isset($datasetdef->id)
1122              && $datasetdef->options != $olddatasetdefs[$defid]->options) {
1123                 // Save the new value for options
1124                 $DB->update_record('question_dataset_definitions', $datasetdef);
1126             }
1127             // Get maxnumber
1128             if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) {
1129                 $maxnumber = $datasetdef->itemcount;
1130             }
1131         }
1132         // Handle adding and removing of dataset items
1133         $i = 1;
1134         if ($maxnumber > CALCULATEDQUESTIONMAXITEMNUMBER ){
1135             $maxnumber = CALCULATEDQUESTIONMAXITEMNUMBER ;
1136         }
1138         ksort($fromform->definition);
1139         foreach ($fromform->definition as $key => $defid) {
1140             //if the delete button has not been pressed then skip the datasetitems
1141             //in the 'add item' part of the form.
1142             if ( $i > count($datasetdefs)*$maxnumber ) {
1143                 break;
1144             }
1145             $addeditem = new stdClass();
1146             $addeditem->definition = $datasetdefs[$defid]->id;
1147             $addeditem->value = $fromform->number[$i];
1148             $addeditem->itemnumber = ceil($i / count($datasetdefs));
1150             if ($fromform->itemid[$i]) {
1151                 // Reuse any previously used record
1152                 $addeditem->id = $fromform->itemid[$i];
1153                 $DB->update_record('question_dataset_items', $addeditem);
1154             } else {
1155                 $DB->insert_record('question_dataset_items', $addeditem);
1156             }
1158             $i++;
1159         }
1160         if (isset($addeditem->itemnumber) && $maxnumber < $addeditem->itemnumber
1161                 && $addeditem->itemnumber < CALCULATEDQUESTIONMAXITEMNUMBER ){
1162             $maxnumber = $addeditem->itemnumber;
1163             foreach ($datasetdefs as $key => $newdef) {
1164                 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
1165                     $newdef->itemcount = $maxnumber;
1166                     // Save the new value for options
1167                     $DB->update_record('question_dataset_definitions', $newdef);
1168                 }
1169             }
1170         }
1171         // adding supplementary items
1172         $numbertoadd =0;
1173         if (isset($fromform->addbutton) && $fromform->selectadd > 1 && $maxnumber < CALCULATEDQUESTIONMAXITEMNUMBER ) {
1174             $numbertoadd =$fromform->selectadd ;
1175             if ( $max100 - $maxnumber < $numbertoadd ) {
1176                 $numbertoadd = $max100 - $maxnumber ;
1177             }
1178             //add the other items.
1179             // Generate a new dataset item (or reuse an old one)
1180             foreach ($datasetdefs as $defid => $datasetdef) {
1181                 // in case that for category datasets some new items has been added
1182                 // get actual values
1183                 // fix regenerate for this datadefs
1184                 $defregenerate = 0 ;
1185                 if($synchronize && !empty ($fromform->nextpageparam["datasetregenerate[$datasetdef->name"])) {
1186                     $defregenerate = 1 ;
1187                 }else if(!$synchronize && (($regenerate == 1 && $datasetdef->category == 0) ||$regenerate == 2 )){
1188                     $defregenerate = 1 ;
1189                 }
1190                 if (isset($datasetdef->id)) {
1191                         $datasetdefs[$defid]->items = $this->get_database_dataset_items($datasetdef->id);
1192                 }
1193                 for ($numberadded =$maxnumber+1 ; $numberadded <= $maxnumber+$numbertoadd ; $numberadded++){
1194                     if (isset($datasetdefs[$defid]->items[$numberadded])  ){
1195                                 // in case of regenerate it modifies the already existing record
1196                                 if ( $defregenerate  ) {
1197                                         $datasetitem = new stdClass;
1198                                         $datasetitem->id = $datasetdefs[$defid]->items[$numberadded]->id;
1199                                         $datasetitem->definition = $datasetdef->id ;
1200                                         $datasetitem->itemnumber = $numberadded;
1201                                                 $datasetitem->value = $this->generate_dataset_item($datasetdef->options);
1202                                                                 $DB->update_record('question_dataset_items', $datasetitem);
1203                                                       }
1204                                               //if not regenerate do nothing as there is already a record
1205                     } else {
1206                         $datasetitem = new stdClass;
1207                         $datasetitem->definition = $datasetdef->id ;
1208                         $datasetitem->itemnumber = $numberadded;
1209                         if ($this->supports_dataset_item_generation()) {
1210                             $datasetitem->value = $this->generate_dataset_item($datasetdef->options);
1211                         } else {
1212                             $datasetitem->value = '';
1213                         }
1214                         $DB->insert_record('question_dataset_items', $datasetitem);
1215                     }
1216                 }//for number added
1217             }// datasetsdefs end
1218             $maxnumber += $numbertoadd ;
1219             foreach ($datasetdefs as $key => $newdef) {
1220                 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
1221                     $newdef->itemcount = $maxnumber;
1222                     // Save the new value for options
1223                     $DB->update_record('question_dataset_definitions', $newdef);
1224                 }
1225             }
1226         }
1228         if (isset($fromform->deletebutton))  {
1229             if(isset($fromform->selectdelete)) $newmaxnumber = $maxnumber-$fromform->selectdelete ;
1230             else $newmaxnumber = $maxnumber-1 ;
1231             if ($newmaxnumber < 0 ) $newmaxnumber = 0 ;
1232             foreach ($datasetdefs as $datasetdef) {
1233                 if ($datasetdef->itemcount == $maxnumber) {
1234                     $datasetdef->itemcount= $newmaxnumber ;
1235                     $DB->update_record('question_dataset_definitions', $datasetdef);
1236                 }
1237             }
1238        }
1239     }
1240     function generate_dataset_item($options) {
1241         if (!preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
1242                 $options, $regs)) {
1243             // Unknown options...
1244             return false;
1245         }
1246         if ($regs[1] == 'uniform') {
1247             $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
1248             return sprintf("%.".$regs[4]."f",$nbr);
1250         } else if ($regs[1] == 'loguniform') {
1251             $log0 = log(abs($regs[2])); // It would have worked the other way to
1252             $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
1253             return sprintf("%.".$regs[4]."f",$nbr);
1255         } else {
1256             print_error('disterror', 'question', '', $regs[1]);
1257         }
1258         return '';
1259     }
1261     function comment_header($question) {
1262         //$this->get_question_options($question);
1263         $strheader = '';
1264         $delimiter = '';
1266         $answers = $question->options->answers;
1268         foreach ($answers as $key => $answer) {
1269             if (is_string($answer)) {
1270                 $strheader .= $delimiter.$answer;
1271             } else {
1272                 $strheader .= $delimiter.$answer->answer;
1273             }
1274                 $delimiter = '<br/><br/><br/>';
1275         }
1276         return $strheader;
1277     }
1279     function comment_on_datasetitems($qtypeobj,$questionid,$questiontext, $answers,$data, $number) {
1280         global $DB, $QTYPES;
1281         $comment = new stdClass;
1282         $comment->stranswers = array();
1283         $comment->outsidelimit = false ;
1284         $comment->answers = array();
1285         /// Find a default unit:
1286         if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units', array('question'=> $questionid, 'multiplier' => 1.0))) {
1287             $unit = $unit->unit;
1288         } else {
1289             $unit = '';
1290         }
1292         $answers = fullclone($answers);
1293         $strmin = get_string('min', 'quiz');
1294         $strmax = get_string('max', 'quiz');
1295         $errors = '';
1296         $delimiter = ': ';
1297         $virtualqtype =  $qtypeobj->get_virtual_qtype();//& $QTYPES['numerical'];
1298         foreach ($answers as $key => $answer) {
1299             $formula = $this->substitute_variables($answer->answer,$data);
1300             $formattedanswer = qtype_calculated_calculate_answer(
1301                     $answer->answer, $data, $answer->tolerance,
1302                     $answer->tolerancetype, $answer->correctanswerlength,
1303                     $answer->correctanswerformat, $unit);
1304                     if ( $formula === '*'){
1305                         $answer->min = ' ';
1306                         $formattedanswer->answer = $answer->answer ;
1307                     }else {
1308                         eval('$answer->answer = '.$formula.';') ;
1309                         $virtualqtype->get_tolerance_interval($answer);
1310                     }
1311             if ($answer->min === '') {
1312                 // This should mean that something is wrong
1313                 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
1314             } else if ($formula === '*'){
1315                 $comment->stranswers[$key] = $formula.' = '.get_string('anyvalue','qtype_calculated').'<br/><br/><br/>';
1316             }else{
1317                 $comment->stranswers[$key]= $formula.' = '.$formattedanswer->answer.'<br/>' ;
1318                 $correcttrue->correct = $formattedanswer->answer ;
1319                 $correcttrue->true = $answer->answer ;
1320                 if ($formattedanswer->answer < $answer->min || $formattedanswer->answer > $answer->max){
1321                     $comment->outsidelimit = true ;
1322                     $comment->answers[$key] = $key;
1323                     $comment->stranswers[$key] .=get_string('trueansweroutsidelimits','qtype_calculated',$correcttrue);//<span class="error">ERROR True answer '..' outside limits</span>';
1324                 }else {
1325                     $comment->stranswers[$key] .=get_string('trueanswerinsidelimits','qtype_calculated',$correcttrue);//' True answer :'.$calculated->trueanswer.' inside limits';
1326                 }
1327                 $comment->stranswers[$key] .='<br/>';
1328                 $comment->stranswers[$key] .= $strmin.$delimiter.$answer->min.' --- ';
1329                 $comment->stranswers[$key] .= $strmax.$delimiter.$answer->max;
1330                 $comment->stranswers[$key] .='';
1331             }
1332         }
1333         return fullclone($comment);
1334     }
1335     function multichoice_comment_on_datasetitems($questionid,$questiontext, $answers,$data, $number) {
1336         global $DB;
1337         $comment = new stdClass;
1338         $comment->stranswers = array();
1339         $comment->outsidelimit = false ;
1340         $comment->answers = array();
1341         /// Find a default unit:
1342         if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units', array('question'=> $questionid, 'multiplier' => 1.0))) {
1343             $unit = $unit->unit;
1344         } else {
1345             $unit = '';
1346         }
1348         $answers = fullclone($answers);
1349         $strmin = get_string('min', 'quiz');
1350         $strmax = get_string('max', 'quiz');
1351         $errors = '';
1352         $delimiter = ': ';
1353         foreach ($answers as $key => $answer) {
1354                 $answer->answer = $this->substitute_variables($answer->answer, $data);
1355                 //evaluate the equations i.e {=5+4)
1356                 $qtext = "";
1357                 $qtextremaining = $answer->answer ;
1358                 while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
1359                     $qtextsplits = explode($regs1[0], $qtextremaining, 2);
1360                     $qtext =$qtext.$qtextsplits[0];
1361                     $qtextremaining = $qtextsplits[1];
1362                     if (empty($regs1[1])) {
1363                             $str = '';
1364                         } else {
1365                             if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){
1366                                 $str=$formulaerrors ;
1367                             }else {
1368                                 eval('$str = '.$regs1[1].';');
1370                        $texteval= qtype_calculated_calculate_answer(
1371                      $str, $data, $answer->tolerance,
1372                      $answer->tolerancetype, $answer->correctanswerlength,
1373                         $answer->correctanswerformat, '');
1374                         $str = $texteval->answer;
1376                             }
1377                         }
1378                         $qtext = $qtext.$str ;
1379                 }
1380                 $answer->answer = $qtext.$qtextremaining ; ;
1381                 $comment->stranswers[$key]= $answer->answer ;
1384           /*  $formula = $this->substitute_variables($answer->answer,$data);
1385             $formattedanswer = qtype_calculated_calculate_answer(
1386                     $answer->answer, $data, $answer->tolerance,
1387                     $answer->tolerancetype, $answer->correctanswerlength,
1388                     $answer->correctanswerformat, $unit);
1389                     if ( $formula === '*'){
1390                         $answer->min = ' ';
1391                         $formattedanswer->answer = $answer->answer ;
1392                     }else {
1393                         eval('$answer->answer = '.$formula.';') ;
1394                         $virtualqtype->get_tolerance_interval($answer);
1395                     }
1396             if ($answer->min === '') {
1397                 // This should mean that something is wrong
1398                 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
1399             } else if ($formula === '*'){
1400                 $comment->stranswers[$key] = $formula.' = '.get_string('anyvalue','qtype_calculated').'<br/><br/><br/>';
1401             }else{
1402                 $comment->stranswers[$key]= $formula.' = '.$formattedanswer->answer.'<br/>' ;
1403                 $comment->stranswers[$key] .= $strmin. $delimiter.$answer->min.'---';
1404                 $comment->stranswers[$key] .= $strmax.$delimiter.$answer->max;
1405                 $comment->stranswers[$key] .='<br/>';
1406                 $correcttrue->correct = $formattedanswer->answer ;
1407                 $correcttrue->true = $answer->answer ;
1408                 if ($formattedanswer->answer < $answer->min || $formattedanswer->answer > $answer->max){
1409                     $comment->outsidelimit = true ;
1410                     $comment->answers[$key] = $key;
1411                     $comment->stranswers[$key] .=get_string('trueansweroutsidelimits','qtype_calculated',$correcttrue);//<span class="error">ERROR True answer '..' outside limits</span>';
1412                 }else {
1413                     $comment->stranswers[$key] .=get_string('trueanswerinsidelimits','qtype_calculated',$correcttrue);//' True answer :'.$calculated->trueanswer.' inside limits';
1414                 }
1415                 $comment->stranswers[$key] .='';
1416             }*/
1417         }
1418         return fullclone($comment);
1419     }
1421     function tolerance_types() {
1422         return array('1'  => get_string('relative', 'quiz'),
1423                      '2'  => get_string('nominal', 'quiz'),
1424                      '3'  => get_string('geometric', 'quiz'));
1425     }
1427     function dataset_options($form, $name, $mandatory=true,$renameabledatasets=false) {
1428     // Takes datasets from the parent implementation but
1429     // filters options that are currently not accepted by calculated
1430     // It also determines a default selection...
1431     //$renameabledatasets not implemented anmywhere
1432         list($options, $selected) = $this->dataset_options_from_database($form, $name,'','qtype_calculated');
1433   //  list($options, $selected) = $this->dataset_optionsa($form, $name);
1435         foreach ($options as $key => $whatever) {
1436             if (!preg_match('~^1-~', $key) && $key != '0') {
1437                 unset($options[$key]);
1438             }
1439         }
1440         if (!$selected) {
1441             if ($mandatory){
1442             $selected =  "1-0-$name"; // Default
1443             }else {
1444                 $selected = "0"; // Default
1445             }
1446         }
1447         return array($options, $selected);
1448     }
1450     function construct_dataset_menus($form, $mandatorydatasets,
1451                                      $optionaldatasets) {
1452         global $OUTPUT;
1453         $datasetmenus = array();
1454         foreach ($mandatorydatasets as $datasetname) {
1455             if (!isset($datasetmenus[$datasetname])) {
1456                 list($options, $selected) =
1457                         $this->dataset_options($form, $datasetname);
1458                 unset($options['0']); // Mandatory...
1459                 $datasetmenus[$datasetname] = html_writer::select($options, 'dataset[]', $selected, null);
1460             }
1461         }
1462         foreach ($optionaldatasets as $datasetname) {
1463             if (!isset($datasetmenus[$datasetname])) {
1464                 list($options, $selected) =
1465                         $this->dataset_options($form, $datasetname);
1466                 $datasetmenus[$datasetname] = html_writer::select($options, 'dataset[]', $selected, null);
1467             }
1468         }
1469         return $datasetmenus;
1470     }
1472     function print_question_grading_details(&$question, &$state, &$cmoptions, &$options) {
1473         $virtualqtype = $this->get_virtual_qtype();
1474         $virtualqtype->print_question_grading_details($question, $state, $cmoptions, $options) ;
1475     }
1477     function get_correct_responses(&$question, &$state) {
1478         // virtual type for printing
1479         $virtualqtype = $this->get_virtual_qtype();
1480         $unit = $this->get_default_numerical_unit($question,$virtualqtype);
1481         // We modify the question to look like a numerical question
1482             $this->convert_answers($question, $state);
1483             return $virtualqtype->get_correct_responses($question, $state) ;
1484     }
1486     function substitute_variables($str, $dataset) {
1487         global $OUTPUT ;
1488           //  testing for wrong numerical values
1489           // all calculations used this function so testing here should be OK
1491         foreach ($dataset as $name => $value) {
1492             $val = $value ;
1493             if(! is_numeric($val)){
1494                 $a = new stdClass;
1495                 $a->name = '{'.$name.'}' ;
1496                 $a->value = $value ;
1497                     echo $OUTPUT->notification(get_string('notvalidnumber','qtype_calculated',$a));
1498                     $val = 1.0 ;
1499             }
1500             if($val < 0 ){
1501                 $str = str_replace('{'.$name.'}', '('.$val.')', $str);
1502             } else {
1503                 $str = str_replace('{'.$name.'}', $val, $str);
1504             }
1505         }
1506         return $str;
1507     }
1508     function evaluate_equations($str, $dataset){
1509         $formula = $this->substitute_variables($str, $dataset) ;
1510        if ($error = qtype_calculated_find_formula_errors($formula)) {
1511             return $error;
1512         }
1513         return $str;
1514     }
1517     function substitute_variables_and_eval($str, $dataset) {
1518         $formula = $this->substitute_variables($str, $dataset) ;
1519        if ($error = qtype_calculated_find_formula_errors($formula)) {
1520             return $error;
1521         }
1522         /// Calculate the correct answer
1523         if (empty($formula)) {
1524             $str = '';
1525         } else if ($formula === '*'){
1526             $str = '*';
1527         } else {
1528             eval('$str = '.$formula.';');
1529         }
1530         return $str;
1531     }
1533     function get_dataset_definitions($questionid, $newdatasets) {
1534         global $DB;
1535         //get the existing datasets for this question
1536         $datasetdefs = array();
1537         if (!empty($questionid)) {
1538             global $CFG;
1539             $sql = "SELECT i.*
1540                     FROM {question_datasets} d,
1541                          {question_dataset_definitions} i
1542                     WHERE d.question = ?
1543                     AND   d.datasetdefinition = i.id
1544                    ";
1545             if ($records = $DB->get_records_sql($sql, array($questionid))) {
1546                 foreach ($records as $r) {
1547                     $datasetdefs["$r->type-$r->category-$r->name"] = $r;
1548                 }
1549             }
1550         }
1552         foreach ($newdatasets as $dataset) {
1553             if (!$dataset) {
1554                 continue; // The no dataset case...
1555             }
1557             if (!isset($datasetdefs[$dataset])) {
1558                 //make new datasetdef
1559                 list($type, $category, $name) = explode('-', $dataset, 3);
1560                 $datasetdef = new stdClass;
1561                 $datasetdef->type = $type;
1562                 $datasetdef->name = $name;
1563                 $datasetdef->category  = $category;
1564                 $datasetdef->itemcount = 0;
1565                 $datasetdef->options   = 'uniform:1.0:10.0:1';
1566                 $datasetdefs[$dataset] = clone($datasetdef);
1567             }
1568         }
1569         return $datasetdefs;
1570     }
1572     function save_dataset_definitions($form) {
1573         global $DB;
1574         // save synchronize
1576         // Save datasets
1577         $datasetdefinitions = $this->get_dataset_definitions($form->id, $form->dataset);
1578         $tmpdatasets = array_flip($form->dataset);
1579         $defids = array_keys($datasetdefinitions);
1580         foreach ($defids as $defid) {
1581             $datasetdef = &$datasetdefinitions[$defid];
1582             if (isset($datasetdef->id)) {
1583                 if (!isset($tmpdatasets[$defid])) {
1584                 // This dataset is not used any more, delete it
1585                     $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $datasetdef->id));
1586                     if ($datasetdef->category == 0) { // Question local dataset
1587                         $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
1588                         $DB->delete_records('question_dataset_items', array('definition' => $datasetdef->id));
1589                     }
1590                 }
1591                 // This has already been saved or just got deleted
1592                 unset($datasetdefinitions[$defid]);
1593                 continue;
1594             }
1596             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
1598             if (0 != $datasetdef->category) {
1599                 // We need to look for already existing
1600                 // datasets in the category.
1601                 // By first creating the datasetdefinition above we
1602                 // can manage to automatically take care of
1603                 // some possible realtime concurrence
1604                 if ($olderdatasetdefs = $DB->get_records_select( 'question_dataset_definitions',
1605                         "type = ?
1606                         AND name = ?
1607                         AND category = ?
1608                         AND id < ?
1609                         ORDER BY id DESC", array($datasetdef->type, $datasetdef->name, $datasetdef->category, $datasetdef->id))) {
1611                     while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1612                         $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
1613                         $datasetdef = $olderdatasetdef;
1614                     }
1615                 }
1616             }
1618             // Create relation to this dataset:
1619             $questiondataset = new stdClass;
1620             $questiondataset->question = $form->id;
1621             $questiondataset->datasetdefinition = $datasetdef->id;
1622             $DB->insert_record('question_datasets', $questiondataset);
1623             unset($datasetdefinitions[$defid]);
1624         }
1626         // Remove local obsolete datasets as well as relations
1627         // to datasets in other categories:
1628         if (!empty($datasetdefinitions)) {
1629             foreach ($datasetdefinitions as $def) {
1630                 $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $def->id));
1632                 if ($def->category == 0) { // Question local dataset
1633                     $DB->delete_records('question_dataset_definitions', array('id' => $def->id));
1634                     $DB->delete_records('question_dataset_items', array('definition' => $def->id));
1635                 }
1636             }
1637         }
1638     }
1639     /** This function create a copy of the datasets ( definition and dataitems)
1640     * from the preceding question if they remain in the new question
1641     * otherwise its create the datasets that have been added as in the
1642     * save_dataset_definitions()
1643     */
1644     function save_as_new_dataset_definitions($form, $initialid) {
1645     global $CFG, $DB;
1646         // Get the datasets from the intial question
1647         $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset);
1648         // $tmpdatasets contains those of the new question
1649         $tmpdatasets = array_flip($form->dataset);
1650         $defids = array_keys($datasetdefinitions);// new datasets
1651         foreach ($defids as $defid) {
1652             $datasetdef = &$datasetdefinitions[$defid];
1653             if (isset($datasetdef->id)) {
1654                 // This dataset exist in the initial question
1655                 if (!isset($tmpdatasets[$defid])) {
1656                     // do not exist in the new question so ignore
1657                     unset($datasetdefinitions[$defid]);
1658                     continue;
1659                 }
1660                 // create a copy but not for category one
1661                 if (0 == $datasetdef->category) {
1662                    $olddatasetid = $datasetdef->id ;
1663                    $olditemcount = $datasetdef->itemcount ;
1664                    $datasetdef->itemcount =0;
1665                    $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
1666                    //copy the dataitems
1667                    $olditems = $this->get_database_dataset_items($olddatasetid);
1668                    if (count($olditems) > 0 ) {
1669                         $itemcount = 0;
1670                         foreach($olditems as $item ){
1671                             $item->definition = $datasetdef->id;
1672                             $DB->insert_record('question_dataset_items', $item);
1673                             $itemcount++;
1674                         }
1675                         //update item count to olditemcount if
1676                         // at least this number of items has been recover from the database
1677                         if( $olditemcount <= $itemcount ) {
1678                             $datasetdef->itemcount = $olditemcount;
1679                         } else {
1680                             $datasetdef->itemcount = $itemcount ;
1681                         }
1682                         $DB->update_record('question_dataset_definitions', $datasetdef);
1683                     } // end of  copy the dataitems
1684                 }// end of  copy the datasetdef
1685                 // Create relation to the new question with this
1686                 // copy as new datasetdef from the initial question
1687                 $questiondataset = new stdClass;
1688                 $questiondataset->question = $form->id;
1689                 $questiondataset->datasetdefinition = $datasetdef->id;
1690                 $DB->insert_record('question_datasets', $questiondataset);
1691                 unset($datasetdefinitions[$defid]);
1692                 continue;
1693             }// end of datasetdefs from the initial question
1694             // really new one code similar to save_dataset_definitions()
1695             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
1697             if (0 != $datasetdef->category) {
1698                 // We need to look for already existing
1699                 // datasets in the category.
1700                 // By first creating the datasetdefinition above we
1701                 // can manage to automatically take care of
1702                 // some possible realtime concurrence
1703                 if ($olderdatasetdefs = $DB->get_records_select(
1704                         'question_dataset_definitions',
1705                         "type = ?
1706                         AND name = ?
1707                         AND category = ?
1708                         AND id < ?
1709                         ORDER BY id DESC", array($datasetdef->type, $datasetdef->name, $datasetdef->category, $datasetdef->id))) {
1711                     while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1712                         $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id));
1713                         $datasetdef = $olderdatasetdef;
1714                     }
1715                 }
1716             }
1718             // Create relation to this dataset:
1719             $questiondataset = new stdClass;
1720             $questiondataset->question = $form->id;
1721             $questiondataset->datasetdefinition = $datasetdef->id;
1722             $DB->insert_record('question_datasets', $questiondataset);
1723             unset($datasetdefinitions[$defid]);
1724         }
1726         // Remove local obsolete datasets as well as relations
1727         // to datasets in other categories:
1728         if (!empty($datasetdefinitions)) {
1729             foreach ($datasetdefinitions as $def) {
1730                 $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $def->id));
1732                 if ($def->category == 0) { // Question local dataset
1733                     $DB->delete_records('question_dataset_definitions', array('id' => $def->id));
1734                     $DB->delete_records('question_dataset_items', array('definition' => $def->id));
1735                 }
1736             }
1737         }
1738     }
1740 /// Dataset functionality
1741     function pick_question_dataset($question, $datasetitem) {
1742         // Select a dataset in the following format:
1743         // An array indexed by the variable names (d.name) pointing to the value
1744         // to be substituted
1745         global $CFG, $DB;
1746         if (!$dataitems = $DB->get_records_sql(
1747                         "SELECT i.id, d.name, i.value
1748                         FROM {question_dataset_definitions} d,
1749                              {question_dataset_items} i,
1750                              {question_datasets} q
1751                         WHERE q.question = ?
1752                         AND q.datasetdefinition = d.id
1753                         AND d.id = i.definition
1754                         AND i.itemnumber = ? ORDER by i.id DESC ", array($question->id, $datasetitem))) {
1755             print_error('cannotgetdsfordependent', 'question', '', array($question->id, $datasetitem));
1756         }
1757         $dataset = Array();
1758         foreach($dataitems as $id => $dataitem  ){
1759                 if (!isset($dataset[$dataitem->name])){
1760                             $dataset[$dataitem->name] = $dataitem->value ;
1761                           }else {
1762                                 // deleting the unused records could be added here
1763                           }
1764         }
1765         return $dataset;
1766     }
1768     function dataset_options_from_database($form, $name,$prefix='',$langfile='quiz') {
1769         global $CFG, $DB;
1770         $type = 1 ; // only type = 1 (i.e. old 'LITERAL') has ever been used
1772         // First options - it is not a dataset...
1773         $options['0'] = get_string($prefix.'nodataset', $langfile);
1774         // new question no local
1775         if (!isset($form->id) || $form->id == 0 ){
1776             $key = "$type-0-$name";
1777             $options[$key] = get_string($prefix."newlocal$type", $langfile);
1778             $currentdatasetdef = new stdClass;
1779             $currentdatasetdef->type = '0';
1780         }else {
1782         // Construct question local options
1783         if ( ! $currentdatasetdef = $DB->get_record_sql(
1784                 "SELECT a.*
1785                    FROM {question_dataset_definitions} a,
1786                         {question_datasets} b
1787                   WHERE a.id = b.datasetdefinition
1788                     AND a.type = '1'
1789                     AND b.question = ?
1790                     AND a.name = ?", array($form->id, $name))){
1791             $currentdatasetdef->type = '0';
1792          };
1793         $key = "$type-0-$name";
1794         if ($currentdatasetdef->type == $type
1795                 and $currentdatasetdef->category == 0) {
1796             $options[$key] = get_string($prefix."keptlocal$type", $langfile);
1797         } else {
1798             $options[$key] = get_string($prefix."newlocal$type", $langfile);
1799         }
1800         }
1801         // Construct question category options
1802         $categorydatasetdefs = $DB->get_records_sql(
1803                 "SELECT b.question, a.*
1804                    FROM {question_datasets} b,
1805                         {question_dataset_definitions} a
1806                   WHERE a.id = b.datasetdefinition
1807                     AND a.type = '1'
1808                     AND a.category = ?
1809                     AND a.name = ?", array($form->category, $name));
1810         $type = 1 ;
1811         $key = "$type-$form->category-$name";
1812         if (!empty($categorydatasetdefs)){ // there is at least one with the same name
1813             if (isset($form->id) && isset($categorydatasetdefs[$form->id])) {// it is already used by this question
1814                     $options[$key] = get_string($prefix."keptcategory$type", $langfile);
1815                 } else {
1816                     $options[$key] = get_string($prefix."existingcategory$type", $langfile);
1817                 }
1818         } else {
1819             $options[$key] = get_string($prefix."newcategory$type", $langfile);
1820         }
1821         // All done!
1822         return array($options, $currentdatasetdef->type
1823                 ? "$currentdatasetdef->type-$currentdatasetdef->category-$name"
1824                 : '');
1825     }
1827     function find_dataset_names($text) {
1828     /// Returns the possible dataset names found in the text as an array
1829     /// The array has the dataset name for both key and value
1830         $datasetnames = array();
1831         while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) {
1832             $datasetnames[$regs[1]] = $regs[1];
1833             $text = str_replace($regs[0], '', $text);
1834         }
1835         return $datasetnames;
1836     }
1838     /**
1839     * This function retrieve the item count of the available category shareable
1840     * wild cards that is added as a comment displayed when a wild card with
1841     * the same name is displayed in datasetdefinitions_form.php
1842     */
1843     function get_dataset_definitions_category($form) {
1844         global $CFG, $DB;
1845         $datasetdefs = array();
1846         $lnamemax = 30;
1847         if (!empty($form->category)) {
1848             $sql = "SELECT i.*,d.*
1849                     FROM {question_datasets} d,
1850                          {question_dataset_definitions} i
1851                   WHERE i.id = d.datasetdefinition
1852                     AND i.category = ?";
1853              if ($records = $DB->get_records_sql($sql, array($form->category))) {
1854                    foreach ($records as $r) {
1855                        if ( !isset ($datasetdefs["$r->name"])) $datasetdefs["$r->name"] = $r->itemcount;
1856                     }
1857                 }
1858         }
1859         return  $datasetdefs ;
1860     }
1862     /**
1863     * This function build a table showing the available category shareable
1864     * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1865     * and the name of the question where they are used.
1866     * This table is intended to be add before the question text to help the user use
1867     * these wild cards
1868     */
1870     function print_dataset_definitions_category($form) {
1871         global $CFG, $DB;
1872         $datasetdefs = array();
1873         $lnamemax = 22;
1874         $namestr =get_string('name', 'quiz');
1875         $minstr=get_string('min', 'quiz');
1876         $maxstr=get_string('max', 'quiz');
1877         $rangeofvaluestr=get_string('minmax','qtype_calculated');
1878         $questionusingstr = get_string('usedinquestion','qtype_calculated');
1879         $itemscountstr = get_string('itemscount','qtype_calculated');
1880        $text ='';
1881         if (!empty($form->category)) {
1882             list($category) = explode(',', $form->category);
1883             $sql = "SELECT i.*,d.*
1884                     FROM {question_datasets} d,
1885                          {question_dataset_definitions} i
1886                     WHERE i.id = d.datasetdefinition
1887                     AND i.category = ?";
1888             if ($records = $DB->get_records_sql($sql, array($category))) {
1889                 foreach ($records as $r) {
1890                     $sql1 = "SELECT q.*
1891                         FROM  {question} q
1892                              WHERE q.id = ?";
1893                     if ( !isset ($datasetdefs["$r->type-$r->category-$r->name"])){
1894                         $datasetdefs["$r->type-$r->category-$r->name"]= $r;
1895                     }
1896                     if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
1897                         $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question]->name =$questionb[$r->question]->name ;
1898                     }
1899                 }
1900             }
1901         }
1902         if (!empty ($datasetdefs)){
1904             $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>";
1905             foreach ($datasetdefs as $datasetdef){
1906                 list($distribution, $min, $max,$dec) = explode(':', $datasetdef->options, 4);
1907                 $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\">";
1908                 foreach ($datasetdef->questions as $qu) {
1909                     //limit the name length displayed
1910                     if (!empty($qu->name)) {
1911                         $qu->name = (strlen($qu->name) > $lnamemax) ?
1912                         substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1913                     } else {
1914                         $qu->name = '';
1915                     }
1916                     $text .=" &nbsp;&nbsp; $qu->name <br/>";
1917                 }
1918                 $text .="</td></tr>";
1919             }
1920             $text .="</table>";
1921         }else{
1922              $text .=get_string('nosharedwildcard', 'qtype_calculated');
1923         }
1924         return  $text ;
1925     }
1927     /**
1928     * This function build a table showing the available category shareable
1929     * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1930     * and the name of the question where they are used.
1931     * This table is intended to be add before the question text to help the user use
1932     * these wild cards
1933     */
1935     function print_dataset_definitions_category_shared($question,$datasetdefsq) {
1936         global $CFG, $DB;
1937         $datasetdefs = array();
1938         $lnamemax = 22;
1939         $namestr =get_string('name', 'quiz');
1940         $minstr=get_string('min', 'quiz');
1941         $maxstr=get_string('max', 'quiz');
1942         $rangeofvaluestr=get_string('minmax','qtype_calculated');
1943         $questionusingstr = get_string('usedinquestion','qtype_calculated');
1944         $itemscountstr = get_string('itemscount','qtype_calculated');
1945        $text ='';
1946         if (!empty($question->category)) {
1947             list($category) = explode(',', $question->category);
1948             $sql = "SELECT i.*,d.*
1949                     FROM {question_datasets} d,
1950                          {question_dataset_definitions} i
1951                     WHERE i.id = d.datasetdefinition
1952                     AND i.category = ?";
1953             if ($records = $DB->get_records_sql($sql, array($category))) {
1954                 foreach ($records as $r) {
1955                     $sql1 = "SELECT q.*
1956                         FROM  {question} q
1957                              WHERE q.id = ?";
1958                     if ( !isset ($datasetdefs["$r->type-$r->category-$r->name"])){
1959                         $datasetdefs["$r->type-$r->category-$r->name"]= $r;
1960                     }
1961                     if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
1962                         $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question]->name =$questionb[$r->question]->name ;
1963                         $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question]->id =$questionb[$r->question]->id ;
1964                     }
1965                 }
1966             }
1967         }
1968         if (!empty ($datasetdefs)){
1970             $text ="<table width=\"100%\" border=\"1\"><tr><th  style=\"white-space:nowrap;\" class=\"header\" scope=\"col\" >$namestr</th>";
1971             $text .="<th  style=\"white-space:nowrap;\" class=\"header\" scope=\"col\">$itemscountstr</th>";
1972             $text .="<th style=\"white-space:nowrap;\" class=\"header\" scope=\"col\">&nbsp;&nbsp;$questionusingstr &nbsp;&nbsp; </th>";
1973             $text .="<th style=\"white-space:nowrap;\" class=\"header\" scope=\"col\">Quiz</th>";
1974             $text .="<th style=\"white-space:nowrap;\" class=\"header\" scope=\"col\">Attempts</th></tr>";
1975             foreach ($datasetdefs as $datasetdef){
1976                 list($distribution, $min, $max,$dec) = explode(':', $datasetdef->options, 4);
1977                 $count = count($datasetdef->questions);
1978                 $text .="<tr><td style=\"white-space:nowrap;\" valign=\"top\" align=\"center\" rowspan=\"$count\"> $datasetdef->name </td><td align=\"right\" valign=\"top\" rowspan=\"$count\" >$datasetdef->itemcount&nbsp;&nbsp;</td>";
1979                 //$text .="<td align=\"left\">";
1980                 $line = 0 ;
1981                 foreach ($datasetdef->questions as $qu) {
1982                     //limit the name length displayed
1983                     if (!empty($qu->name)) {
1984                         $qu->name = (strlen($qu->name) > $lnamemax) ?
1985                         substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1986                     } else {
1987                         $qu->name = '';
1988                     }
1989                     if( $line ) {
1990                         $text .="<tr>";
1991                     }
1992                     $line++;
1993                     $text .="<td align=\"left\" style=\"white-space:nowrap;\" >$qu->name</td>";
1994                  $nb_of_quiz = 0;
1995                  $nb_of_attempts=0;
1996                  $used_in_quiz = false ;
1997                  if ($list = $DB->get_records('quiz_question_instances', array( 'question'=> $qu->id))){
1998                             $used_in_quiz = true;
1999                     foreach($list as $key => $li){
2000                         $nb_of_quiz ++;
2001                         if($att = $DB->get_records('quiz_attempts',array( 'quiz'=> $li->quiz, 'preview'=> '0'))){
2002                             $nb_of_attempts+= count($att);
2003                         }
2004                     }
2005                   }
2006                   if($used_in_quiz){
2007                     $text .="<td align=\"center\">$nb_of_quiz</td>";
2008                 }else {
2009                     $text .="<td align=\"center\">0</td>";
2010                 }
2011                   if($used_in_quiz){
2012                     $text .="<td align=\"center\">$nb_of_attempts";
2013                 }else {
2014                      $text .="<td align=\"left\"><br/>";
2015                 }
2017                 $text .="</td></tr>";
2018                 }
2019             }
2020             $text .="</table>";
2021         }else{
2022              $text .=get_string('nosharedwildcard', 'qtype_calculated');
2023         }
2024         return  $text ;
2025     }
2027     function find_math_equations($text) {
2028     /// Returns the possible dataset names found in the text as an array
2029     /// The array has the dataset name for both key and value
2030         $equations = array();
2031  /*               $qtext = "";
2032         $qtextremaining = $numericalquestion->questiontext ;
2033         while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
2034       //  while  (preg_match('~\{=|%=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
2035             $qtextsplits = explode($regs1[0], $qtextremaining, 2);
2036             $qtext =$qtext.$qtextsplits[0];
2037             $qtextremaining = $qtextsplits[1];
2038             if (empty($regs1[1])) {
2039                     $str = '';
2040                 } else {
2041 */
2042         while (preg_match('~\{=([^[:space:]}]*)}~', $text, $regs)) {
2043             $equations[] = $regs[1];
2044             $text = str_replace($regs[0], '', $text);
2045         }
2046         return $equations;
2047     }
2049     function get_virtual_qtype() {
2050         global $QTYPES;
2051             $this->virtualqtype =& $QTYPES['numerical'];
2052         return $this->virtualqtype;
2053     }
2056 /// BACKUP FUNCTIONS ////////////////////////////
2058     /*
2059      * Backup the data in the question
2060      *
2061      * This is used in question/backuplib.php
2062      */
2063     function backup($bf,$preferences,$question,$level=6) {
2064         global $DB;
2065         $status = true;
2067         $calculateds = $DB->get_records("question_calculated",array("question" =>$question,"id"));
2068         //If there are calculated-s
2069         if ($calculateds) {
2070             //Iterate over each calculateds
2071             foreach ($calculateds as $calculated) {
2072                 $status = $status &&fwrite ($bf,start_tag("CALCULATED",$level,true));
2073                 //Print calculated contents
2074                 fwrite ($bf,full_tag("ANSWER",$level+1,false,$calculated->answer));
2075                 fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$calculated->tolerance));
2076                 fwrite ($bf,full_tag("TOLERANCETYPE",$level+1,false,$calculated->tolerancetype));
2077                 fwrite ($bf,full_tag("CORRECTANSWERLENGTH",$level+1,false,$calculated->correctanswerlength));
2078                 fwrite ($bf,full_tag("CORRECTANSWERFORMAT",$level+1,false,$calculated->correctanswerformat));
2079                 //Now backup numerical_units
2080                 $status = question_backup_numerical_units($bf,$preferences,$question,7);
2081                 //Now backup required dataset definitions and items...
2082                 $status = question_backup_datasets($bf,$preferences,$question,7);
2083                 //End calculated data
2084                 $status = $status &&fwrite ($bf,end_tag("CALCULATED",$level,true));
2085             }
2086             $calculated_options = $DB->get_records("question_calculated_options",array("questionid" => $question),"id");
2087             if ($calculated_options) {
2088                 //Iterate over each calculated_option
2089                 foreach ($calculated_options as $calculated_option) {
2090                     $status = fwrite ($bf,start_tag("CALCULATED_OPTIONS",$level,true));
2091                     //Print calculated_option contents
2092                     fwrite ($bf,full_tag("SYNCHRONIZE",$level+1,false,$calculated_option->synchronize));
2093                     fwrite ($bf,full_tag("SINGLE",$level+1,false,$calculated_option->single));
2094                     fwrite ($bf,full_tag("SHUFFLEANSWERS",$level+1,false,$calculated_option->shuffleanswers));
2095                     fwrite ($bf,full_tag("CORRECTFEEDBACK",$level+1,false,$calculated_option->correctfeedback));
2096                     fwrite ($bf,full_tag("PARTIALLYCORRECTFEEDBACK",$level+1,false,$calculated_option->partiallycorrectfeedback));
2097                     fwrite ($bf,full_tag("INCORRECTFEEDBACK",$level+1,false,$calculated_option->incorrectfeedback));
2098                     fwrite ($bf,full_tag("ANSWERNUMBERING",$level+1,false,$calculated_option->answernumbering));
2099                     $status = fwrite ($bf,end_tag("CALCULATED_OPTIONS",$level,true));
2100                 }
2101             }
2102             $status = question_backup_numerical_options($bf,$preferences,$question,$level);
2104         }
2105         return $status;
2106     }
2108 /// RESTORE FUNCTIONS /////////////////
2110     /*
2111      * Restores the data in the question
2112      *
2113      * This is used in question/restorelib.php
2114      */
2115     function restore($old_question_id,$new_question_id,$info,$restore) {
2116         global $DB;
2118         $status = true;
2120         //Get the calculated-s array
2121         $calculateds = $info['#']['CALCULATED'];
2123         //Iterate over calculateds
2124         for($i = 0; $i < sizeof($calculateds); $i++) {
2125             $cal_info = $calculateds[$i];
2126             //traverse_xmlize($cal_info);                                                                 //Debug
2127             //print_object ($GLOBALS['traverse_array']);                                                  //Debug
2128             //$GLOBALS['traverse_array']="";                                                              //Debug
2130             //Now, build the question_calculated record structure
2131             $calculated->question = $new_question_id;
2132             $calculated->answer = backup_todb($cal_info['#']['ANSWER']['0']['#']);
2133             $calculated->tolerance = backup_todb($cal_info['#']['TOLERANCE']['0']['#']);
2134             $calculated->tolerancetype = backup_todb($cal_info['#']['TOLERANCETYPE']['0']['#']);
2135             $calculated->correctanswerlength = backup_todb($cal_info['#']['CORRECTANSWERLENGTH']['0']['#']);
2136             $calculated->correctanswerformat = backup_todb($cal_info['#']['CORRECTANSWERFORMAT']['0']['#']);
2138             ////We have to recode the answer field
2139             $answer = backup_getid($restore->backup_unique_code,"question_answers",$calculated->answer);
2140             if ($answer) {
2141                 $calculated->answer = $answer->new_id;
2142             }
2144             //The structure is equal to the db, so insert the question_calculated
2145             $newid = $DB->insert_record ("question_calculated",$calculated);
2147             //Do some output
2148             if (($i+1) % 50 == 0) {
2149                 if (!defined('RESTORE_SILENTLY')) {
2150                     echo ".";
2151                     if (($i+1) % 1000 == 0) {
2152                         echo "<br />";
2153                     }
2154                 }
2155                 backup_flush(300);
2156             }
2157         //Get the calculated_options array
2158         // need to check as old questions don't have calculated_options record
2159         if(isset($info['#']['CALCULATED_OPTIONS'])){
2160             $calculatedoptions = $info['#']['CALCULATED_OPTIONS'];
2162             //Iterate over calculated_options
2163             for($i = 0; $i < sizeof($calculatedoptions); $i++){
2164                 $cal_info = $calculatedoptions[$i];
2165                 //traverse_xmlize($cal_info);                                                                 //Debug
2166                 //print_object ($GLOBALS['traverse_array']);                                                  //Debug
2167                 //$GLOBALS['traverse_array']="";                                                              //Debug
2169                 //Now, build the question_calculated_options record structure
2170                 $calculated_options->questionid = $new_question_id;
2171                 $calculated_options->synchronize = backup_todb($cal_info['#']['SYNCHRONIZE']['0']['#']);
2172                 $calculated_options->single = backup_todb($cal_info['#']['SINGLE']['0']['#']);
2173                 $calculated_options->shuffleanswers = isset($cal_info['#']['SHUFFLEANSWERS']['0']['#'])?backup_todb($mul_info['#']['SHUFFLEANSWERS']['0']['#']):'';
2174                 $calculated_options->correctfeedback = backup_todb($cal_info['#']['CORRECTFEEDBACK']['0']['#']);
2175                 $calculated_options->partiallycorrectfeedback = backup_todb($cal_info['#']['PARTIALLYCORRECTFEEDBACK']['0']['#']);
2176                 $calculated_options->incorrectfeedback = backup_todb($cal_info['#']['INCORRECTFEEDBACK']['0']['#']);
2177                 $calculated_options->answernumbering = backup_todb($cal_info['#']['ANSWERNUMBERING']['0']['#']);
2179                 //The structure is equal to the db, so insert the question_calculated_options
2180                 $newid = $DB->insert_record ("question_calculated_options",$calculated_options);
2182                 //Do some output
2183                 if (($i+1) % 50 == 0) {
2184                     if (!defined('RESTORE_SILENTLY')) {
2185                         echo ".";
2186                         if (($i+1) % 1000 == 0) {
2187                             echo "<br />";
2188                         }
2189                     }
2190                     backup_flush(300);
2191                 }
2192             }
2193             }
2194             //Now restore numerical_units
2195             $status = question_restore_numerical_units ($old_question_id,$new_question_id,$cal_info,$restore);
2196             $status = question_restore_numerical_options($old_question_id,$new_question_id,$info,$restore);
2197             //Now restore dataset_definitions
2198             if ($status && $newid) {
2199                 $status = question_restore_dataset_definitions ($old_question_id,$new_question_id,$cal_info,$restore);
2200             }
2202             if (!$newid) {
2203                 $status = false;
2204             }
2205         }
2207         return $status;
2208     }
2210         /**
2211    * Runs all the code required to set up and save an essay question for testing purposes.
2212    * Alternate DB table prefix may be used to facilitate data deletion.
2213    */
2214   function generate_test($name, $courseid = null) {
2215       global $DB;
2216       list($form, $question) = parent::generate_test($name, $courseid);
2217       $form->feedback = 1;
2218       $form->multiplier = array(1, 1);
2219       $form->shuffleanswers = 1;
2220       $form->noanswers = 1;
2221       $form->qtype ='calculated';
2222       $question->qtype ='calculated';
2223       $form->answers = array('{a} + {b}');
2224       $form->fraction = array(1);
2225       $form->tolerance = array(0.01);
2226       $form->tolerancetype = array(1);
2227       $form->correctanswerlength = array(2);
2228       $form->correctanswerformat = array(1);
2229       $form->questiontext = "What is {a} + {b}?";
2231       if ($courseid) {
2232           $course = $DB->get_record('course', array('id'=> $courseid));
2233       }
2235       $new_question = $this->save_question($question, $form, $course);
2237       $dataset_form = new stdClass();
2238       $dataset_form->nextpageparam["forceregeneration"]= 1;
2239       $dataset_form->calcmin = array(1 => 1.0, 2 => 1.0);
2240       $dataset_form->calcmax = array(1 => 10.0, 2 => 10.0);
2241       $dataset_form->calclength = array(1 => 1, 2 => 1);
2242       $dataset_form->number = array(1 => 5.4 , 2 => 4.9);
2243       $dataset_form->itemid = array(1 => '' , 2 => '');
2244       $dataset_form->calcdistribution = array(1 => 'uniform', 2 => 'uniform');
2245       $dataset_form->definition = array(1 => "1-0-a",
2246                                         2 => "1-0-b");
2247       $dataset_form->nextpageparam = array('forceregeneration' => false);
2248       $dataset_form->addbutton = 1;
2249       $dataset_form->selectadd = 1;
2250       $dataset_form->courseid = $courseid;
2251       $dataset_form->cmid = 0;
2252       $dataset_form->id = $new_question->id;
2253       $this->save_dataset_items($new_question, $dataset_form);
2255       return $new_question;
2256   }
2258 //// END OF CLASS ////
2260 //////////////////////////////////////////////////////////////////////////
2261 //// INITIATION - Without this line the question type is not in use... ///
2262 //////////////////////////////////////////////////////////////////////////
2263 question_register_questiontype(new question_calculated_qtype());
2265 if ( ! defined ("CALCULATEDQUESTIONMAXITEMNUMBER")) {
2266     define("CALCULATEDQUESTIONMAXITEMNUMBER", 100);
2269 function qtype_calculated_calculate_answer($formula, $individualdata,
2270         $tolerance, $tolerancetype, $answerlength, $answerformat='1', $unit='') {
2271 /// The return value has these properties:
2272 /// ->answer    the correct answer
2273 /// ->min       the lower bound for an acceptable response
2274 /// ->max       the upper bound for an accetpable response
2276     /// Exchange formula variables with the correct values...
2277     global $QTYPES;
2278     $answer = $QTYPES['calculated']->substitute_variables_and_eval($formula, $individualdata);
2279     if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */
2280         /*** Adjust to the correct number of decimals ***/
2281         if (stripos($answer,'e')>0 ){
2282             $answerlengthadd = strlen($answer)-stripos($answer,'e');
2283         }else {
2284             $answerlengthadd = 0 ;
2285         }
2286         $calculated->answer = round(floatval($answer), $answerlength+$answerlengthadd);
2288         if ($answerlength) {
2289             /* Try to include missing zeros at the end */
2291             if (preg_match('~^(.*\\.)(.*)$~', $calculated->answer, $regs)) {
2292                 $calculated->answer = $regs[1] . substr(
2293                         $regs[2] . '00000000000000000000000000000000000000000x',
2294                         0, $answerlength)
2295                         . $unit;
2296             } else {
2297                 $calculated->answer .=
2298                         substr('.00000000000000000000000000000000000000000x',
2299                         0, $answerlength + 1) . $unit;
2300             }
2301         } else {
2302             /* Attach unit */
2303             $calculated->answer .= $unit;
2304         }
2306     } else if ($answer) { // Significant figures does only apply if the result is non-zero
2308         // Convert to positive answer...
2309         if ($answer < 0) {
2310             $answer = -$answer;
2311             $sign = '-';
2312         } else {
2313             $sign = '';
2314         }
2316         // Determine the format 0.[1-9][0-9]* for the answer...
2317         $p10 = 0;
2318         while ($answer < 1) {
2319             --$p10;
2320             $answer *= 10;
2321         }
2322         while ($answer >= 1) {
2323             ++$p10;
2324             $answer /= 10;
2325         }
2326         // ... and have the answer rounded of to the correct length
2327         $answer = round($answer, $answerlength);
2329         // Have the answer written on a suitable format,
2330         // Either scientific or plain numeric
2331         if (-2 > $p10 || 4 < $p10) {
2332             // Use scientific format:
2333             $eX = 'e'.--$p10;
2334             $answer *= 10;
2335             if (1 == $answerlength) {
2336                 $calculated->answer = $sign.$answer.$eX.$unit;
2337             } else {
2338                 // Attach additional zeros at the end of $answer,
2339                 $answer .= (1==strlen($answer) ? '.' : '')
2340                         . '00000000000000000000000000000000000000000x';
2341                 $calculated->answer = $sign
2342                         .substr($answer, 0, $answerlength +1).$eX.$unit;
2343             }
2344         } else {
2345             // Stick to plain numeric format
2346             $answer *= "1e$p10";
2347             if (0.1 <= $answer / "1e$answerlength") {
2348                 $calculated->answer = $sign.$answer.$unit;
2349             } else {
2350                 // Could be an idea to add some zeros here
2351                 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '')
2352                         . '00000000000000000000000000000000000000000x';
2353                 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
2354                 $calculated->answer = $sign.substr($answer, 0, $oklen).$unit;
2355             }
2356         }
2358     } else {
2359         $calculated->answer = 0.0;
2360     }
2362     /// Return the result
2363     return $calculated;
2367 function qtype_calculated_find_formula_errors($formula) {
2368 /// Validates the formula submitted from the question edit page.
2369 /// Returns false if everything is alright.
2370 /// Otherwise it constructs an error message
2371     // Strip away dataset names
2372     while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
2373         $formula = str_replace($regs[0], '1', $formula);
2374     }
2376     // Strip away empty space and lowercase it
2377     $formula = strtolower(str_replace(' ', '', $formula));
2379     $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */
2380     $operatorornumber = "[$safeoperatorchar.0-9eE]";
2382     while ( preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~",
2383             $formula, $regs)) {
2385         switch ($regs[2]) {
2386             // Simple parenthesis
2387             case '':
2388                 if ($regs[4] || strlen($regs[3])==0) {
2389                     return get_string('illegalformulasyntax', 'quiz', $regs[0]);
2390                 }
2391                 break;
2393             // Zero argument functions
2394             case 'pi':
2395                 if ($regs[3]) {
2396                     return get_string('functiontakesnoargs', 'quiz', $regs[2]);
2397                 }
2398                 break;
2400             // Single argument functions (the most common case)
2401             case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
2402             case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
2403             case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
2404             case 'exp': case 'expm1': case 'floor': case 'is_finite':
2405             case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
2406             case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
2407             case 'tan': case 'tanh':
2408                 if (!empty($regs[4]) || empty($regs[3])) {
2409                     return get_string('functiontakesonearg','quiz',$regs[2]);
2410                 }
2411                 break;
2413             // Functions that take one or two arguments
2414             case 'log': case 'round':
2415                 if (!empty($regs[5]) || empty($regs[3])) {
2416                     return get_string('functiontakesoneortwoargs','quiz',$regs[2]);
2417                 }
2418                 break;
2420             // Functions that must have two arguments
2421             case 'atan2': case 'fmod': case 'pow':
2422                 if (!empty($regs[5]) || empty($regs[4])) {
2423                     return get_string('functiontakestwoargs', 'quiz', $regs[2]);
2424                 }
2425                 break;
2427             // Functions that take two or more arguments
2428             case 'min': case 'max':
2429                 if (empty($regs[4])) {
2430                     return get_string('functiontakesatleasttwo','quiz',$regs[2]);
2431                 }
2432                 break;
2434             default:
2435                 return get_string('unsupportedformulafunction','quiz',$regs[2]);
2436         }
2438         // Exchange the function call with '1' and then chack for
2439         // another function call...
2440         if ($regs[1]) {
2441             // The function call is proceeded by an operator
2442             $formula = str_replace($regs[0], $regs[1] . '1', $formula);
2443         } else {
2444             // The function call starts the formula
2445             $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula);
2446         }
2447     }
2449     if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) {
2450         return get_string('illegalformulasyntax', 'quiz', $regs[0]);
2451     } else {
2452         // Formula just might be valid
2453         return false;
2454     }
2458 function dump($obj) {
2459     echo "<pre>\n";
2460     var_dump($obj);
2461     echo "</pre><br />\n";