MDL-43670 quiz: improve code comments a little
[moodle.git] / question / type / calculated / questiontype.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Question type class for the calculated question type.
19  *
20  * @package    qtype
21  * @subpackage calculated
22  * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/question/type/questionbase.php');
30 require_once($CFG->dirroot . '/question/type/numerical/question.php');
33 /**
34  * The calculated question type.
35  *
36  * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
37  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class qtype_calculated extends question_type {
40     const MAX_DATASET_ITEMS = 100;
42     public $wizardpagesnumber = 3;
44     public function get_question_options($question) {
45         // First get the datasets and default options.
46         // The code is used for calculated, calculatedsimple and calculatedmulti qtypes.
47         global $CFG, $DB, $OUTPUT;
48         if (!$question->options = $DB->get_record('question_calculated_options',
49                 array('question' => $question->id))) {
50             $question->options = new stdClass();
51             $question->options->synchronize = 0;
52             $question->options->single = 0;
53             $question->options->answernumbering = 'abc';
54             $question->options->shuffleanswers = 0;
55             $question->options->correctfeedback = '';
56             $question->options->partiallycorrectfeedback = '';
57             $question->options->incorrectfeedback = '';
58             $question->options->correctfeedbackformat = 0;
59             $question->options->partiallycorrectfeedbackformat = 0;
60             $question->options->incorrectfeedbackformat = 0;
61         }
63         if (!$question->options->answers = $DB->get_records_sql("
64             SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.correctanswerformat
65             FROM {question_answers} a,
66                  {question_calculated} c
67             WHERE a.question = ?
68             AND   a.id = c.answer
69             ORDER BY a.id ASC", array($question->id))) {
70                 return false;
71         }
73         if ($this->get_virtual_qtype()->name() == 'numerical') {
74             $this->get_virtual_qtype()->get_numerical_units($question);
75             $this->get_virtual_qtype()->get_numerical_options($question);
76         }
78         $question->hints = $DB->get_records('question_hints',
79                 array('questionid' => $question->id), 'id ASC');
81         if (isset($question->export_process)&&$question->export_process) {
82             $question->options->datasets = $this->get_datasets_for_export($question);
83         }
84         return true;
85     }
87     public function get_datasets_for_export($question) {
88         global $DB, $CFG;
89         $datasetdefs = array();
90         if (!empty($question->id)) {
91             $sql = "SELECT i.*
92                       FROM {question_datasets} d, {question_dataset_definitions} i
93                      WHERE d.question = ? AND d.datasetdefinition = i.id";
94             if ($records = $DB->get_records_sql($sql, array($question->id))) {
95                 foreach ($records as $r) {
96                     $def = $r;
97                     if ($def->category == '0') {
98                         $def->status = 'private';
99                     } else {
100                         $def->status = 'shared';
101                     }
102                     $def->type = 'calculated';
103                     list($distribution, $min, $max, $dec) = explode(':', $def->options, 4);
104                     $def->distribution = $distribution;
105                     $def->minimum = $min;
106                     $def->maximum = $max;
107                     $def->decimals = $dec;
108                     if ($def->itemcount > 0) {
109                         // Get the datasetitems.
110                         $def->items = array();
111                         if ($items = $this->get_database_dataset_items($def->id)) {
112                             $n = 0;
113                             foreach ($items as $ii) {
114                                 $n++;
115                                 $def->items[$n] = new stdClass();
116                                 $def->items[$n]->itemnumber = $ii->itemnumber;
117                                 $def->items[$n]->value = $ii->value;
118                             }
119                             $def->number_of_items = $n;
120                         }
121                     }
122                     $datasetdefs["1-$r->category-$r->name"] = $def;
123                 }
124             }
125         }
126         return $datasetdefs;
127     }
129     public function save_question_options($question) {
130         global $CFG, $DB;
131         // The code is used for calculated, calculatedsimple and calculatedmulti qtypes.
132         $context = $question->context;
133         if (isset($question->answer) && !isset($question->answers)) {
134             $question->answers = $question->answer;
135         }
136         // Calculated options.
137         $update = true;
138         $options = $DB->get_record('question_calculated_options',
139                 array('question' => $question->id));
140         if (!$options) {
141             $update = false;
142             $options = new stdClass();
143             $options->question = $question->id;
144         }
145         // As used only by calculated.
146         if (isset($question->synchronize)) {
147             $options->synchronize = $question->synchronize;
148         } else {
149             $options->synchronize = 0;
150         }
151         $options->single = 0;
152         $options->answernumbering =  $question->answernumbering;
153         $options->shuffleanswers = $question->shuffleanswers;
155         foreach (array('correctfeedback', 'partiallycorrectfeedback',
156                 'incorrectfeedback') as $feedbackname) {
157             $options->$feedbackname = '';
158             $feedbackformat = $feedbackname . 'format';
159             $options->$feedbackformat = 0;
160         }
162         if ($update) {
163             $DB->update_record('question_calculated_options', $options);
164         } else {
165             $DB->insert_record('question_calculated_options', $options);
166         }
168         // Get old versions of the objects.
169         $oldanswers = $DB->get_records('question_answers',
170                 array('question' => $question->id), 'id ASC');
172         $oldoptions = $DB->get_records('question_calculated',
173                 array('question' => $question->id), 'answer ASC');
175         // Save the units.
176         $virtualqtype = $this->get_virtual_qtype();
178         $result = $virtualqtype->save_units($question);
179         if (isset($result->error)) {
180             return $result;
181         } else {
182             $units = $result->units;
183         }
185         // Insert all the new answers.
186         if (isset($question->answer) && !isset($question->answers)) {
187             $question->answers = $question->answer;
188         }
189         foreach ($question->answers as $key => $answerdata) {
190             if (is_array($answerdata)) {
191                 $answerdata = $answerdata['text'];
192             }
193             if (trim($answerdata) == '') {
194                 continue;
195             }
197             // Update an existing answer if possible.
198             $answer = array_shift($oldanswers);
199             if (!$answer) {
200                 $answer = new stdClass();
201                 $answer->question = $question->id;
202                 $answer->answer   = '';
203                 $answer->feedback = '';
204                 $answer->id       = $DB->insert_record('question_answers', $answer);
205             }
207             $answer->answer   = trim($answerdata);
208             $answer->fraction = $question->fraction[$key];
209             $answer->feedback = $this->import_or_save_files($question->feedback[$key],
210                     $context, 'question', 'answerfeedback', $answer->id);
211             $answer->feedbackformat = $question->feedback[$key]['format'];
213             $DB->update_record("question_answers", $answer);
215             // Set up the options object.
216             if (!$options = array_shift($oldoptions)) {
217                 $options = new stdClass();
218             }
219             $options->question            = $question->id;
220             $options->answer              = $answer->id;
221             $options->tolerance           = trim($question->tolerance[$key]);
222             $options->tolerancetype       = trim($question->tolerancetype[$key]);
223             $options->correctanswerlength = trim($question->correctanswerlength[$key]);
224             $options->correctanswerformat = trim($question->correctanswerformat[$key]);
226             // Save options.
227             if (isset($options->id)) {
228                 // Reusing existing record.
229                 $DB->update_record('question_calculated', $options);
230             } else {
231                 // New options.
232                 $DB->insert_record('question_calculated', $options);
233             }
234         }
236         // Delete old answer records.
237         if (!empty($oldanswers)) {
238             foreach ($oldanswers as $oa) {
239                 $DB->delete_records('question_answers', array('id' => $oa->id));
240             }
241         }
243         // Delete old answer records.
244         if (!empty($oldoptions)) {
245             foreach ($oldoptions as $oo) {
246                 $DB->delete_records('question_calculated', array('id' => $oo->id));
247             }
248         }
250         $result = $virtualqtype->save_unit_options($question);
251         if (isset($result->error)) {
252             return $result;
253         }
255         $this->save_hints($question);
257         if (isset($question->import_process)&&$question->import_process) {
258             $this->import_datasets($question);
259         }
260         // Report any problems.
261         if (!empty($result->notice)) {
262             return $result;
263         }
264         return true;
265     }
267     public function import_datasets($question) {
268         global $DB;
269         $n = count($question->dataset);
270         foreach ($question->dataset as $dataset) {
271             // Name, type, option.
272             $datasetdef = new stdClass();
273             $datasetdef->name = $dataset->name;
274             $datasetdef->type = 1;
275             $datasetdef->options =  $dataset->distribution . ':' . $dataset->min . ':' .
276                     $dataset->max . ':' . $dataset->length;
277             $datasetdef->itemcount = $dataset->itemcount;
278             if ($dataset->status == 'private') {
279                 $datasetdef->category = 0;
280                 $todo = 'create';
281             } else if ($dataset->status == 'shared') {
282                 if ($sharedatasetdefs = $DB->get_records_select(
283                     'question_dataset_definitions',
284                     "type = '1'
285                     AND name = ?
286                     AND category = ?
287                     ORDER BY id DESC ", array($dataset->name, $question->category)
288                 )) { // So there is at least one.
289                     $sharedatasetdef = array_shift($sharedatasetdefs);
290                     if ($sharedatasetdef->options ==  $datasetdef->options) {// Identical so use it.
291                         $todo = 'useit';
292                         $datasetdef = $sharedatasetdef;
293                     } else { // Different so create a private one.
294                         $datasetdef->category = 0;
295                         $todo = 'create';
296                     }
297                 } else { // No so create one.
298                     $datasetdef->category = $question->category;
299                     $todo = 'create';
300                 }
301             }
302             if ($todo == 'create') {
303                 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
304             }
305             // Create relation to the dataset.
306             $questiondataset = new stdClass();
307             $questiondataset->question = $question->id;
308             $questiondataset->datasetdefinition = $datasetdef->id;
309             $DB->insert_record('question_datasets', $questiondataset);
310             if ($todo == 'create') {
311                 // Add the items.
312                 foreach ($dataset->datasetitem as $dataitem) {
313                     $datasetitem = new stdClass();
314                     $datasetitem->definition = $datasetdef->id;
315                     $datasetitem->itemnumber = $dataitem->itemnumber;
316                     $datasetitem->value = $dataitem->value;
317                     $DB->insert_record('question_dataset_items', $datasetitem);
318                 }
319             }
320         }
321     }
323     protected function initialise_question_instance(question_definition $question, $questiondata) {
324         parent::initialise_question_instance($question, $questiondata);
326         question_bank::get_qtype('numerical')->initialise_numerical_answers(
327                 $question, $questiondata);
328         foreach ($questiondata->options->answers as $a) {
329             $question->answers[$a->id]->tolerancetype = $a->tolerancetype;
330             $question->answers[$a->id]->correctanswerlength = $a->correctanswerlength;
331             $question->answers[$a->id]->correctanswerformat = $a->correctanswerformat;
332         }
334         $question->synchronised = $questiondata->options->synchronize;
336         $question->unitdisplay = $questiondata->options->showunits;
337         $question->unitgradingtype = $questiondata->options->unitgradingtype;
338         $question->unitpenalty = $questiondata->options->unitpenalty;
339         $question->ap = question_bank::get_qtype(
340                 'numerical')->make_answer_processor(
341                 $questiondata->options->units, $questiondata->options->unitsleft);
343         $question->datasetloader = new qtype_calculated_dataset_loader($questiondata->id);
344     }
346     public function validate_form($form) {
347         switch($form->wizardpage) {
348             case 'question':
349                 $calculatedmessages = array();
350                 if (empty($form->name)) {
351                     $calculatedmessages[] = get_string('missingname', 'qtype_calculated');
352                 }
353                 if (empty($form->questiontext)) {
354                     $calculatedmessages[] = get_string('missingquestiontext', 'qtype_calculated');
355                 }
356                 // Verify formulas.
357                 foreach ($form->answers as $key => $answer) {
358                     if ('' === trim($answer)) {
359                         $calculatedmessages[] = get_string(
360                                 'missingformula', 'qtype_calculated');
361                     }
362                     if ($formulaerrors = qtype_calculated_find_formula_errors($answer)) {
363                         $calculatedmessages[] = $formulaerrors;
364                     }
365                     if (! isset($form->tolerance[$key])) {
366                         $form->tolerance[$key] = 0.0;
367                     }
368                     if (! is_numeric($form->tolerance[$key])) {
369                         $calculatedmessages[] = get_string('xmustbenumeric', 'qtype_numerical',
370                                 get_string('tolerance', 'qtype_calculated'));
371                     }
372                 }
374                 if (!empty($calculatedmessages)) {
375                     $errorstring = "The following errors were found:<br />";
376                     foreach ($calculatedmessages as $msg) {
377                         $errorstring .= $msg . '<br />';
378                     }
379                     print_error($errorstring);
380                 }
382                 break;
383             default:
384                 return parent::validate_form($form);
385                 break;
386         }
387         return true;
388     }
389     public function finished_edit_wizard($form) {
390         return isset($form->savechanges);
391     }
392     public function wizardpagesnumber() {
393         return 3;
394     }
395     // This gets called by editquestion.php after the standard question is saved.
396     public function print_next_wizard_page($question, $form, $course) {
397         global $CFG, $SESSION, $COURSE;
399         // Catch invalid navigation & reloads.
400         if (empty($question->id) && empty($SESSION->calculated)) {
401             redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired.', 3);
402         }
404         // See where we're coming from.
405         switch($form->wizardpage) {
406             case 'question':
407                 require("$CFG->dirroot/question/type/calculated/datasetdefinitions.php");
408                 break;
409             case 'datasetdefinitions':
410             case 'datasetitems':
411                 require("$CFG->dirroot/question/type/calculated/datasetitems.php");
412                 break;
413             default:
414                 print_error('invalidwizardpage', 'question');
415                 break;
416         }
417     }
419     // This gets called by question2.php after the standard question is saved.
420     public function &next_wizard_form($submiturl, $question, $wizardnow) {
421         global $CFG, $SESSION, $COURSE;
423         // Catch invalid navigation & reloads.
424         if (empty($question->id) && empty($SESSION->calculated)) {
425             redirect('edit.php?courseid=' . $COURSE->id,
426                     'The page you are loading has expired. Cannot get next wizard form.', 3);
427         }
428         if (empty($question->id)) {
429             $question = $SESSION->calculated->questionform;
430         }
432         // See where we're coming from.
433         switch($wizardnow) {
434             case 'datasetdefinitions':
435                 require("$CFG->dirroot/question/type/calculated/datasetdefinitions_form.php");
436                 $mform = new question_dataset_dependent_definitions_form(
437                         "$submiturl?wizardnow=datasetdefinitions", $question);
438                 break;
439             case 'datasetitems':
440                 require("$CFG->dirroot/question/type/calculated/datasetitems_form.php");
441                 $regenerate = optional_param('forceregeneration', false, PARAM_BOOL);
442                 $mform = new question_dataset_dependent_items_form(
443                         "$submiturl?wizardnow=datasetitems", $question, $regenerate);
444                 break;
445             default:
446                 print_error('invalidwizardpage', 'question');
447                 break;
448         }
450         return $mform;
451     }
453     /**
454      * This method should be overriden if you want to include a special heading or some other
455      * html on a question editing page besides the question editing form.
456      *
457      * @param question_edit_form $mform a child of question_edit_form
458      * @param object $question
459      * @param string $wizardnow is '' for first page.
460      */
461     public function display_question_editing_page($mform, $question, $wizardnow) {
462         global $OUTPUT;
463         switch ($wizardnow) {
464             case '':
465                 // On the first page, the default display is fine.
466                 parent::display_question_editing_page($mform, $question, $wizardnow);
467                 return;
469             case 'datasetdefinitions':
470                 echo $OUTPUT->heading_with_help(
471                         get_string('choosedatasetproperties', 'qtype_calculated'),
472                         'questiondatasets', 'qtype_calculated');
473                 break;
475             case 'datasetitems':
476                 echo $OUTPUT->heading_with_help(get_string('editdatasets', 'qtype_calculated'),
477                         'questiondatasets', 'qtype_calculated');
478                 break;
479         }
481         $mform->display();
482     }
484     /**
485      * This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php
486      * so that they can be saved
487      * using the function save_dataset_definitions($form)
488      * when creating a new calculated question or
489      * when editing an already existing calculated question
490      * or by  function save_as_new_dataset_definitions($form, $initialid)
491      * when saving as new an already existing calculated question.
492      *
493      * @param object $form
494      * @param int $questionfromid default = '0'
495      */
496     public function preparedatasets($form , $questionfromid = '0') {
497         // The dataset names present in the edit_question_form and edit_calculated_form
498         // are retrieved.
499         $possibledatasets = $this->find_dataset_names($form->questiontext);
500         $mandatorydatasets = array();
501         foreach ($form->answers as $answer) {
502             $mandatorydatasets += $this->find_dataset_names($answer);
503         }
504         // If there are identical datasetdefs already saved in the original question
505         // either when editing a question or saving as new,
506         // they are retrieved using $questionfromid.
507         if ($questionfromid != '0') {
508             $form->id = $questionfromid;
509         }
510         $datasets = array();
511         $key = 0;
512         // Always prepare the mandatorydatasets present in the answers.
513         // The $options are not used here.
514         foreach ($mandatorydatasets as $datasetname) {
515             if (!isset($datasets[$datasetname])) {
516                 list($options, $selected) =
517                     $this->dataset_options($form, $datasetname);
518                 $datasets[$datasetname] = '';
519                 $form->dataset[$key] = $selected;
520                 $key++;
521             }
522         }
523         // Do not prepare possibledatasets when creating a question.
524         // They will defined and stored with datasetdefinitions_form.php.
525         // The $options are not used here.
526         if ($questionfromid != '0') {
528             foreach ($possibledatasets as $datasetname) {
529                 if (!isset($datasets[$datasetname])) {
530                     list($options, $selected) =
531                         $this->dataset_options($form, $datasetname, false);
532                     $datasets[$datasetname] = '';
533                     $form->dataset[$key] = $selected;
534                     $key++;
535                 }
536             }
537         }
538         return $datasets;
539     }
540     public function addnamecategory(&$question) {
541         global $DB;
542         $categorydatasetdefs = $DB->get_records_sql(
543             "SELECT  a.*
544                FROM {question_datasets} b, {question_dataset_definitions} a
545               WHERE a.id = b.datasetdefinition
546                 AND a.type = '1'
547                 AND a.category != 0
548                 AND b.question = ?
549            ORDER BY a.name ", array($question->id));
550         $questionname = $question->name;
551         $regs= array();
552         if (preg_match('~#\{([^[:space:]]*)#~', $questionname , $regs)) {
553             $questionname = str_replace($regs[0], '', $questionname);
554         };
556         if (!empty($categorydatasetdefs)) {
557             // There is at least one with the same name.
558             $questionname = '#' . $questionname;
559             foreach ($categorydatasetdefs as $def) {
560                 if (strlen($def->name) + strlen($questionname) < 250) {
561                     $questionname = '{' . $def->name . '}' . $questionname;
562                 }
563             }
564             $questionname = '#' . $questionname;
565         }
566         $DB->set_field('question', 'name', $questionname, array('id' => $question->id));
567     }
569     /**
570      * this version save the available data at the different steps of the question editing process
571      * without using global $SESSION as storage between steps
572      * at the first step $wizardnow = 'question'
573      *  when creating a new question
574      *  when modifying a question
575      *  when copying as a new question
576      *  the general parameters and answers are saved using parent::save_question
577      *  then the datasets are prepared and saved
578      * at the second step $wizardnow = 'datasetdefinitions'
579      *  the datadefs final type are defined as private, category or not a datadef
580      * at the third step $wizardnow = 'datasetitems'
581      *  the datadefs parameters and the data items are created or defined
582      *
583      * @param object question
584      * @param object $form
585      * @param int $course
586      * @param PARAM_ALPHA $wizardnow should be added as we are coming from question2.php
587      */
588     public function save_question($question, $form) {
589         global $DB;
590         if ($this->wizardpagesnumber() == 1 || $question->qtype == 'calculatedsimple') {
591                 $question = parent::save_question($question, $form);
592             return $question;
593         }
595         $wizardnow =  optional_param('wizardnow', '', PARAM_ALPHA);
596         $id = optional_param('id', 0, PARAM_INT); // Question id.
597         // In case 'question':
598         // For a new question $form->id is empty
599         // when saving as new question.
600         // The $question->id = 0, $form is $data from question2.php
601         // and $data->makecopy is defined as $data->id is the initial question id.
602         // Edit case. If it is a new question we don't necessarily need to
603         // return a valid question object.
605         // See where we're coming from.
606         switch($wizardnow) {
607             case '' :
608             case 'question': // Coming from the first page, creating the second.
609                 if (empty($form->id)) { // or a new question $form->id is empty.
610                     $question = parent::save_question($question, $form);
611                     // Prepare the datasets using default $questionfromid.
612                     $this->preparedatasets($form);
613                     $form->id = $question->id;
614                     $this->save_dataset_definitions($form);
615                     if (isset($form->synchronize) && $form->synchronize == 2) {
616                         $this->addnamecategory($question);
617                     }
618                 } else if (!empty($form->makecopy)) {
619                     $questionfromid =  $form->id;
620                     $question = parent::save_question($question, $form);
621                     // Prepare the datasets.
622                     $this->preparedatasets($form, $questionfromid);
623                     $form->id = $question->id;
624                     $this->save_as_new_dataset_definitions($form, $questionfromid);
625                     if (isset($form->synchronize) && $form->synchronize == 2) {
626                         $this->addnamecategory($question);
627                     }
628                 } else {
629                     // Editing a question.
630                     $question = parent::save_question($question, $form);
631                     // Prepare the datasets.
632                     $this->preparedatasets($form, $question->id);
633                     $form->id = $question->id;
634                     $this->save_dataset_definitions($form);
635                     if (isset($form->synchronize) && $form->synchronize == 2) {
636                         $this->addnamecategory($question);
637                     }
638                 }
639                 break;
640             case 'datasetdefinitions':
641                 // Calculated options.
642                 // It cannot go here without having done the first page,
643                 // so the question_calculated_options should exist.
644                 // We only need to update the synchronize field.
645                 if (isset($form->synchronize)) {
646                     $optionssynchronize = $form->synchronize;
647                 } else {
648                     $optionssynchronize = 0;
649                 }
650                 $DB->set_field('question_calculated_options', 'synchronize', $optionssynchronize,
651                         array('question' => $question->id));
652                 if (isset($form->synchronize) && $form->synchronize == 2) {
653                     $this->addnamecategory($question);
654                 }
656                 $this->save_dataset_definitions($form);
657                 break;
658             case 'datasetitems':
659                 $this->save_dataset_items($question, $form);
660                 $this->save_question_calculated($question, $form);
661                 break;
662             default:
663                 print_error('invalidwizardpage', 'question');
664                 break;
665         }
666         return $question;
667     }
669     public function delete_question($questionid, $contextid) {
670         global $DB;
672         $DB->delete_records('question_calculated', array('question' => $questionid));
673         $DB->delete_records('question_calculated_options', array('question' => $questionid));
674         $DB->delete_records('question_numerical_units', array('question' => $questionid));
675         if ($datasets = $DB->get_records('question_datasets', array('question' => $questionid))) {
676             foreach ($datasets as $dataset) {
677                 if (!$DB->get_records_select('question_datasets',
678                         "question != ? AND datasetdefinition = ? ",
679                         array($questionid, $dataset->datasetdefinition))) {
680                     $DB->delete_records('question_dataset_definitions',
681                             array('id' => $dataset->datasetdefinition));
682                     $DB->delete_records('question_dataset_items',
683                             array('definition' => $dataset->datasetdefinition));
684                 }
685             }
686         }
687         $DB->delete_records('question_datasets', array('question' => $questionid));
689         parent::delete_question($questionid, $contextid);
690     }
692     public function get_random_guess_score($questiondata) {
693         foreach ($questiondata->options->answers as $aid => $answer) {
694             if ('*' == trim($answer->answer)) {
695                 return max($answer->fraction - $questiondata->options->unitpenalty, 0);
696             }
697         }
698         return 0;
699     }
701     public function supports_dataset_item_generation() {
702         // Calculated support generation of randomly distributed number data.
703         return true;
704     }
706     public function custom_generator_tools_part($mform, $idx, $j) {
708         $minmaxgrp = array();
709         $minmaxgrp[] = $mform->createElement('text', "calcmin[$idx]",
710                 get_string('calcmin', 'qtype_calculated'));
711         $minmaxgrp[] = $mform->createElement('text', "calcmax[$idx]",
712                 get_string('calcmax', 'qtype_calculated'));
713         $mform->addGroup($minmaxgrp, 'minmaxgrp',
714                 get_string('minmax', 'qtype_calculated'), ' - ', false);
715         $mform->setType("calcmin[$idx]", PARAM_FLOAT);
716         $mform->setType("calcmax[$idx]", PARAM_FLOAT);
718         $precisionoptions = range(0, 10);
719         $mform->addElement('select', "calclength[$idx]",
720                 get_string('calclength', 'qtype_calculated'), $precisionoptions);
722         $distriboptions = array('uniform' => get_string('uniform', 'qtype_calculated'),
723                 'loguniform' => get_string('loguniform', 'qtype_calculated'));
724         $mform->addElement('select', "calcdistribution[$idx]",
725                 get_string('calcdistribution', 'qtype_calculated'), $distriboptions);
726     }
728     public function custom_generator_set_data($datasetdefs, $formdata) {
729         $idx = 1;
730         foreach ($datasetdefs as $datasetdef) {
731             if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
732                     $datasetdef->options, $regs)) {
733                 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
734                 $formdata["calcdistribution[$idx]"] = $regs[1];
735                 $formdata["calcmin[$idx]"] = $regs[2];
736                 $formdata["calcmax[$idx]"] = $regs[3];
737                 $formdata["calclength[$idx]"] = $regs[4];
738             }
739             $idx++;
740         }
741         return $formdata;
742     }
744     public function custom_generator_tools($datasetdef) {
745         global $OUTPUT;
746         if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
747                 $datasetdef->options, $regs)) {
748             $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
749             for ($i = 0; $i<10; ++$i) {
750                 $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
751                     ? 'decimals'
752                     : 'significantfigures'), 'qtype_calculated', $i);
753             }
754             $menu1 = html_writer::label(get_string('lengthoption', 'qtype_calculated'),
755                 'menucalclength', false, array('class' => 'accesshide'));
756             $menu1 .= html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
758             $options = array('uniform' => get_string('uniformbit', 'qtype_calculated'),
759                 'loguniform' => get_string('loguniformbit', 'qtype_calculated'));
760             $menu2 = html_writer::label(get_string('distributionoption', 'qtype_calculated'),
761                 'menucalcdistribution', false, array('class' => 'accesshide'));
762             $menu2 .= html_writer::select($options, 'calcdistribution[]', $regs[1], null);
763             return '<input type="submit" onclick="'
764                 . "getElementById('addform').regenerateddefid.value='$defid'; return true;"
765                 .'" value="'. get_string('generatevalue', 'qtype_calculated') . '"/><br/>'
766                 . '<input type="text" size="3" name="calcmin[]" '
767                 . " value=\"$regs[2]\"/> &amp; <input name=\"calcmax[]\" "
768                 . ' type="text" size="3" value="' . $regs[3] .'"/> '
769                 . $menu1 . '<br/>'
770                 . $menu2;
771         } else {
772             return '';
773         }
774     }
777     public function update_dataset_options($datasetdefs, $form) {
778         global $OUTPUT;
779         // Do we have information about new options ?
780         if (empty($form->definition) || empty($form->calcmin)
781                 ||empty($form->calcmax) || empty($form->calclength)
782                 || empty($form->calcdistribution)) {
783             // I guess not.
785         } else {
786             // Looks like we just could have some new information here.
787             $uniquedefs = array_values(array_unique($form->definition));
788             foreach ($uniquedefs as $key => $defid) {
789                 if (isset($datasetdefs[$defid])
790                         && is_numeric($form->calcmin[$key+1])
791                         && is_numeric($form->calcmax[$key+1])
792                         && is_numeric($form->calclength[$key+1])) {
793                     switch     ($form->calcdistribution[$key+1]) {
794                         case 'uniform': case 'loguniform':
795                             $datasetdefs[$defid]->options =
796                                 $form->calcdistribution[$key+1] . ':'
797                                 . $form->calcmin[$key+1] . ':'
798                                 . $form->calcmax[$key+1] . ':'
799                                 . $form->calclength[$key+1];
800                             break;
801                         default:
802                             echo $OUTPUT->notification(
803                                     "Unexpected distribution ".$form->calcdistribution[$key+1]);
804                     }
805                 }
806             }
807         }
809         // Look for empty options, on which we set default values.
810         foreach ($datasetdefs as $defid => $def) {
811             if (empty($def->options)) {
812                 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1';
813             }
814         }
815         return $datasetdefs;
816     }
818     public function save_question_calculated($question, $fromform) {
819         global $DB;
821         foreach ($question->options->answers as $key => $answer) {
822             if ($options = $DB->get_record('question_calculated', array('answer' => $key))) {
823                 $options->tolerance = trim($fromform->tolerance[$key]);
824                 $options->tolerancetype  = trim($fromform->tolerancetype[$key]);
825                 $options->correctanswerlength  = trim($fromform->correctanswerlength[$key]);
826                 $options->correctanswerformat  = trim($fromform->correctanswerformat[$key]);
827                 $DB->update_record('question_calculated', $options);
828             }
829         }
830     }
832     /**
833      * This function get the dataset items using id as unique parameter and return an
834      * array with itemnumber as index sorted ascendant
835      * If the multiple records with the same itemnumber exist, only the newest one
836      * i.e with the greatest id is used, the others are ignored but not deleted.
837      * MDL-19210
838      */
839     public function get_database_dataset_items($definition) {
840         global $CFG, $DB;
841         $databasedataitems = $DB->get_records_sql(// Use number as key!!
842             " SELECT id , itemnumber, definition,  value
843             FROM {question_dataset_items}
844             WHERE definition = $definition order by id DESC ", array($definition));
845         $dataitems = Array();
846         foreach ($databasedataitems as $id => $dataitem) {
847             if (!isset($dataitems[$dataitem->itemnumber])) {
848                 $dataitems[$dataitem->itemnumber] = $dataitem;
849             }
850         }
851         ksort($dataitems);
852         return $dataitems;
853     }
855     public function save_dataset_items($question, $fromform) {
856         global $CFG, $DB;
857         $synchronize = false;
858         if (isset($fromform->nextpageparam['forceregeneration'])) {
859             $regenerate = $fromform->nextpageparam['forceregeneration'];
860         } else {
861             $regenerate = 0;
862         }
863         if (empty($question->options)) {
864             $this->get_question_options($question);
865         }
866         if (!empty($question->options->synchronize)) {
867             $synchronize = true;
868         }
870         // Get the old datasets for this question.
871         $datasetdefs = $this->get_dataset_definitions($question->id, array());
872         // Handle generator options...
873         $olddatasetdefs = fullclone($datasetdefs);
874         $datasetdefs = $this->update_dataset_options($datasetdefs, $fromform);
875         $maxnumber = -1;
876         foreach ($datasetdefs as $defid => $datasetdef) {
877             if (isset($datasetdef->id)
878                     && $datasetdef->options != $olddatasetdefs[$defid]->options) {
879                 // Save the new value for options.
880                 $DB->update_record('question_dataset_definitions', $datasetdef);
882             }
883             // Get maxnumber.
884             if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) {
885                 $maxnumber = $datasetdef->itemcount;
886             }
887         }
888         // Handle adding and removing of dataset items.
889         $i = 1;
890         if ($maxnumber > self::MAX_DATASET_ITEMS) {
891             $maxnumber = self::MAX_DATASET_ITEMS;
892         }
894         ksort($fromform->definition);
895         foreach ($fromform->definition as $key => $defid) {
896             // If the delete button has not been pressed then skip the datasetitems
897             // in the 'add item' part of the form.
898             if ($i > count($datasetdefs)*$maxnumber) {
899                 break;
900             }
901             $addeditem = new stdClass();
902             $addeditem->definition = $datasetdefs[$defid]->id;
903             $addeditem->value = $fromform->number[$i];
904             $addeditem->itemnumber = ceil($i / count($datasetdefs));
906             if ($fromform->itemid[$i]) {
907                 // Reuse any previously used record.
908                 $addeditem->id = $fromform->itemid[$i];
909                 $DB->update_record('question_dataset_items', $addeditem);
910             } else {
911                 $DB->insert_record('question_dataset_items', $addeditem);
912             }
914             $i++;
915         }
916         if (isset($addeditem->itemnumber) && $maxnumber < $addeditem->itemnumber
917                 && $addeditem->itemnumber < self::MAX_DATASET_ITEMS) {
918             $maxnumber = $addeditem->itemnumber;
919             foreach ($datasetdefs as $key => $newdef) {
920                 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
921                     $newdef->itemcount = $maxnumber;
922                     // Save the new value for options.
923                     $DB->update_record('question_dataset_definitions', $newdef);
924                 }
925             }
926         }
927         // Adding supplementary items.
928         $numbertoadd = 0;
929         if (isset($fromform->addbutton) && $fromform->selectadd > 0 &&
930                 $maxnumber < self::MAX_DATASET_ITEMS) {
931             $numbertoadd = $fromform->selectadd;
932             if (self::MAX_DATASET_ITEMS - $maxnumber < $numbertoadd) {
933                 $numbertoadd = self::MAX_DATASET_ITEMS - $maxnumber;
934             }
935             // Add the other items.
936             // Generate a new dataset item (or reuse an old one).
937             foreach ($datasetdefs as $defid => $datasetdef) {
938                 // In case that for category datasets some new items has been added,
939                 // get actual values.
940                 // Fix regenerate for this datadefs.
941                 $defregenerate = 0;
942                 if ($synchronize &&
943                         !empty ($fromform->nextpageparam["datasetregenerate[$datasetdef->name"])) {
944                     $defregenerate = 1;
945                 } else if (!$synchronize &&
946                         (($regenerate == 1 && $datasetdef->category == 0) ||$regenerate == 2)) {
947                     $defregenerate = 1;
948                 }
949                 if (isset($datasetdef->id)) {
950                     $datasetdefs[$defid]->items =
951                             $this->get_database_dataset_items($datasetdef->id);
952                 }
953                 for ($numberadded = $maxnumber+1; $numberadded <= $maxnumber + $numbertoadd; $numberadded++) {
954                     if (isset($datasetdefs[$defid]->items[$numberadded])) {
955                         // In case of regenerate it modifies the already existing record.
956                         if ($defregenerate) {
957                             $datasetitem = new stdClass();
958                             $datasetitem->id = $datasetdefs[$defid]->items[$numberadded]->id;
959                             $datasetitem->definition = $datasetdef->id;
960                             $datasetitem->itemnumber = $numberadded;
961                             $datasetitem->value =
962                                     $this->generate_dataset_item($datasetdef->options);
963                             $DB->update_record('question_dataset_items', $datasetitem);
964                         }
965                         // If not regenerate do nothing as there is already a record.
966                     } else {
967                         $datasetitem = new stdClass();
968                         $datasetitem->definition = $datasetdef->id;
969                         $datasetitem->itemnumber = $numberadded;
970                         if ($this->supports_dataset_item_generation()) {
971                             $datasetitem->value =
972                                     $this->generate_dataset_item($datasetdef->options);
973                         } else {
974                             $datasetitem->value = '';
975                         }
976                         $DB->insert_record('question_dataset_items', $datasetitem);
977                     }
978                 }// For number added.
979             }// Datasetsdefs end.
980             $maxnumber += $numbertoadd;
981             foreach ($datasetdefs as $key => $newdef) {
982                 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
983                     $newdef->itemcount = $maxnumber;
984                     // Save the new value for options.
985                     $DB->update_record('question_dataset_definitions', $newdef);
986                 }
987             }
988         }
990         if (isset($fromform->deletebutton)) {
991             if (isset($fromform->selectdelete)) {
992                 $newmaxnumber = $maxnumber-$fromform->selectdelete;
993             } else {
994                 $newmaxnumber = $maxnumber-1;
995             }
996             if ($newmaxnumber < 0) {
997                 $newmaxnumber = 0;
998             }
999             foreach ($datasetdefs as $datasetdef) {
1000                 if ($datasetdef->itemcount == $maxnumber) {
1001                     $datasetdef->itemcount= $newmaxnumber;
1002                     $DB->update_record('question_dataset_definitions', $datasetdef);
1003                 }
1004             }
1005         }
1006     }
1007     public function generate_dataset_item($options) {
1008         if (!preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
1009                 $options, $regs)) {
1010             // Unknown options...
1011             return false;
1012         }
1013         if ($regs[1] == 'uniform') {
1014             $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
1015             return sprintf("%.".$regs[4].'f', $nbr);
1017         } else if ($regs[1] == 'loguniform') {
1018             $log0 = log(abs($regs[2])); // It would have worked the other way to.
1019             $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
1020             return sprintf("%.".$regs[4].'f', $nbr);
1022         } else {
1023             print_error('disterror', 'question', '', $regs[1]);
1024         }
1025         return '';
1026     }
1028     public function comment_header($question) {
1029         $strheader = '';
1030         $delimiter = '';
1032         $answers = $question->options->answers;
1034         foreach ($answers as $key => $answer) {
1035             $ans = shorten_text($answer->answer, 17, true);
1036             $strheader .= $delimiter.$ans;
1037             $delimiter = '<br/><br/><br/>';
1038         }
1039         return $strheader;
1040     }
1042     public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext,
1043             $answers, $data, $number) {
1044         global $DB;
1045         $comment = new stdClass();
1046         $comment->stranswers = array();
1047         $comment->outsidelimit = false;
1048         $comment->answers = array();
1049         // Find a default unit.
1050         if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units',
1051                 array('question' => $questionid, 'multiplier' => 1.0))) {
1052             $unit = $unit->unit;
1053         } else {
1054             $unit = '';
1055         }
1057         $answers = fullclone($answers);
1058         $errors = '';
1059         $delimiter = ': ';
1060         $virtualqtype =  $qtypeobj->get_virtual_qtype();
1061         foreach ($answers as $key => $answer) {
1062             $formula = $this->substitute_variables($answer->answer, $data);
1063             $formattedanswer = qtype_calculated_calculate_answer(
1064                 $answer->answer, $data, $answer->tolerance,
1065                 $answer->tolerancetype, $answer->correctanswerlength,
1066                 $answer->correctanswerformat, $unit);
1067             if ($formula === '*') {
1068                 $answer->min = ' ';
1069                 $formattedanswer->answer = $answer->answer;
1070             } else {
1071                 eval('$ansvalue = '.$formula.';');
1072                 $ans = new qtype_numerical_answer(0, $ansvalue, 0, '', 0, $answer->tolerance);
1073                 $ans->tolerancetype = $answer->tolerancetype;
1074                 list($answer->min, $answer->max) = $ans->get_tolerance_interval($answer);
1075             }
1076             if ($answer->min === '') {
1077                 // This should mean that something is wrong.
1078                 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
1079             } else if ($formula === '*') {
1080                 $comment->stranswers[$key] = $formula . ' = ' .
1081                         get_string('anyvalue', 'qtype_calculated') . '<br/><br/><br/>';
1082             } else {
1083                 $formula = shorten_text($formula, 57, true);
1084                 $comment->stranswers[$key] = $formula . ' = ' . $formattedanswer->answer . '<br/>';
1085                 $correcttrue = new stdClass();
1086                 $correcttrue->correct = $formattedanswer->answer;
1087                 $correcttrue->true = '';
1088                 if ($formattedanswer->answer < $answer->min ||
1089                         $formattedanswer->answer > $answer->max) {
1090                     $comment->outsidelimit = true;
1091                     $comment->answers[$key] = $key;
1092                     $comment->stranswers[$key] .=
1093                             get_string('trueansweroutsidelimits', 'qtype_calculated', $correcttrue);
1094                 } else {
1095                     $comment->stranswers[$key] .=
1096                             get_string('trueanswerinsidelimits', 'qtype_calculated', $correcttrue);
1097                 }
1098                 $comment->stranswers[$key] .= '<br/>';
1099                 $comment->stranswers[$key] .= get_string('min', 'qtype_calculated') .
1100                         $delimiter . $answer->min . ' --- ';
1101                 $comment->stranswers[$key] .= get_string('max', 'qtype_calculated') .
1102                         $delimiter . $answer->max;
1103             }
1104         }
1105         return fullclone($comment);
1106     }
1108     public function tolerance_types() {
1109         return array(
1110             '1' => get_string('relative', 'qtype_numerical'),
1111             '2' => get_string('nominal', 'qtype_numerical'),
1112             '3' => get_string('geometric', 'qtype_numerical')
1113         );
1114     }
1116     public function dataset_options($form, $name, $mandatory = true,
1117             $renameabledatasets = false) {
1118         // Takes datasets from the parent implementation but
1119         // filters options that are currently not accepted by calculated.
1120         // It also determines a default selection.
1121         // Param $renameabledatasets not implemented anywhere.
1123         list($options, $selected) = $this->dataset_options_from_database(
1124                 $form, $name, '', 'qtype_calculated');
1126         foreach ($options as $key => $whatever) {
1127             if (!preg_match('~^1-~', $key) && $key != '0') {
1128                 unset($options[$key]);
1129             }
1130         }
1131         if (!$selected) {
1132             if ($mandatory) {
1133                 $selected =  "1-0-$name"; // Default.
1134             } else {
1135                 $selected = '0'; // Default.
1136             }
1137         }
1138         return array($options, $selected);
1139     }
1141     public function construct_dataset_menus($form, $mandatorydatasets,
1142             $optionaldatasets) {
1143         global $OUTPUT;
1144         $datasetmenus = array();
1145         foreach ($mandatorydatasets as $datasetname) {
1146             if (!isset($datasetmenus[$datasetname])) {
1147                 list($options, $selected) =
1148                     $this->dataset_options($form, $datasetname);
1149                 unset($options['0']); // Mandatory...
1150                 $datasetmenus[$datasetname] = html_writer::select(
1151                         $options, 'dataset[]', $selected, null);
1152             }
1153         }
1154         foreach ($optionaldatasets as $datasetname) {
1155             if (!isset($datasetmenus[$datasetname])) {
1156                 list($options, $selected) =
1157                     $this->dataset_options($form, $datasetname);
1158                 $datasetmenus[$datasetname] = html_writer::select(
1159                         $options, 'dataset[]', $selected, null);
1160             }
1161         }
1162         return $datasetmenus;
1163     }
1165     public function substitute_variables($str, $dataset) {
1166         global $OUTPUT;
1167         // Testing for wrong numerical values.
1168         // All calculations used this function so testing here should be OK.
1170         foreach ($dataset as $name => $value) {
1171             $val = $value;
1172             if (! is_numeric($val)) {
1173                 $a = new stdClass();
1174                 $a->name = '{'.$name.'}';
1175                 $a->value = $value;
1176                 echo $OUTPUT->notification(get_string('notvalidnumber', 'qtype_calculated', $a));
1177                 $val = 1.0;
1178             }
1179             if ($val <= 0) { // MDL-36025 Use parentheses for "-0" .
1180                 $str = str_replace('{'.$name.'}', '('.$val.')', $str);
1181             } else {
1182                 $str = str_replace('{'.$name.'}', $val, $str);
1183             }
1184         }
1185         return $str;
1186     }
1188     public function evaluate_equations($str, $dataset) {
1189         $formula = $this->substitute_variables($str, $dataset);
1190         if ($error = qtype_calculated_find_formula_errors($formula)) {
1191             return $error;
1192         }
1193         return $str;
1194     }
1196     public function substitute_variables_and_eval($str, $dataset) {
1197         $formula = $this->substitute_variables($str, $dataset);
1198         if ($error = qtype_calculated_find_formula_errors($formula)) {
1199             return $error;
1200         }
1201         // Calculate the correct answer.
1202         if (empty($formula)) {
1203             $str = '';
1204         } else if ($formula === '*') {
1205             $str = '*';
1206         } else {
1207             $str = null;
1208             eval('$str = '.$formula.';');
1209         }
1210         return $str;
1211     }
1213     public function get_dataset_definitions($questionid, $newdatasets) {
1214         global $DB;
1215         // Get the existing datasets for this question.
1216         $datasetdefs = array();
1217         if (!empty($questionid)) {
1218             global $CFG;
1219             $sql = "SELECT i.*
1220                       FROM {question_datasets} d, {question_dataset_definitions} i
1221                      WHERE d.question = ? AND d.datasetdefinition = i.id
1222                   ORDER BY i.id";
1223             if ($records = $DB->get_records_sql($sql, array($questionid))) {
1224                 foreach ($records as $r) {
1225                     $datasetdefs["$r->type-$r->category-$r->name"] = $r;
1226                 }
1227             }
1228         }
1230         foreach ($newdatasets as $dataset) {
1231             if (!$dataset) {
1232                 continue; // The no dataset case...
1233             }
1235             if (!isset($datasetdefs[$dataset])) {
1236                 // Make new datasetdef.
1237                 list($type, $category, $name) = explode('-', $dataset, 3);
1238                 $datasetdef = new stdClass();
1239                 $datasetdef->type = $type;
1240                 $datasetdef->name = $name;
1241                 $datasetdef->category  = $category;
1242                 $datasetdef->itemcount = 0;
1243                 $datasetdef->options   = 'uniform:1.0:10.0:1';
1244                 $datasetdefs[$dataset] = clone($datasetdef);
1245             }
1246         }
1247         return $datasetdefs;
1248     }
1250     public function save_dataset_definitions($form) {
1251         global $DB;
1252         // Save synchronize.
1254         if (empty($form->dataset)) {
1255             $form->dataset = array();
1256         }
1257         // Save datasets.
1258         $datasetdefinitions = $this->get_dataset_definitions($form->id, $form->dataset);
1259         $tmpdatasets = array_flip($form->dataset);
1260         $defids = array_keys($datasetdefinitions);
1261         foreach ($defids as $defid) {
1262             $datasetdef = &$datasetdefinitions[$defid];
1263             if (isset($datasetdef->id)) {
1264                 if (!isset($tmpdatasets[$defid])) {
1265                     // This dataset is not used any more, delete it.
1266                     $DB->delete_records('question_datasets',
1267                             array('question' => $form->id, 'datasetdefinition' => $datasetdef->id));
1268                     if ($datasetdef->category == 0) {
1269                         // Question local dataset.
1270                         $DB->delete_records('question_dataset_definitions',
1271                                 array('id' => $datasetdef->id));
1272                         $DB->delete_records('question_dataset_items',
1273                                 array('definition' => $datasetdef->id));
1274                     }
1275                 }
1276                 // This has already been saved or just got deleted.
1277                 unset($datasetdefinitions[$defid]);
1278                 continue;
1279             }
1281             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
1283             if (0 != $datasetdef->category) {
1284                 // We need to look for already existing datasets in the category.
1285                 // First creating the datasetdefinition above
1286                 // then we can manage to automatically take care of some possible realtime concurrence.
1288                 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
1289                         'type = ? AND name = ? AND category = ? AND id < ?
1290                         ORDER BY id DESC',
1291                         array($datasetdef->type, $datasetdef->name,
1292                                 $datasetdef->category, $datasetdef->id))) {
1294                     while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1295                         $DB->delete_records('question_dataset_definitions',
1296                                 array('id' => $datasetdef->id));
1297                         $datasetdef = $olderdatasetdef;
1298                     }
1299                 }
1300             }
1302             // Create relation to this dataset.
1303             $questiondataset = new stdClass();
1304             $questiondataset->question = $form->id;
1305             $questiondataset->datasetdefinition = $datasetdef->id;
1306             $DB->insert_record('question_datasets', $questiondataset);
1307             unset($datasetdefinitions[$defid]);
1308         }
1310         // Remove local obsolete datasets as well as relations
1311         // to datasets in other categories.
1312         if (!empty($datasetdefinitions)) {
1313             foreach ($datasetdefinitions as $def) {
1314                 $DB->delete_records('question_datasets',
1315                         array('question' => $form->id, 'datasetdefinition' => $def->id));
1317                 if ($def->category == 0) { // Question local dataset.
1318                     $DB->delete_records('question_dataset_definitions',
1319                             array('id' => $def->id));
1320                     $DB->delete_records('question_dataset_items',
1321                             array('definition' => $def->id));
1322                 }
1323             }
1324         }
1325     }
1326     /** This function create a copy of the datasets (definition and dataitems)
1327      * from the preceding question if they remain in the new question
1328      * otherwise its create the datasets that have been added as in the
1329      * save_dataset_definitions()
1330      */
1331     public function save_as_new_dataset_definitions($form, $initialid) {
1332         global $CFG, $DB;
1333         // Get the datasets from the intial question.
1334         $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset);
1335         // Param $tmpdatasets contains those of the new question.
1336         $tmpdatasets = array_flip($form->dataset);
1337         $defids = array_keys($datasetdefinitions);// New datasets.
1338         foreach ($defids as $defid) {
1339             $datasetdef = &$datasetdefinitions[$defid];
1340             if (isset($datasetdef->id)) {
1341                 // This dataset exist in the initial question.
1342                 if (!isset($tmpdatasets[$defid])) {
1343                     // Do not exist in the new question so ignore.
1344                     unset($datasetdefinitions[$defid]);
1345                     continue;
1346                 }
1347                 // Create a copy but not for category one.
1348                 if (0 == $datasetdef->category) {
1349                     $olddatasetid = $datasetdef->id;
1350                     $olditemcount = $datasetdef->itemcount;
1351                     $datasetdef->itemcount = 0;
1352                     $datasetdef->id = $DB->insert_record('question_dataset_definitions',
1353                             $datasetdef);
1354                     // Copy the dataitems.
1355                     $olditems = $this->get_database_dataset_items($olddatasetid);
1356                     if (count($olditems) > 0) {
1357                         $itemcount = 0;
1358                         foreach ($olditems as $item) {
1359                             $item->definition = $datasetdef->id;
1360                             $DB->insert_record('question_dataset_items', $item);
1361                             $itemcount++;
1362                         }
1363                         // Update item count to olditemcount if
1364                         // at least this number of items has been recover from the database.
1365                         if ($olditemcount <= $itemcount) {
1366                             $datasetdef->itemcount = $olditemcount;
1367                         } else {
1368                             $datasetdef->itemcount = $itemcount;
1369                         }
1370                         $DB->update_record('question_dataset_definitions', $datasetdef);
1371                     } // End of  copy the dataitems.
1372                 }// End of  copy the datasetdef.
1373                 // Create relation to the new question with this
1374                 // copy as new datasetdef from the initial question.
1375                 $questiondataset = new stdClass();
1376                 $questiondataset->question = $form->id;
1377                 $questiondataset->datasetdefinition = $datasetdef->id;
1378                 $DB->insert_record('question_datasets', $questiondataset);
1379                 unset($datasetdefinitions[$defid]);
1380                 continue;
1381             }// End of datasetdefs from the initial question.
1382             // Really new one code similar to save_dataset_definitions().
1383             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
1385             if (0 != $datasetdef->category) {
1386                 // We need to look for already existing
1387                 // datasets in the category.
1388                 // By first creating the datasetdefinition above we
1389                 // can manage to automatically take care of
1390                 // some possible realtime concurrence.
1391                 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
1392                         "type = ? AND name = ? AND category = ? AND id < ?
1393                         ORDER BY id DESC",
1394                         array($datasetdef->type, $datasetdef->name,
1395                                 $datasetdef->category, $datasetdef->id))) {
1397                     while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1398                         $DB->delete_records('question_dataset_definitions',
1399                                 array('id' => $datasetdef->id));
1400                         $datasetdef = $olderdatasetdef;
1401                     }
1402                 }
1403             }
1405             // Create relation to this dataset.
1406             $questiondataset = new stdClass();
1407             $questiondataset->question = $form->id;
1408             $questiondataset->datasetdefinition = $datasetdef->id;
1409             $DB->insert_record('question_datasets', $questiondataset);
1410             unset($datasetdefinitions[$defid]);
1411         }
1413         // Remove local obsolete datasets as well as relations
1414         // to datasets in other categories.
1415         if (!empty($datasetdefinitions)) {
1416             foreach ($datasetdefinitions as $def) {
1417                 $DB->delete_records('question_datasets',
1418                         array('question' => $form->id, 'datasetdefinition' => $def->id));
1420                 if ($def->category == 0) { // Question local dataset.
1421                     $DB->delete_records('question_dataset_definitions',
1422                             array('id' => $def->id));
1423                     $DB->delete_records('question_dataset_items',
1424                             array('definition' => $def->id));
1425                 }
1426             }
1427         }
1428     }
1430     // Dataset functionality.
1431     public function pick_question_dataset($question, $datasetitem) {
1432         // Select a dataset in the following format:
1433         // an array indexed by the variable names (d.name) pointing to the value
1434         // to be substituted.
1435         global $CFG, $DB;
1436         if (!$dataitems = $DB->get_records_sql(
1437                 "SELECT i.id, d.name, i.value
1438                    FROM {question_dataset_definitions} d,
1439                         {question_dataset_items} i,
1440                         {question_datasets} q
1441                   WHERE q.question = ?
1442                     AND q.datasetdefinition = d.id
1443                     AND d.id = i.definition
1444                     AND i.itemnumber = ?
1445                ORDER BY i.id DESC ", array($question->id, $datasetitem))) {
1446             $a = new stdClass();
1447             $a->id = $question->id;
1448             $a->item = $datasetitem;
1449             print_error('cannotgetdsfordependent', 'question', '', $a);
1450         }
1451         $dataset = Array();
1452         foreach ($dataitems as $id => $dataitem) {
1453             if (!isset($dataset[$dataitem->name])) {
1454                 $dataset[$dataitem->name] = $dataitem->value;
1455             }
1456         }
1457         return $dataset;
1458     }
1460     public function dataset_options_from_database($form, $name, $prefix = '',
1461             $langfile = 'qtype_calculated') {
1462         global $CFG, $DB;
1463         $type = 1; // Only type = 1 (i.e. old 'LITERAL') has ever been used.
1464         // First options - it is not a dataset...
1465         $options['0'] = get_string($prefix.'nodataset', $langfile);
1466         // New question no local.
1467         if (!isset($form->id) || $form->id == 0) {
1468             $key = "$type-0-$name";
1469             $options[$key] = get_string($prefix."newlocal$type", $langfile);
1470             $currentdatasetdef = new stdClass();
1471             $currentdatasetdef->type = '0';
1472         } else {
1473             // Construct question local options.
1474             $sql = "SELECT a.*
1475                 FROM {question_dataset_definitions} a, {question_datasets} b
1476                WHERE a.id = b.datasetdefinition AND a.type = '1' AND b.question = ? AND a.name = ?";
1477             $currentdatasetdef = $DB->get_record_sql($sql, array($form->id, $name));
1478             if (!$currentdatasetdef) {
1479                 $currentdatasetdef = new stdClass();
1480                 $currentdatasetdef->type = '0';
1481             }
1482             $key = "$type-0-$name";
1483             if ($currentdatasetdef->type == $type
1484                     and $currentdatasetdef->category == 0) {
1485                 $options[$key] = get_string($prefix."keptlocal$type", $langfile);
1486             } else {
1487                 $options[$key] = get_string($prefix."newlocal$type", $langfile);
1488             }
1489         }
1490         // Construct question category options.
1491         $categorydatasetdefs = $DB->get_records_sql(
1492             "SELECT b.question, a.*
1493             FROM {question_datasets} b,
1494             {question_dataset_definitions} a
1495             WHERE a.id = b.datasetdefinition
1496             AND a.type = '1'
1497             AND a.category = ?
1498             AND a.name = ?", array($form->category, $name));
1499         $type = 1;
1500         $key = "$type-$form->category-$name";
1501         if (!empty($categorydatasetdefs)) {
1502             // There is at least one with the same name.
1503             if (isset($form->id) && isset($categorydatasetdefs[$form->id])) {
1504                 // It is already used by this question.
1505                 $options[$key] = get_string($prefix."keptcategory$type", $langfile);
1506             } else {
1507                 $options[$key] = get_string($prefix."existingcategory$type", $langfile);
1508             }
1509         } else {
1510             $options[$key] = get_string($prefix."newcategory$type", $langfile);
1511         }
1512         // All done!
1513         return array($options, $currentdatasetdef->type
1514             ? "$currentdatasetdef->type-$currentdatasetdef->category-$name"
1515             : '');
1516     }
1518     public function find_dataset_names($text) {
1519         // Returns the possible dataset names found in the text as an array.
1520         // The array has the dataset name for both key and value.
1521         $datasetnames = array();
1522         while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) {
1523             $datasetnames[$regs[1]] = $regs[1];
1524             $text = str_replace($regs[0], '', $text);
1525         }
1526         return $datasetnames;
1527     }
1529     /**
1530      * This function retrieve the item count of the available category shareable
1531      * wild cards that is added as a comment displayed when a wild card with
1532      * the same name is displayed in datasetdefinitions_form.php
1533      */
1534     public function get_dataset_definitions_category($form) {
1535         global $CFG, $DB;
1536         $datasetdefs = array();
1537         $lnamemax = 30;
1538         if (!empty($form->category)) {
1539             $sql = "SELECT i.*, d.*
1540                       FROM {question_datasets} d, {question_dataset_definitions} i
1541                      WHERE i.id = d.datasetdefinition AND i.category = ?";
1542             if ($records = $DB->get_records_sql($sql, array($form->category))) {
1543                 foreach ($records as $r) {
1544                     if (!isset ($datasetdefs["$r->name"])) {
1545                         $datasetdefs["$r->name"] = $r->itemcount;
1546                     }
1547                 }
1548             }
1549         }
1550         return $datasetdefs;
1551     }
1553     /**
1554      * This function build a table showing the available category shareable
1555      * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1556      * and the name of the question where they are used.
1557      * This table is intended to be add before the question text to help the user use
1558      * these wild cards
1559      */
1560     public function print_dataset_definitions_category($form) {
1561         global $CFG, $DB;
1562         $datasetdefs = array();
1563         $lnamemax = 22;
1564         $namestr          = get_string('name');
1565         $rangeofvaluestr  = get_string('minmax', 'qtype_calculated');
1566         $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
1567         $itemscountstr    = get_string('itemscount', 'qtype_calculated');
1568         $text = '';
1569         if (!empty($form->category)) {
1570             list($category) = explode(',', $form->category);
1571             $sql = "SELECT i.*, d.*
1572                 FROM {question_datasets} d,
1573         {question_dataset_definitions} i
1574         WHERE i.id = d.datasetdefinition
1575         AND i.category = ?";
1576             if ($records = $DB->get_records_sql($sql, array($category))) {
1577                 foreach ($records as $r) {
1578                     $sql1 = "SELECT q.*
1579                                FROM {question} q
1580                               WHERE q.id = ?";
1581                     if (!isset ($datasetdefs["$r->type-$r->category-$r->name"])) {
1582                         $datasetdefs["$r->type-$r->category-$r->name"] = $r;
1583                     }
1584                     if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
1585                         if (!isset ($datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question])) {
1586                             $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question] = new stdClass();
1587                         }
1588                         $datasetdefs["$r->type-$r->category-$r->name"]->questions[
1589                                 $r->question]->name = $questionb[$r->question]->name;
1590                     }
1591                 }
1592             }
1593         }
1594         if (!empty ($datasetdefs)) {
1596             $text = "<table width=\"100%\" border=\"1\"><tr>
1597                     <th style=\"white-space:nowrap;\" class=\"header\"
1598                             scope=\"col\">$namestr</th>
1599                     <th style=\"white-space:nowrap;\" class=\"header\"
1600                             scope=\"col\">$rangeofvaluestr</th>
1601                     <th style=\"white-space:nowrap;\" class=\"header\"
1602                             scope=\"col\">$itemscountstr</th>
1603                     <th style=\"white-space:nowrap;\" class=\"header\"
1604                             scope=\"col\">$questionusingstr</th>
1605                     </tr>";
1606             foreach ($datasetdefs as $datasetdef) {
1607                 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4);
1608                 $text .= "<tr>
1609                         <td valign=\"top\" align=\"center\">$datasetdef->name</td>
1610                         <td align=\"center\" valign=\"top\">$min <strong>-</strong> $max</td>
1611                         <td align=\"right\" valign=\"top\">$datasetdef->itemcount&nbsp;&nbsp;</td>
1612                         <td align=\"left\">";
1613                 foreach ($datasetdef->questions as $qu) {
1614                     // Limit the name length displayed.
1615                     if (!empty($qu->name)) {
1616                         $qu->name = (strlen($qu->name) > $lnamemax) ?
1617                             substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1618                     } else {
1619                         $qu->name = '';
1620                     }
1621                     $text .= " &nbsp;&nbsp; $qu->name <br/>";
1622                 }
1623                 $text .= "</td></tr>";
1624             }
1625             $text .= "</table>";
1626         } else {
1627             $text .= get_string('nosharedwildcard', 'qtype_calculated');
1628         }
1629         return $text;
1630     }
1632     /**
1633      * This function build a table showing the available category shareable
1634      * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1635      * and the name of the question where they are used.
1636      * This table is intended to be add before the question text to help the user use
1637      * these wild cards
1638      */
1640     public function print_dataset_definitions_category_shared($question, $datasetdefsq) {
1641         global $CFG, $DB;
1642         $datasetdefs = array();
1643         $lnamemax = 22;
1644         $namestr          = get_string('name', 'quiz');
1645         $rangeofvaluestr  = get_string('minmax', 'qtype_calculated');
1646         $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
1647         $itemscountstr    = get_string('itemscount', 'qtype_calculated');
1648         $text = '';
1649         if (!empty($question->category)) {
1650             list($category) = explode(',', $question->category);
1651             $sql = "SELECT i.*, d.*
1652                       FROM {question_datasets} d, {question_dataset_definitions} i
1653                      WHERE i.id = d.datasetdefinition AND i.category = ?";
1654             if ($records = $DB->get_records_sql($sql, array($category))) {
1655                 foreach ($records as $r) {
1656                     $key = "$r->type-$r->category-$r->name";
1657                     $sql1 = "SELECT q.*
1658                                FROM {question} q
1659                               WHERE q.id = ?";
1660                     if (!isset($datasetdefs[$key])) {
1661                         $datasetdefs[$key] = $r;
1662                     }
1663                     if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
1664                         $datasetdefs[$key]->questions[$r->question] = new stdClass();
1665                         $datasetdefs[$key]->questions[$r->question]->name =
1666                                 $questionb[$r->question]->name;
1667                         $datasetdefs[$key]->questions[$r->question]->id =
1668                                 $questionb[$r->question]->id;
1669                     }
1670                 }
1671             }
1672         }
1673         if (!empty ($datasetdefs)) {
1675             $text  = "<table width=\"100%\" border=\"1\"><tr>
1676                     <th style=\"white-space:nowrap;\" class=\"header\"
1677                             scope=\"col\">$namestr</th>";
1678             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1679                     scope=\"col\">$itemscountstr</th>";
1680             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1681                     scope=\"col\">&nbsp;&nbsp;$questionusingstr &nbsp;&nbsp;</th>";
1682             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1683                     scope=\"col\">Quiz</th>";
1684             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1685                     scope=\"col\">Attempts</th></tr>";
1686             foreach ($datasetdefs as $datasetdef) {
1687                 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4);
1688                 $count = count($datasetdef->questions);
1689                 $text .= "<tr>
1690                         <td style=\"white-space:nowrap;\" valign=\"top\"
1691                                 align=\"center\" rowspan=\"$count\"> $datasetdef->name </td>
1692                         <td align=\"right\" valign=\"top\"
1693                                 rowspan=\"$count\">$datasetdef->itemcount</td>";
1694                 $line = 0;
1695                 foreach ($datasetdef->questions as $qu) {
1696                     // Limit the name length displayed.
1697                     if (!empty($qu->name)) {
1698                         $qu->name = (strlen($qu->name) > $lnamemax) ?
1699                             substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1700                     } else {
1701                         $qu->name = '';
1702                     }
1703                     if ($line) {
1704                         $text .= "<tr>";
1705                     }
1706                     $line++;
1707                     $text .= "<td align=\"left\" style=\"white-space:nowrap;\">$qu->name</td>";
1708                     // TODO MDL-43779 should not have quiz-specific code here.
1709                     $nbofquiz = $DB->count_records('quiz_question_instances', array('questionid' => $qu->id));
1710                     $nbofattempts = $DB->count_records_sql("
1711                             SELECT count(1)
1712                               FROM {quiz_question_instances} qqi
1713                               JOIN {quiz_attempts} quiza ON quiza.quiz = qqi.quizid
1714                              WHERE qqi.questionid = ?
1715                                AND quiza.preview = 0", array($qu->id));
1716                     if ($nbofquiz > 0) {
1717                         $text .= "<td align=\"center\">$nbofquiz</td>";
1718                         $text .= "<td align=\"center\">$nbofattempts";
1719                     } else {
1720                         $text .= "<td align=\"center\">0</td>";
1721                         $text .= "<td align=\"left\"><br/>";
1722                     }
1724                     $text .= "</td></tr>";
1725                 }
1726             }
1727             $text .= "</table>";
1728         } else {
1729             $text .= get_string('nosharedwildcard', 'qtype_calculated');
1730         }
1731         return $text;
1732     }
1734     public function find_math_equations($text) {
1735         // Returns the possible dataset names found in the text as an array.
1736         // The array has the dataset name for both key and value.
1737         $equations = array();
1738         while (preg_match('~\{=([^[:space:]}]*)}~', $text, $regs)) {
1739             $equations[] = $regs[1];
1740             $text = str_replace($regs[0], '', $text);
1741         }
1742         return $equations;
1743     }
1745     public function get_virtual_qtype() {
1746         return question_bank::get_qtype('numerical');
1747     }
1749     public function get_possible_responses($questiondata) {
1750         $responses = array();
1752         $virtualqtype = $this->get_virtual_qtype();
1753         $unit = $virtualqtype->get_default_numerical_unit($questiondata);
1755         $tolerancetypes = $this->tolerance_types();
1757         $starfound = false;
1758         foreach ($questiondata->options->answers as $aid => $answer) {
1759             $responseclass = $answer->answer;
1761             if ($responseclass === '*') {
1762                 $starfound = true;
1763             } else {
1764                 $a = new stdClass();
1765                 $a->answer = $virtualqtype->add_unit($questiondata, $responseclass, $unit);
1766                 $a->tolerance = $answer->tolerance;
1767                 $a->tolerancetype = $tolerancetypes[$answer->tolerancetype];
1769                 $responseclass = get_string('answerwithtolerance', 'qtype_calculated', $a);
1770             }
1772             $responses[$aid] = new question_possible_response($responseclass,
1773                     $answer->fraction);
1774         }
1776         if (!$starfound) {
1777             $responses[0] = new question_possible_response(
1778             get_string('didnotmatchanyanswer', 'question'), 0);
1779         }
1781         $responses[null] = question_possible_response::no_response();
1783         return array($questiondata->id => $responses);
1784     }
1786     public function move_files($questionid, $oldcontextid, $newcontextid) {
1787         $fs = get_file_storage();
1789         parent::move_files($questionid, $oldcontextid, $newcontextid);
1790         $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid);
1791         $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
1792     }
1794     protected function delete_files($questionid, $contextid) {
1795         $fs = get_file_storage();
1797         parent::delete_files($questionid, $contextid);
1798         $this->delete_files_in_answers($questionid, $contextid);
1799         $this->delete_files_in_hints($questionid, $contextid);
1800     }
1804 function qtype_calculated_calculate_answer($formula, $individualdata,
1805     $tolerance, $tolerancetype, $answerlength, $answerformat = '1', $unit = '') {
1806     // The return value has these properties: .
1807     // ->answer    the correct answer
1808     // ->min       the lower bound for an acceptable response
1809     // ->max       the upper bound for an accetpable response.
1810     $calculated = new stdClass();
1811     // Exchange formula variables with the correct values...
1812     $answer = question_bank::get_qtype('calculated')->substitute_variables_and_eval(
1813             $formula, $individualdata);
1814     if (!is_numeric($answer)) {
1815         // Something went wrong, so just return NaN.
1816         $calculated->answer = NAN;
1817         return $calculated;
1818     }
1819     if ('1' == $answerformat) { // Answer is to have $answerlength decimals.
1820         // Decimal places.
1821         $calculated->answer = sprintf('%.' . $answerlength . 'F', $answer);
1823     } else if ($answer) { // Significant figures does only apply if the result is non-zero.
1825         // Convert to positive answer...
1826         if ($answer < 0) {
1827             $answer = -$answer;
1828             $sign = '-';
1829         } else {
1830             $sign = '';
1831         }
1833         // Determine the format 0.[1-9][0-9]* for the answer...
1834         $p10 = 0;
1835         while ($answer < 1) {
1836             --$p10;
1837             $answer *= 10;
1838         }
1839         while ($answer >= 1) {
1840             ++$p10;
1841             $answer /= 10;
1842         }
1843         // ... and have the answer rounded of to the correct length.
1844         $answer = round($answer, $answerlength);
1846         // If we rounded up to 1.0, place the answer back into 0.[1-9][0-9]* format.
1847         if ($answer >= 1) {
1848             ++$p10;
1849             $answer /= 10;
1850         }
1852         // Have the answer written on a suitable format:
1853         // either scientific or plain numeric.
1854         if (-2 > $p10 || 4 < $p10) {
1855             // Use scientific format.
1856             $exponent = 'e'.--$p10;
1857             $answer *= 10;
1858             if (1 == $answerlength) {
1859                 $calculated->answer = $sign.$answer.$exponent;
1860             } else {
1861                 // Attach additional zeros at the end of $answer.
1862                 $answer .= (1 == strlen($answer) ? '.' : '')
1863                     . '00000000000000000000000000000000000000000x';
1864                 $calculated->answer = $sign
1865                     .substr($answer, 0, $answerlength +1).$exponent;
1866             }
1867         } else {
1868             // Stick to plain numeric format.
1869             $answer *= "1e$p10";
1870             if (0.1 <= $answer / "1e$answerlength") {
1871                 $calculated->answer = $sign.$answer;
1872             } else {
1873                 // Could be an idea to add some zeros here.
1874                 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '')
1875                     . '00000000000000000000000000000000000000000x';
1876                 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
1877                 $calculated->answer = $sign.substr($answer, 0, $oklen);
1878             }
1879         }
1881     } else {
1882         $calculated->answer = 0.0;
1883     }
1884     if ($unit != '') {
1885             $calculated->answer = $calculated->answer . ' ' . $unit;
1886     }
1888     // Return the result.
1889     return $calculated;
1893 function qtype_calculated_find_formula_errors($formula) {
1894     // Validates the formula submitted from the question edit page.
1895     // Returns false if everything is alright
1896     // otherwise it constructs an error message.
1897     // Strip away dataset names.
1898     while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
1899         $formula = str_replace($regs[0], '1', $formula);
1900     }
1902     // Strip away empty space and lowercase it.
1903     $formula = strtolower(str_replace(' ', '', $formula));
1905     $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */
1906     $operatorornumber = "[$safeoperatorchar.0-9eE]";
1908     while (preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)" .
1909             "\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~",
1910         $formula, $regs)) {
1911         switch ($regs[2]) {
1912             // Simple parenthesis.
1913             case '':
1914                 if ((isset($regs[4]) && $regs[4]) || strlen($regs[3]) == 0) {
1915                     return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
1916                 }
1917                 break;
1919                 // Zero argument functions.
1920             case 'pi':
1921                 if ($regs[3]) {
1922                     return get_string('functiontakesnoargs', 'qtype_calculated', $regs[2]);
1923                 }
1924                 break;
1926                 // Single argument functions (the most common case).
1927             case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
1928             case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
1929             case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
1930             case 'exp': case 'expm1': case 'floor': case 'is_finite':
1931             case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
1932             case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
1933             case 'tan': case 'tanh':
1934                 if (!empty($regs[4]) || empty($regs[3])) {
1935                     return get_string('functiontakesonearg', 'qtype_calculated', $regs[2]);
1936                 }
1937                 break;
1939                 // Functions that take one or two arguments.
1940             case 'log': case 'round':
1941                 if (!empty($regs[5]) || empty($regs[3])) {
1942                     return get_string('functiontakesoneortwoargs', 'qtype_calculated', $regs[2]);
1943                 }
1944                 break;
1946                 // Functions that must have two arguments.
1947             case 'atan2': case 'fmod': case 'pow':
1948                 if (!empty($regs[5]) || empty($regs[4])) {
1949                     return get_string('functiontakestwoargs', 'qtype_calculated', $regs[2]);
1950                 }
1951                 break;
1953                 // Functions that take two or more arguments.
1954             case 'min': case 'max':
1955                 if (empty($regs[4])) {
1956                     return get_string('functiontakesatleasttwo', 'qtype_calculated', $regs[2]);
1957                 }
1958                 break;
1960             default:
1961                 return get_string('unsupportedformulafunction', 'qtype_calculated', $regs[2]);
1962         }
1964         // Exchange the function call with '1' and then check for
1965         // another function call...
1966         if ($regs[1]) {
1967             // The function call is proceeded by an operator.
1968             $formula = str_replace($regs[0], $regs[1] . '1', $formula);
1969         } else {
1970             // The function call starts the formula.
1971             $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula);
1972         }
1973     }
1975     if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) {
1976         return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
1977     } else {
1978         // Formula just might be valid.
1979         return false;
1980     }