Merge branch 'wip-MDL-40136-m26' of git://github.com/samhemelryk/moodle
[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             if (is_string($answer)) {
1036                 $strheader .= $delimiter.$answer;
1037             } else {
1038                 $strheader .= $delimiter.$answer->answer;
1039             }
1040             $delimiter = '<br/><br/><br/>';
1041         }
1042         return $strheader;
1043     }
1045     public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext,
1046             $answers, $data, $number) {
1047         global $DB;
1048         $comment = new stdClass();
1049         $comment->stranswers = array();
1050         $comment->outsidelimit = false;
1051         $comment->answers = array();
1052         // Find a default unit.
1053         if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units',
1054                 array('question' => $questionid, 'multiplier' => 1.0))) {
1055             $unit = $unit->unit;
1056         } else {
1057             $unit = '';
1058         }
1060         $answers = fullclone($answers);
1061         $errors = '';
1062         $delimiter = ': ';
1063         $virtualqtype =  $qtypeobj->get_virtual_qtype();
1064         foreach ($answers as $key => $answer) {
1065             $formula = $this->substitute_variables($answer->answer, $data);
1066             $formattedanswer = qtype_calculated_calculate_answer(
1067                 $answer->answer, $data, $answer->tolerance,
1068                 $answer->tolerancetype, $answer->correctanswerlength,
1069                 $answer->correctanswerformat, $unit);
1070             if ($formula === '*') {
1071                 $answer->min = ' ';
1072                 $formattedanswer->answer = $answer->answer;
1073             } else {
1074                 eval('$ansvalue = '.$formula.';');
1075                 $ans = new qtype_numerical_answer(0, $ansvalue, 0, '', 0, $answer->tolerance);
1076                 $ans->tolerancetype = $answer->tolerancetype;
1077                 list($answer->min, $answer->max) = $ans->get_tolerance_interval($answer);
1078             }
1079             if ($answer->min === '') {
1080                 // This should mean that something is wrong.
1081                 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
1082             } else if ($formula === '*') {
1083                 $comment->stranswers[$key] = $formula . ' = ' .
1084                         get_string('anyvalue', 'qtype_calculated') . '<br/><br/><br/>';
1085             } else {
1086                 $comment->stranswers[$key] = $formula . ' = ' . $formattedanswer->answer . '<br/>';
1087                 $correcttrue = new stdClass();
1088                 $correcttrue->correct = $formattedanswer->answer;
1089                 $correcttrue->true = $answer->answer;
1090                 if ($formattedanswer->answer < $answer->min ||
1091                         $formattedanswer->answer > $answer->max) {
1092                     $comment->outsidelimit = true;
1093                     $comment->answers[$key] = $key;
1094                     $comment->stranswers[$key] .=
1095                             get_string('trueansweroutsidelimits', 'qtype_calculated', $correcttrue);
1096                 } else {
1097                     $comment->stranswers[$key] .=
1098                             get_string('trueanswerinsidelimits', 'qtype_calculated', $correcttrue);
1099                 }
1100                 $comment->stranswers[$key] .= '<br/>';
1101                 $comment->stranswers[$key] .= get_string('min', 'qtype_calculated') .
1102                         $delimiter . $answer->min . ' --- ';
1103                 $comment->stranswers[$key] .= get_string('max', 'qtype_calculated') .
1104                         $delimiter . $answer->max;
1105             }
1106         }
1107         return fullclone($comment);
1108     }
1109     public function multichoice_comment_on_datasetitems($questionid, $questiontext,
1110             $answers, $data, $number) {
1111         global $DB;
1112         $comment = new stdClass();
1113         $comment->stranswers = array();
1114         $comment->outsidelimit = false;
1115         $comment->answers = array();
1116         // Find a default unit.
1117         if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units',
1118                 array('question' => $questionid, 'multiplier' => 1.0))) {
1119             $unit = $unit->unit;
1120         } else {
1121             $unit = '';
1122         }
1124         $answers = fullclone($answers);
1125         $errors = '';
1126         $delimiter = ': ';
1127         foreach ($answers as $key => $answer) {
1128             $answer->answer = $this->substitute_variables($answer->answer, $data);
1129             // Evaluate the equations i.e {=5+4).
1130             $qtext = '';
1131             $qtextremaining = $answer->answer;
1132             while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
1133                 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
1134                 $qtext = $qtext.$qtextsplits[0];
1135                 $qtextremaining = $qtextsplits[1];
1136                 if (empty($regs1[1])) {
1137                     $str = '';
1138                 } else {
1139                     if ($formulaerrors = qtype_calculated_find_formula_errors($regs1[1])) {
1140                         $str = $formulaerrors;
1141                     } else {
1142                         eval('$str = '.$regs1[1].';');
1144                         $texteval= qtype_calculated_calculate_answer(
1145                             $str, $data, $answer->tolerance,
1146                             $answer->tolerancetype, $answer->correctanswerlength,
1147                             $answer->correctanswerformat, '');
1148                         $str = $texteval->answer;
1150                     }
1151                 }
1152                 $qtext = $qtext.$str;
1153             }
1154             $answer->answer = $qtext.$qtextremaining;
1155             $comment->stranswers[$key]= $answer->answer;
1157         }
1158         return fullclone($comment);
1159     }
1161     public function tolerance_types() {
1162         return array(
1163             '1' => get_string('relative', 'qtype_numerical'),
1164             '2' => get_string('nominal', 'qtype_numerical'),
1165             '3' => get_string('geometric', 'qtype_numerical')
1166         );
1167     }
1169     public function dataset_options($form, $name, $mandatory = true,
1170             $renameabledatasets = false) {
1171         // Takes datasets from the parent implementation but
1172         // filters options that are currently not accepted by calculated.
1173         // It also determines a default selection.
1174         // Param $renameabledatasets not implemented anywhere.
1176         list($options, $selected) = $this->dataset_options_from_database(
1177                 $form, $name, '', 'qtype_calculated');
1179         foreach ($options as $key => $whatever) {
1180             if (!preg_match('~^1-~', $key) && $key != '0') {
1181                 unset($options[$key]);
1182             }
1183         }
1184         if (!$selected) {
1185             if ($mandatory) {
1186                 $selected =  "1-0-$name"; // Default.
1187             } else {
1188                 $selected = '0'; // Default.
1189             }
1190         }
1191         return array($options, $selected);
1192     }
1194     public function construct_dataset_menus($form, $mandatorydatasets,
1195             $optionaldatasets) {
1196         global $OUTPUT;
1197         $datasetmenus = array();
1198         foreach ($mandatorydatasets as $datasetname) {
1199             if (!isset($datasetmenus[$datasetname])) {
1200                 list($options, $selected) =
1201                     $this->dataset_options($form, $datasetname);
1202                 unset($options['0']); // Mandatory...
1203                 $datasetmenus[$datasetname] = html_writer::select(
1204                         $options, 'dataset[]', $selected, null);
1205             }
1206         }
1207         foreach ($optionaldatasets as $datasetname) {
1208             if (!isset($datasetmenus[$datasetname])) {
1209                 list($options, $selected) =
1210                     $this->dataset_options($form, $datasetname);
1211                 $datasetmenus[$datasetname] = html_writer::select(
1212                         $options, 'dataset[]', $selected, null);
1213             }
1214         }
1215         return $datasetmenus;
1216     }
1218     public function substitute_variables($str, $dataset) {
1219         global $OUTPUT;
1220         // Testing for wrong numerical values.
1221         // All calculations used this function so testing here should be OK.
1223         foreach ($dataset as $name => $value) {
1224             $val = $value;
1225             if (! is_numeric($val)) {
1226                 $a = new stdClass();
1227                 $a->name = '{'.$name.'}';
1228                 $a->value = $value;
1229                 echo $OUTPUT->notification(get_string('notvalidnumber', 'qtype_calculated', $a));
1230                 $val = 1.0;
1231             }
1232             if ($val <= 0) { // MDL-36025 Use parentheses for "-0" .
1233                 $str = str_replace('{'.$name.'}', '('.$val.')', $str);
1234             } else {
1235                 $str = str_replace('{'.$name.'}', $val, $str);
1236             }
1237         }
1238         return $str;
1239     }
1241     public function evaluate_equations($str, $dataset) {
1242         $formula = $this->substitute_variables($str, $dataset);
1243         if ($error = qtype_calculated_find_formula_errors($formula)) {
1244             return $error;
1245         }
1246         return $str;
1247     }
1249     public function substitute_variables_and_eval($str, $dataset) {
1250         $formula = $this->substitute_variables($str, $dataset);
1251         if ($error = qtype_calculated_find_formula_errors($formula)) {
1252             return $error;
1253         }
1254         // Calculate the correct answer.
1255         if (empty($formula)) {
1256             $str = '';
1257         } else if ($formula === '*') {
1258             $str = '*';
1259         } else {
1260             $str = null;
1261             eval('$str = '.$formula.';');
1262         }
1263         return $str;
1264     }
1266     public function get_dataset_definitions($questionid, $newdatasets) {
1267         global $DB;
1268         // Get the existing datasets for this question.
1269         $datasetdefs = array();
1270         if (!empty($questionid)) {
1271             global $CFG;
1272             $sql = "SELECT i.*
1273                       FROM {question_datasets} d, {question_dataset_definitions} i
1274                      WHERE d.question = ? AND d.datasetdefinition = i.id
1275                   ORDER BY i.id";
1276             if ($records = $DB->get_records_sql($sql, array($questionid))) {
1277                 foreach ($records as $r) {
1278                     $datasetdefs["$r->type-$r->category-$r->name"] = $r;
1279                 }
1280             }
1281         }
1283         foreach ($newdatasets as $dataset) {
1284             if (!$dataset) {
1285                 continue; // The no dataset case...
1286             }
1288             if (!isset($datasetdefs[$dataset])) {
1289                 // Make new datasetdef.
1290                 list($type, $category, $name) = explode('-', $dataset, 3);
1291                 $datasetdef = new stdClass();
1292                 $datasetdef->type = $type;
1293                 $datasetdef->name = $name;
1294                 $datasetdef->category  = $category;
1295                 $datasetdef->itemcount = 0;
1296                 $datasetdef->options   = 'uniform:1.0:10.0:1';
1297                 $datasetdefs[$dataset] = clone($datasetdef);
1298             }
1299         }
1300         return $datasetdefs;
1301     }
1303     public function save_dataset_definitions($form) {
1304         global $DB;
1305         // Save synchronize.
1307         if (empty($form->dataset)) {
1308             $form->dataset = array();
1309         }
1310         // Save datasets.
1311         $datasetdefinitions = $this->get_dataset_definitions($form->id, $form->dataset);
1312         $tmpdatasets = array_flip($form->dataset);
1313         $defids = array_keys($datasetdefinitions);
1314         foreach ($defids as $defid) {
1315             $datasetdef = &$datasetdefinitions[$defid];
1316             if (isset($datasetdef->id)) {
1317                 if (!isset($tmpdatasets[$defid])) {
1318                     // This dataset is not used any more, delete it.
1319                     $DB->delete_records('question_datasets',
1320                             array('question' => $form->id, 'datasetdefinition' => $datasetdef->id));
1321                     if ($datasetdef->category == 0) {
1322                         // Question local dataset.
1323                         $DB->delete_records('question_dataset_definitions',
1324                                 array('id' => $datasetdef->id));
1325                         $DB->delete_records('question_dataset_items',
1326                                 array('definition' => $datasetdef->id));
1327                     }
1328                 }
1329                 // This has already been saved or just got deleted.
1330                 unset($datasetdefinitions[$defid]);
1331                 continue;
1332             }
1334             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
1336             if (0 != $datasetdef->category) {
1337                 // We need to look for already existing datasets in the category.
1338                 // First creating the datasetdefinition above
1339                 // then we can manage to automatically take care of some possible realtime concurrence.
1341                 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
1342                         'type = ? AND name = ? AND category = ? AND id < ?
1343                         ORDER BY id DESC',
1344                         array($datasetdef->type, $datasetdef->name,
1345                                 $datasetdef->category, $datasetdef->id))) {
1347                     while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1348                         $DB->delete_records('question_dataset_definitions',
1349                                 array('id' => $datasetdef->id));
1350                         $datasetdef = $olderdatasetdef;
1351                     }
1352                 }
1353             }
1355             // Create relation to this dataset.
1356             $questiondataset = new stdClass();
1357             $questiondataset->question = $form->id;
1358             $questiondataset->datasetdefinition = $datasetdef->id;
1359             $DB->insert_record('question_datasets', $questiondataset);
1360             unset($datasetdefinitions[$defid]);
1361         }
1363         // Remove local obsolete datasets as well as relations
1364         // to datasets in other categories.
1365         if (!empty($datasetdefinitions)) {
1366             foreach ($datasetdefinitions as $def) {
1367                 $DB->delete_records('question_datasets',
1368                         array('question' => $form->id, 'datasetdefinition' => $def->id));
1370                 if ($def->category == 0) { // Question local dataset.
1371                     $DB->delete_records('question_dataset_definitions',
1372                             array('id' => $def->id));
1373                     $DB->delete_records('question_dataset_items',
1374                             array('definition' => $def->id));
1375                 }
1376             }
1377         }
1378     }
1379     /** This function create a copy of the datasets (definition and dataitems)
1380      * from the preceding question if they remain in the new question
1381      * otherwise its create the datasets that have been added as in the
1382      * save_dataset_definitions()
1383      */
1384     public function save_as_new_dataset_definitions($form, $initialid) {
1385         global $CFG, $DB;
1386         // Get the datasets from the intial question.
1387         $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset);
1388         // Param $tmpdatasets contains those of the new question.
1389         $tmpdatasets = array_flip($form->dataset);
1390         $defids = array_keys($datasetdefinitions);// New datasets.
1391         foreach ($defids as $defid) {
1392             $datasetdef = &$datasetdefinitions[$defid];
1393             if (isset($datasetdef->id)) {
1394                 // This dataset exist in the initial question.
1395                 if (!isset($tmpdatasets[$defid])) {
1396                     // Do not exist in the new question so ignore.
1397                     unset($datasetdefinitions[$defid]);
1398                     continue;
1399                 }
1400                 // Create a copy but not for category one.
1401                 if (0 == $datasetdef->category) {
1402                     $olddatasetid = $datasetdef->id;
1403                     $olditemcount = $datasetdef->itemcount;
1404                     $datasetdef->itemcount = 0;
1405                     $datasetdef->id = $DB->insert_record('question_dataset_definitions',
1406                             $datasetdef);
1407                     // Copy the dataitems.
1408                     $olditems = $this->get_database_dataset_items($olddatasetid);
1409                     if (count($olditems) > 0) {
1410                         $itemcount = 0;
1411                         foreach ($olditems as $item) {
1412                             $item->definition = $datasetdef->id;
1413                             $DB->insert_record('question_dataset_items', $item);
1414                             $itemcount++;
1415                         }
1416                         // Update item count to olditemcount if
1417                         // at least this number of items has been recover from the database.
1418                         if ($olditemcount <= $itemcount) {
1419                             $datasetdef->itemcount = $olditemcount;
1420                         } else {
1421                             $datasetdef->itemcount = $itemcount;
1422                         }
1423                         $DB->update_record('question_dataset_definitions', $datasetdef);
1424                     } // End of  copy the dataitems.
1425                 }// End of  copy the datasetdef.
1426                 // Create relation to the new question with this
1427                 // copy as new datasetdef from the initial question.
1428                 $questiondataset = new stdClass();
1429                 $questiondataset->question = $form->id;
1430                 $questiondataset->datasetdefinition = $datasetdef->id;
1431                 $DB->insert_record('question_datasets', $questiondataset);
1432                 unset($datasetdefinitions[$defid]);
1433                 continue;
1434             }// End of datasetdefs from the initial question.
1435             // Really new one code similar to save_dataset_definitions().
1436             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
1438             if (0 != $datasetdef->category) {
1439                 // We need to look for already existing
1440                 // datasets in the category.
1441                 // By first creating the datasetdefinition above we
1442                 // can manage to automatically take care of
1443                 // some possible realtime concurrence.
1444                 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
1445                         "type = ? AND name = ? AND category = ? AND id < ?
1446                         ORDER BY id DESC",
1447                         array($datasetdef->type, $datasetdef->name,
1448                                 $datasetdef->category, $datasetdef->id))) {
1450                     while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1451                         $DB->delete_records('question_dataset_definitions',
1452                                 array('id' => $datasetdef->id));
1453                         $datasetdef = $olderdatasetdef;
1454                     }
1455                 }
1456             }
1458             // Create relation to this dataset.
1459             $questiondataset = new stdClass();
1460             $questiondataset->question = $form->id;
1461             $questiondataset->datasetdefinition = $datasetdef->id;
1462             $DB->insert_record('question_datasets', $questiondataset);
1463             unset($datasetdefinitions[$defid]);
1464         }
1466         // Remove local obsolete datasets as well as relations
1467         // to datasets in other categories.
1468         if (!empty($datasetdefinitions)) {
1469             foreach ($datasetdefinitions as $def) {
1470                 $DB->delete_records('question_datasets',
1471                         array('question' => $form->id, 'datasetdefinition' => $def->id));
1473                 if ($def->category == 0) { // Question local dataset.
1474                     $DB->delete_records('question_dataset_definitions',
1475                             array('id' => $def->id));
1476                     $DB->delete_records('question_dataset_items',
1477                             array('definition' => $def->id));
1478                 }
1479             }
1480         }
1481     }
1483     // Dataset functionality.
1484     public function pick_question_dataset($question, $datasetitem) {
1485         // Select a dataset in the following format:
1486         // an array indexed by the variable names (d.name) pointing to the value
1487         // to be substituted.
1488         global $CFG, $DB;
1489         if (!$dataitems = $DB->get_records_sql(
1490                 "SELECT i.id, d.name, i.value
1491                    FROM {question_dataset_definitions} d,
1492                         {question_dataset_items} i,
1493                         {question_datasets} q
1494                   WHERE q.question = ?
1495                     AND q.datasetdefinition = d.id
1496                     AND d.id = i.definition
1497                     AND i.itemnumber = ?
1498                ORDER BY i.id DESC ", array($question->id, $datasetitem))) {
1499             $a = new stdClass();
1500             $a->id = $question->id;
1501             $a->item = $datasetitem;
1502             print_error('cannotgetdsfordependent', 'question', '', $a);
1503         }
1504         $dataset = Array();
1505         foreach ($dataitems as $id => $dataitem) {
1506             if (!isset($dataset[$dataitem->name])) {
1507                 $dataset[$dataitem->name] = $dataitem->value;
1508             }
1509         }
1510         return $dataset;
1511     }
1513     public function dataset_options_from_database($form, $name, $prefix = '',
1514             $langfile = 'qtype_calculated') {
1515         global $CFG, $DB;
1516         $type = 1; // Only type = 1 (i.e. old 'LITERAL') has ever been used.
1517         // First options - it is not a dataset...
1518         $options['0'] = get_string($prefix.'nodataset', $langfile);
1519         // New question no local.
1520         if (!isset($form->id) || $form->id == 0) {
1521             $key = "$type-0-$name";
1522             $options[$key] = get_string($prefix."newlocal$type", $langfile);
1523             $currentdatasetdef = new stdClass();
1524             $currentdatasetdef->type = '0';
1525         } else {
1526             // Construct question local options.
1527             $sql = "SELECT a.*
1528                 FROM {question_dataset_definitions} a, {question_datasets} b
1529                WHERE a.id = b.datasetdefinition AND a.type = '1' AND b.question = ? AND a.name = ?";
1530             $currentdatasetdef = $DB->get_record_sql($sql, array($form->id, $name));
1531             if (!$currentdatasetdef) {
1532                 $currentdatasetdef = new stdClass();
1533                 $currentdatasetdef->type = '0';
1534             }
1535             $key = "$type-0-$name";
1536             if ($currentdatasetdef->type == $type
1537                     and $currentdatasetdef->category == 0) {
1538                 $options[$key] = get_string($prefix."keptlocal$type", $langfile);
1539             } else {
1540                 $options[$key] = get_string($prefix."newlocal$type", $langfile);
1541             }
1542         }
1543         // Construct question category options.
1544         $categorydatasetdefs = $DB->get_records_sql(
1545             "SELECT b.question, a.*
1546             FROM {question_datasets} b,
1547             {question_dataset_definitions} a
1548             WHERE a.id = b.datasetdefinition
1549             AND a.type = '1'
1550             AND a.category = ?
1551             AND a.name = ?", array($form->category, $name));
1552         $type = 1;
1553         $key = "$type-$form->category-$name";
1554         if (!empty($categorydatasetdefs)) {
1555             // There is at least one with the same name.
1556             if (isset($form->id) && isset($categorydatasetdefs[$form->id])) {
1557                 // It is already used by this question.
1558                 $options[$key] = get_string($prefix."keptcategory$type", $langfile);
1559             } else {
1560                 $options[$key] = get_string($prefix."existingcategory$type", $langfile);
1561             }
1562         } else {
1563             $options[$key] = get_string($prefix."newcategory$type", $langfile);
1564         }
1565         // All done!
1566         return array($options, $currentdatasetdef->type
1567             ? "$currentdatasetdef->type-$currentdatasetdef->category-$name"
1568             : '');
1569     }
1571     public function find_dataset_names($text) {
1572         // Returns the possible dataset names found in the text as an array.
1573         // The array has the dataset name for both key and value.
1574         $datasetnames = array();
1575         while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) {
1576             $datasetnames[$regs[1]] = $regs[1];
1577             $text = str_replace($regs[0], '', $text);
1578         }
1579         return $datasetnames;
1580     }
1582     /**
1583      * This function retrieve the item count of the available category shareable
1584      * wild cards that is added as a comment displayed when a wild card with
1585      * the same name is displayed in datasetdefinitions_form.php
1586      */
1587     public function get_dataset_definitions_category($form) {
1588         global $CFG, $DB;
1589         $datasetdefs = array();
1590         $lnamemax = 30;
1591         if (!empty($form->category)) {
1592             $sql = "SELECT i.*, d.*
1593                       FROM {question_datasets} d, {question_dataset_definitions} i
1594                      WHERE i.id = d.datasetdefinition AND i.category = ?";
1595             if ($records = $DB->get_records_sql($sql, array($form->category))) {
1596                 foreach ($records as $r) {
1597                     if (!isset ($datasetdefs["$r->name"])) {
1598                         $datasetdefs["$r->name"] = $r->itemcount;
1599                     }
1600                 }
1601             }
1602         }
1603         return $datasetdefs;
1604     }
1606     /**
1607      * This function build a table showing the available category shareable
1608      * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1609      * and the name of the question where they are used.
1610      * This table is intended to be add before the question text to help the user use
1611      * these wild cards
1612      */
1613     public function print_dataset_definitions_category($form) {
1614         global $CFG, $DB;
1615         $datasetdefs = array();
1616         $lnamemax = 22;
1617         $namestr          = get_string('name');
1618         $rangeofvaluestr  = get_string('minmax', 'qtype_calculated');
1619         $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
1620         $itemscountstr    = get_string('itemscount', 'qtype_calculated');
1621         $text = '';
1622         if (!empty($form->category)) {
1623             list($category) = explode(',', $form->category);
1624             $sql = "SELECT i.*, d.*
1625                 FROM {question_datasets} d,
1626         {question_dataset_definitions} i
1627         WHERE i.id = d.datasetdefinition
1628         AND i.category = ?";
1629             if ($records = $DB->get_records_sql($sql, array($category))) {
1630                 foreach ($records as $r) {
1631                     $sql1 = "SELECT q.*
1632                                FROM {question} q
1633                               WHERE q.id = ?";
1634                     if (!isset ($datasetdefs["$r->type-$r->category-$r->name"])) {
1635                         $datasetdefs["$r->type-$r->category-$r->name"] = $r;
1636                     }
1637                     if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
1638                         if (!isset ($datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question])) {
1639                             $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question] = new stdClass();
1640                         }
1641                         $datasetdefs["$r->type-$r->category-$r->name"]->questions[
1642                                 $r->question]->name = $questionb[$r->question]->name;
1643                     }
1644                 }
1645             }
1646         }
1647         if (!empty ($datasetdefs)) {
1649             $text = "<table width=\"100%\" border=\"1\"><tr>
1650                     <th style=\"white-space:nowrap;\" class=\"header\"
1651                             scope=\"col\">$namestr</th>
1652                     <th style=\"white-space:nowrap;\" class=\"header\"
1653                             scope=\"col\">$rangeofvaluestr</th>
1654                     <th style=\"white-space:nowrap;\" class=\"header\"
1655                             scope=\"col\">$itemscountstr</th>
1656                     <th style=\"white-space:nowrap;\" class=\"header\"
1657                             scope=\"col\">$questionusingstr</th>
1658                     </tr>";
1659             foreach ($datasetdefs as $datasetdef) {
1660                 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4);
1661                 $text .= "<tr>
1662                         <td valign=\"top\" align=\"center\">$datasetdef->name</td>
1663                         <td align=\"center\" valign=\"top\">$min <strong>-</strong> $max</td>
1664                         <td align=\"right\" valign=\"top\">$datasetdef->itemcount&nbsp;&nbsp;</td>
1665                         <td align=\"left\">";
1666                 foreach ($datasetdef->questions as $qu) {
1667                     // Limit the name length displayed.
1668                     if (!empty($qu->name)) {
1669                         $qu->name = (strlen($qu->name) > $lnamemax) ?
1670                             substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1671                     } else {
1672                         $qu->name = '';
1673                     }
1674                     $text .= " &nbsp;&nbsp; $qu->name <br/>";
1675                 }
1676                 $text .= "</td></tr>";
1677             }
1678             $text .= "</table>";
1679         } else {
1680             $text .= get_string('nosharedwildcard', 'qtype_calculated');
1681         }
1682         return $text;
1683     }
1685     /**
1686      * This function build a table showing the available category shareable
1687      * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1688      * and the name of the question where they are used.
1689      * This table is intended to be add before the question text to help the user use
1690      * these wild cards
1691      */
1693     public function print_dataset_definitions_category_shared($question, $datasetdefsq) {
1694         global $CFG, $DB;
1695         $datasetdefs = array();
1696         $lnamemax = 22;
1697         $namestr          = get_string('name', 'quiz');
1698         $rangeofvaluestr  = get_string('minmax', 'qtype_calculated');
1699         $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
1700         $itemscountstr    = get_string('itemscount', 'qtype_calculated');
1701         $text = '';
1702         if (!empty($question->category)) {
1703             list($category) = explode(',', $question->category);
1704             $sql = "SELECT i.*, d.*
1705                       FROM {question_datasets} d, {question_dataset_definitions} i
1706                      WHERE i.id = d.datasetdefinition AND i.category = ?";
1707             if ($records = $DB->get_records_sql($sql, array($category))) {
1708                 foreach ($records as $r) {
1709                     $key = "$r->type-$r->category-$r->name";
1710                     $sql1 = "SELECT q.*
1711                                FROM {question} q
1712                               WHERE q.id = ?";
1713                     if (!isset($datasetdefs[$key])) {
1714                         $datasetdefs[$key] = $r;
1715                     }
1716                     if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
1717                         $datasetdefs[$key]->questions[$r->question] = new stdClass();
1718                         $datasetdefs[$key]->questions[$r->question]->name =
1719                                 $questionb[$r->question]->name;
1720                         $datasetdefs[$key]->questions[$r->question]->id =
1721                                 $questionb[$r->question]->id;
1722                     }
1723                 }
1724             }
1725         }
1726         if (!empty ($datasetdefs)) {
1728             $text  = "<table width=\"100%\" border=\"1\"><tr>
1729                     <th style=\"white-space:nowrap;\" class=\"header\"
1730                             scope=\"col\">$namestr</th>";
1731             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1732                     scope=\"col\">$itemscountstr</th>";
1733             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1734                     scope=\"col\">&nbsp;&nbsp;$questionusingstr &nbsp;&nbsp;</th>";
1735             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1736                     scope=\"col\">Quiz</th>";
1737             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1738                     scope=\"col\">Attempts</th></tr>";
1739             foreach ($datasetdefs as $datasetdef) {
1740                 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4);
1741                 $count = count($datasetdef->questions);
1742                 $text .= "<tr>
1743                         <td style=\"white-space:nowrap;\" valign=\"top\"
1744                                 align=\"center\" rowspan=\"$count\"> $datasetdef->name </td>
1745                         <td align=\"right\" valign=\"top\"
1746                                 rowspan=\"$count\">$datasetdef->itemcount</td>";
1747                 $line = 0;
1748                 foreach ($datasetdef->questions as $qu) {
1749                     // Limit the name length displayed.
1750                     if (!empty($qu->name)) {
1751                         $qu->name = (strlen($qu->name) > $lnamemax) ?
1752                             substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1753                     } else {
1754                         $qu->name = '';
1755                     }
1756                     if ($line) {
1757                         $text .= "<tr>";
1758                     }
1759                     $line++;
1760                     $text .= "<td align=\"left\" style=\"white-space:nowrap;\">$qu->name</td>";
1761                     $nbofquiz = 0;
1762                     $nbofattempts= 0;
1763                     $usedinquiz = false;
1764                     if ($list = $DB->get_records('quiz_question_instances',
1765                             array('question' => $qu->id))) {
1766                         $usedinquiz = true;
1767                         foreach ($list as $key => $li) {
1768                             $nbofquiz ++;
1769                             if ($att = $DB->get_records('quiz_attempts',
1770                                     array('quiz' => $li->quiz, 'preview' => '0'))) {
1771                                 $nbofattempts+= count($att);
1772                             }
1773                         }
1774                     }
1775                     if ($usedinquiz) {
1776                         $text .= "<td align=\"center\">$nbofquiz</td>";
1777                     } else {
1778                         $text .= "<td align=\"center\">0</td>";
1779                     }
1780                     if ($usedinquiz) {
1781                         $text .= "<td align=\"center\">$nbofattempts";
1782                     } else {
1783                         $text .= "<td align=\"left\"><br/>";
1784                     }
1786                     $text .= "</td></tr>";
1787                 }
1788             }
1789             $text .= "</table>";
1790         } else {
1791             $text .= get_string('nosharedwildcard', 'qtype_calculated');
1792         }
1793         return $text;
1794     }
1796     public function find_math_equations($text) {
1797         // Returns the possible dataset names found in the text as an array.
1798         // The array has the dataset name for both key and value.
1799         $equations = array();
1800         while (preg_match('~\{=([^[:space:]}]*)}~', $text, $regs)) {
1801             $equations[] = $regs[1];
1802             $text = str_replace($regs[0], '', $text);
1803         }
1804         return $equations;
1805     }
1807     public function get_virtual_qtype() {
1808         return question_bank::get_qtype('numerical');
1809     }
1811     public function get_possible_responses($questiondata) {
1812         $responses = array();
1814         $virtualqtype = $this->get_virtual_qtype();
1815         $unit = $virtualqtype->get_default_numerical_unit($questiondata);
1817         $tolerancetypes = $this->tolerance_types();
1819         $starfound = false;
1820         foreach ($questiondata->options->answers as $aid => $answer) {
1821             $responseclass = $answer->answer;
1823             if ($responseclass === '*') {
1824                 $starfound = true;
1825             } else {
1826                 $a = new stdClass();
1827                 $a->answer = $virtualqtype->add_unit($questiondata, $responseclass, $unit);
1828                 $a->tolerance = $answer->tolerance;
1829                 $a->tolerancetype = $tolerancetypes[$answer->tolerancetype];
1831                 $responseclass = get_string('answerwithtolerance', 'qtype_calculated', $a);
1832             }
1834             $responses[$aid] = new question_possible_response($responseclass,
1835                     $answer->fraction);
1836         }
1838         if (!$starfound) {
1839             $responses[0] = new question_possible_response(
1840             get_string('didnotmatchanyanswer', 'question'), 0);
1841         }
1843         $responses[null] = question_possible_response::no_response();
1845         return array($questiondata->id => $responses);
1846     }
1848     public function move_files($questionid, $oldcontextid, $newcontextid) {
1849         $fs = get_file_storage();
1851         parent::move_files($questionid, $oldcontextid, $newcontextid);
1852         $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid);
1853         $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
1854     }
1856     protected function delete_files($questionid, $contextid) {
1857         $fs = get_file_storage();
1859         parent::delete_files($questionid, $contextid);
1860         $this->delete_files_in_answers($questionid, $contextid);
1861         $this->delete_files_in_hints($questionid, $contextid);
1862     }
1866 function qtype_calculated_calculate_answer($formula, $individualdata,
1867     $tolerance, $tolerancetype, $answerlength, $answerformat = '1', $unit = '') {
1868     // The return value has these properties: .
1869     // ->answer    the correct answer
1870     // ->min       the lower bound for an acceptable response
1871     // ->max       the upper bound for an accetpable response.
1872     $calculated = new stdClass();
1873     // Exchange formula variables with the correct values...
1874     $answer = question_bank::get_qtype('calculated')->substitute_variables_and_eval(
1875             $formula, $individualdata);
1876     if (!is_numeric($answer)) {
1877         // Something went wrong, so just return NaN.
1878         $calculated->answer = NAN;
1879         return $calculated;
1880     }
1881     if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */
1882         /*** Adjust to the correct number of decimals ***/
1883         if (stripos($answer, 'e')>0) {
1884             $answerlengthadd = strlen($answer)-stripos($answer, 'e');
1885         } else {
1886             $answerlengthadd = 0;
1887         }
1888         $calculated->answer = round(floatval($answer), $answerlength+$answerlengthadd);
1890         if ($answerlength) {
1891             /* Try to include missing zeros at the end */
1893             if (preg_match('~^(.*\\.)(.*)$~', $calculated->answer, $regs)) {
1894                 $calculated->answer = $regs[1] . substr(
1895                     $regs[2] . '00000000000000000000000000000000000000000x',
1896                     0, $answerlength)
1897                     . $unit;
1898             } else {
1899                 $calculated->answer .=
1900                     substr('.00000000000000000000000000000000000000000x',
1901                         0, $answerlength + 1) . $unit;
1902             }
1903         } else {
1904             /* Attach unit */
1905             $calculated->answer .= $unit;
1906         }
1908     } else if ($answer) { // Significant figures does only apply if the result is non-zero.
1910         // Convert to positive answer...
1911         if ($answer < 0) {
1912             $answer = -$answer;
1913             $sign = '-';
1914         } else {
1915             $sign = '';
1916         }
1918         // Determine the format 0.[1-9][0-9]* for the answer...
1919         $p10 = 0;
1920         while ($answer < 1) {
1921             --$p10;
1922             $answer *= 10;
1923         }
1924         while ($answer >= 1) {
1925             ++$p10;
1926             $answer /= 10;
1927         }
1928         // ... and have the answer rounded of to the correct length.
1929         $answer = round($answer, $answerlength);
1931         // If we rounded up to 1.0, place the answer back into 0.[1-9][0-9]* format.
1932         if ($answer >= 1) {
1933             ++$p10;
1934             $answer /= 10;
1935         }
1937         // Have the answer written on a suitable format:
1938         // either scientific or plain numeric.
1939         if (-2 > $p10 || 4 < $p10) {
1940             // Use scientific format.
1941             $exponent = 'e'.--$p10;
1942             $answer *= 10;
1943             if (1 == $answerlength) {
1944                 $calculated->answer = $sign.$answer.$exponent.$unit;
1945             } else {
1946                 // Attach additional zeros at the end of $answer.
1947                 $answer .= (1 == strlen($answer) ? '.' : '')
1948                     . '00000000000000000000000000000000000000000x';
1949                 $calculated->answer = $sign
1950                     .substr($answer, 0, $answerlength +1).$exponent.$unit;
1951             }
1952         } else {
1953             // Stick to plain numeric format.
1954             $answer *= "1e$p10";
1955             if (0.1 <= $answer / "1e$answerlength") {
1956                 $calculated->answer = $sign.$answer.$unit;
1957             } else {
1958                 // Could be an idea to add some zeros here.
1959                 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '')
1960                     . '00000000000000000000000000000000000000000x';
1961                 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
1962                 $calculated->answer = $sign.substr($answer, 0, $oklen).$unit;
1963             }
1964         }
1966     } else {
1967         $calculated->answer = 0.0;
1968     }
1970     // Return the result.
1971     return $calculated;
1975 function qtype_calculated_find_formula_errors($formula) {
1976     // Validates the formula submitted from the question edit page.
1977     // Returns false if everything is alright
1978     // otherwise it constructs an error message.
1979     // Strip away dataset names.
1980     while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
1981         $formula = str_replace($regs[0], '1', $formula);
1982     }
1984     // Strip away empty space and lowercase it.
1985     $formula = strtolower(str_replace(' ', '', $formula));
1987     $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */
1988     $operatorornumber = "[$safeoperatorchar.0-9eE]";
1990     while (preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)" .
1991             "\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~",
1992         $formula, $regs)) {
1993         switch ($regs[2]) {
1994             // Simple parenthesis.
1995             case '':
1996                 if ((isset($regs[4]) && $regs[4]) || strlen($regs[3]) == 0) {
1997                     return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
1998                 }
1999                 break;
2001                 // Zero argument functions.
2002             case 'pi':
2003                 if ($regs[3]) {
2004                     return get_string('functiontakesnoargs', 'qtype_calculated', $regs[2]);
2005                 }
2006                 break;
2008                 // Single argument functions (the most common case).
2009             case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
2010             case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
2011             case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
2012             case 'exp': case 'expm1': case 'floor': case 'is_finite':
2013             case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
2014             case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
2015             case 'tan': case 'tanh':
2016                 if (!empty($regs[4]) || empty($regs[3])) {
2017                     return get_string('functiontakesonearg', 'qtype_calculated', $regs[2]);
2018                 }
2019                 break;
2021                 // Functions that take one or two arguments.
2022             case 'log': case 'round':
2023                 if (!empty($regs[5]) || empty($regs[3])) {
2024                     return get_string('functiontakesoneortwoargs', 'qtype_calculated', $regs[2]);
2025                 }
2026                 break;
2028                 // Functions that must have two arguments.
2029             case 'atan2': case 'fmod': case 'pow':
2030                 if (!empty($regs[5]) || empty($regs[4])) {
2031                     return get_string('functiontakestwoargs', 'qtype_calculated', $regs[2]);
2032                 }
2033                 break;
2035                 // Functions that take two or more arguments.
2036             case 'min': case 'max':
2037                 if (empty($regs[4])) {
2038                     return get_string('functiontakesatleasttwo', 'qtype_calculated', $regs[2]);
2039                 }
2040                 break;
2042             default:
2043                 return get_string('unsupportedformulafunction', 'qtype_calculated', $regs[2]);
2044         }
2046         // Exchange the function call with '1' and then check for
2047         // another function call...
2048         if ($regs[1]) {
2049             // The function call is proceeded by an operator.
2050             $formula = str_replace($regs[0], $regs[1] . '1', $formula);
2051         } else {
2052             // The function call starts the formula.
2053             $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula);
2054         }
2055     }
2057     if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) {
2058         return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
2059     } else {
2060         // Formula just might be valid.
2061         return false;
2062     }