Moodle  2.2.1
http://www.collinsharper.com
C:/xampp/htdocs/moodle/question/type/calculated/questiontype.php
Go to the documentation of this file.
00001 <?php
00002 // This file is part of Moodle - http://moodle.org/
00003 //
00004 // Moodle is free software: you can redistribute it and/or modify
00005 // it under the terms of the GNU General Public License as published by
00006 // the Free Software Foundation, either version 3 of the License, or
00007 // (at your option) any later version.
00008 //
00009 // Moodle is distributed in the hope that it will be useful,
00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of
00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00012 // GNU General Public License for more details.
00013 //
00014 // You should have received a copy of the GNU General Public License
00015 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
00016 
00027 defined('MOODLE_INTERNAL') || die();
00028 
00029 require_once($CFG->dirroot . '/question/type/numerical/question.php');
00030 
00031 
00038 class qtype_calculated extends question_type {
00039     const MAX_DATASET_ITEMS = 100;
00040 
00041     public $wizardpagesnumber = 3;
00042 
00043     public function get_question_options($question) {
00044         // First get the datasets and default options
00045         // the code is used for calculated, calculatedsimple and calculatedmulti qtypes
00046         global $CFG, $DB, $OUTPUT;
00047         if (!$question->options = $DB->get_record('question_calculated_options',
00048                 array('question' => $question->id))) {
00049             $question->options->synchronize = 0;
00050             $question->options->single = 0;
00051             $question->options->answernumbering = 'abc';
00052             $question->options->shuffleanswers = 0;
00053             $question->options->correctfeedback = '';
00054             $question->options->partiallycorrectfeedback = '';
00055             $question->options->incorrectfeedback = '';
00056             $question->options->correctfeedbackformat = 0;
00057             $question->options->partiallycorrectfeedbackformat = 0;
00058             $question->options->incorrectfeedbackformat = 0;
00059         }
00060 
00061         if (!$question->options->answers = $DB->get_records_sql("
00062             SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.correctanswerformat
00063             FROM {question_answers} a,
00064                  {question_calculated} c
00065             WHERE a.question = ?
00066             AND   a.id = c.answer
00067             ORDER BY a.id ASC", array($question->id))) {
00068                 return false;
00069         }
00070 
00071         if ($this->get_virtual_qtype()->name() == 'numerical') {
00072             $this->get_virtual_qtype()->get_numerical_units($question);
00073             $this->get_virtual_qtype()->get_numerical_options($question);
00074         }
00075 
00076         $question->hints = $DB->get_records('question_hints',
00077                 array('questionid' => $question->id), 'id ASC');
00078 
00079         if (isset($question->export_process)&&$question->export_process) {
00080             $question->options->datasets = $this->get_datasets_for_export($question);
00081         }
00082         return true;
00083     }
00084 
00085     public function get_datasets_for_export($question) {
00086         global $DB, $CFG;
00087         $datasetdefs = array();
00088         if (!empty($question->id)) {
00089             $sql = "SELECT i.*
00090                       FROM {question_datasets} d, {question_dataset_definitions} i
00091                      WHERE d.question = ? AND d.datasetdefinition = i.id";
00092             if ($records = $DB->get_records_sql($sql, array($question->id))) {
00093                 foreach ($records as $r) {
00094                     $def = $r;
00095                     if ($def->category == '0') {
00096                         $def->status = 'private';
00097                     } else {
00098                         $def->status = 'shared';
00099                     }
00100                     $def->type = 'calculated';
00101                     list($distribution, $min, $max, $dec) = explode(':', $def->options, 4);
00102                     $def->distribution = $distribution;
00103                     $def->minimum = $min;
00104                     $def->maximum = $max;
00105                     $def->decimals = $dec;
00106                     if ($def->itemcount > 0) {
00107                         // get the datasetitems
00108                         $def->items = array();
00109                         if ($items = $this->get_database_dataset_items($def->id)) {
00110                             $n = 0;
00111                             foreach ($items as $ii) {
00112                                 $n++;
00113                                 $def->items[$n] = new stdClass();
00114                                 $def->items[$n]->itemnumber = $ii->itemnumber;
00115                                 $def->items[$n]->value = $ii->value;
00116                             }
00117                             $def->number_of_items = $n;
00118                         }
00119                     }
00120                     $datasetdefs["1-$r->category-$r->name"] = $def;
00121                 }
00122             }
00123         }
00124         return $datasetdefs;
00125     }
00126 
00127     public function save_question_options($question) {
00128         global $CFG, $DB;
00129         // the code is used for calculated, calculatedsimple and calculatedmulti qtypes
00130         $context = $question->context;
00131         if (isset($question->answer) && !isset($question->answers)) {
00132             $question->answers = $question->answer;
00133         }
00134         // calculated options
00135         $update = true;
00136         $options = $DB->get_record('question_calculated_options',
00137                 array('question' => $question->id));
00138         if (!$options) {
00139             $update = false;
00140             $options = new stdClass();
00141             $options->question = $question->id;
00142         }
00143         // as used only by calculated
00144         if (isset($question->synchronize)) {
00145             $options->synchronize = $question->synchronize;
00146         } else {
00147             $options->synchronize = 0;
00148         }
00149         $options->single = 0;
00150         $options->answernumbering =  $question->answernumbering;
00151         $options->shuffleanswers = $question->shuffleanswers;
00152 
00153         foreach (array('correctfeedback', 'partiallycorrectfeedback',
00154                 'incorrectfeedback') as $feedbackname) {
00155             $options->$feedbackname = '';
00156             $feedbackformat = $feedbackname . 'format';
00157             $options->$feedbackformat = 0;
00158         }
00159 
00160         if ($update) {
00161             $DB->update_record('question_calculated_options', $options);
00162         } else {
00163             $DB->insert_record('question_calculated_options', $options);
00164         }
00165 
00166         // Get old versions of the objects
00167         $oldanswers = $DB->get_records('question_answers',
00168                 array('question' => $question->id), 'id ASC');
00169 
00170         $oldoptions = $DB->get_records('question_calculated',
00171                 array('question' => $question->id), 'answer ASC');
00172 
00173         // Save the units.
00174         $virtualqtype = $this->get_virtual_qtype();
00175 
00176         $result = $virtualqtype->save_units($question);
00177         if (isset($result->error)) {
00178             return $result;
00179         } else {
00180             $units = $result->units;
00181         }
00182 
00183         // Insert all the new answers
00184         if (isset($question->answer) && !isset($question->answers)) {
00185             $question->answers = $question->answer;
00186         }
00187         foreach ($question->answers as $key => $answerdata) {
00188             if (is_array($answerdata)) {
00189                 $answerdata = $answerdata['text'];
00190             }
00191             if (trim($answerdata) == '') {
00192                 continue;
00193             }
00194 
00195             // Update an existing answer if possible.
00196             $answer = array_shift($oldanswers);
00197             if (!$answer) {
00198                 $answer = new stdClass();
00199                 $answer->question = $question->id;
00200                 $answer->answer   = '';
00201                 $answer->feedback = '';
00202                 $answer->id       = $DB->insert_record('question_answers', $answer);
00203             }
00204 
00205             $answer->answer   = trim($answerdata);
00206             $answer->fraction = $question->fraction[$key];
00207             $answer->feedback = $this->import_or_save_files($question->feedback[$key],
00208                     $context, 'question', 'answerfeedback', $answer->id);
00209             $answer->feedbackformat = $question->feedback[$key]['format'];
00210 
00211             $DB->update_record("question_answers", $answer);
00212 
00213             // Set up the options object
00214             if (!$options = array_shift($oldoptions)) {
00215                 $options = new stdClass();
00216             }
00217             $options->question            = $question->id;
00218             $options->answer              = $answer->id;
00219             $options->tolerance           = trim($question->tolerance[$key]);
00220             $options->tolerancetype       = trim($question->tolerancetype[$key]);
00221             $options->correctanswerlength = trim($question->correctanswerlength[$key]);
00222             $options->correctanswerformat = trim($question->correctanswerformat[$key]);
00223 
00224             // Save options
00225             if (isset($options->id)) {
00226                 // reusing existing record
00227                 $DB->update_record('question_calculated', $options);
00228             } else {
00229                 // new options
00230                 $DB->insert_record('question_calculated', $options);
00231             }
00232         }
00233 
00234         // delete old answer records
00235         if (!empty($oldanswers)) {
00236             foreach ($oldanswers as $oa) {
00237                 $DB->delete_records('question_answers', array('id' => $oa->id));
00238             }
00239         }
00240 
00241         // delete old answer records
00242         if (!empty($oldoptions)) {
00243             foreach ($oldoptions as $oo) {
00244                 $DB->delete_records('question_calculated', array('id' => $oo->id));
00245             }
00246         }
00247 
00248         $result = $virtualqtype->save_unit_options($question);
00249         if (isset($result->error)) {
00250             return $result;
00251         }
00252 
00253         $this->save_hints($question);
00254 
00255         if (isset($question->import_process)&&$question->import_process) {
00256             $this->import_datasets($question);
00257         }
00258         // Report any problems.
00259         if (!empty($result->notice)) {
00260             return $result;
00261         }
00262         return true;
00263     }
00264 
00265     public function import_datasets($question) {
00266         global $DB;
00267         $n = count($question->dataset);
00268         foreach ($question->dataset as $dataset) {
00269             // name, type, option,
00270             $datasetdef = new stdClass();
00271             $datasetdef->name = $dataset->name;
00272             $datasetdef->type = 1;
00273             $datasetdef->options =  $dataset->distribution . ':' . $dataset->min . ':' .
00274                     $dataset->max . ':' . $dataset->length;
00275             $datasetdef->itemcount = $dataset->itemcount;
00276             if ($dataset->status == 'private') {
00277                 $datasetdef->category = 0;
00278                 $todo = 'create';
00279             } else if ($dataset->status == 'shared') {
00280                 if ($sharedatasetdefs = $DB->get_records_select(
00281                     'question_dataset_definitions',
00282                     "type = '1'
00283                     AND name = ?
00284                     AND category = ?
00285                     ORDER BY id DESC ", array($dataset->name, $question->category)
00286                 )) { // so there is at least one
00287                     $sharedatasetdef = array_shift($sharedatasetdefs);
00288                     if ($sharedatasetdef->options ==  $datasetdef->options) {// identical so use it
00289                         $todo = 'useit';
00290                         $datasetdef = $sharedatasetdef;
00291                     } else { // different so create a private one
00292                         $datasetdef->category = 0;
00293                         $todo = 'create';
00294                     }
00295                 } else { // no so create one
00296                     $datasetdef->category = $question->category;
00297                     $todo = 'create';
00298                 }
00299             }
00300             if ($todo == 'create') {
00301                 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
00302             }
00303             // Create relation to the dataset:
00304             $questiondataset = new stdClass();
00305             $questiondataset->question = $question->id;
00306             $questiondataset->datasetdefinition = $datasetdef->id;
00307             $DB->insert_record('question_datasets', $questiondataset);
00308             if ($todo == 'create') {
00309                 // add the items
00310                 foreach ($dataset->datasetitem as $dataitem) {
00311                     $datasetitem = new stdClass();
00312                     $datasetitem->definition = $datasetdef->id;
00313                     $datasetitem->itemnumber = $dataitem->itemnumber;
00314                     $datasetitem->value = $dataitem->value;
00315                     $DB->insert_record('question_dataset_items', $datasetitem);
00316                 }
00317             }
00318         }
00319     }
00320 
00321     protected function initialise_question_instance(question_definition $question, $questiondata) {
00322         parent::initialise_question_instance($question, $questiondata);
00323 
00324         question_bank::get_qtype('numerical')->initialise_numerical_answers(
00325                 $question, $questiondata);
00326         foreach ($questiondata->options->answers as $a) {
00327             $question->answers[$a->id]->tolerancetype = $a->tolerancetype;
00328             $question->answers[$a->id]->correctanswerlength = $a->correctanswerlength;
00329             $question->answers[$a->id]->correctanswerformat = $a->correctanswerformat;
00330         }
00331 
00332         $question->synchronised = $questiondata->options->synchronize;
00333 
00334         $question->unitdisplay = $questiondata->options->showunits;
00335         $question->unitgradingtype = $questiondata->options->unitgradingtype;
00336         $question->unitpenalty = $questiondata->options->unitpenalty;
00337         $question->ap = question_bank::get_qtype(
00338                 'numerical')->make_answer_processor(
00339                 $questiondata->options->units, $questiondata->options->unitsleft);
00340 
00341         $question->datasetloader = new qtype_calculated_dataset_loader($questiondata->id);
00342     }
00343 
00344     public function validate_form($form) {
00345         switch($form->wizardpage) {
00346             case 'question':
00347                 $calculatedmessages = array();
00348                 if (empty($form->name)) {
00349                     $calculatedmessages[] = get_string('missingname', 'qtype_calculated');
00350                 }
00351                 if (empty($form->questiontext)) {
00352                     $calculatedmessages[] = get_string('missingquestiontext', 'qtype_calculated');
00353                 }
00354                 // Verify formulas
00355                 foreach ($form->answers as $key => $answer) {
00356                     if ('' === trim($answer)) {
00357                         $calculatedmessages[] = get_string(
00358                                 'missingformula', 'qtype_calculated');
00359                     }
00360                     if ($formulaerrors = qtype_calculated_find_formula_errors($answer)) {
00361                         $calculatedmessages[] = $formulaerrors;
00362                     }
00363                     if (! isset($form->tolerance[$key])) {
00364                         $form->tolerance[$key] = 0.0;
00365                     }
00366                     if (! is_numeric($form->tolerance[$key])) {
00367                         $calculatedmessages[] = get_string(
00368                                 'tolerancemustbenumeric', 'qtype_calculated');
00369                     }
00370                 }
00371 
00372                 if (!empty($calculatedmessages)) {
00373                     $errorstring = "The following errors were found:<br />";
00374                     foreach ($calculatedmessages as $msg) {
00375                         $errorstring .= $msg . '<br />';
00376                     }
00377                     print_error($errorstring);
00378                 }
00379 
00380                 break;
00381             default:
00382                 return parent::validate_form($form);
00383                 break;
00384         }
00385         return true;
00386     }
00387     public function finished_edit_wizard($form) {
00388         return isset($form->savechanges);
00389     }
00390     public function wizardpagesnumber() {
00391         return 3;
00392     }
00393     // This gets called by editquestion.php after the standard question is saved
00394     public function print_next_wizard_page($question, $form, $course) {
00395         global $CFG, $SESSION, $COURSE;
00396 
00397         // Catch invalid navigation & reloads
00398         if (empty($question->id) && empty($SESSION->calculated)) {
00399             redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired.', 3);
00400         }
00401 
00402         // See where we're coming from
00403         switch($form->wizardpage) {
00404             case 'question':
00405                 require("$CFG->dirroot/question/type/calculated/datasetdefinitions.php");
00406                 break;
00407             case 'datasetdefinitions':
00408             case 'datasetitems':
00409                 require("$CFG->dirroot/question/type/calculated/datasetitems.php");
00410                 break;
00411             default:
00412                 print_error('invalidwizardpage', 'question');
00413                 break;
00414         }
00415     }
00416 
00417     // This gets called by question2.php after the standard question is saved
00418     public function &next_wizard_form($submiturl, $question, $wizardnow) {
00419         global $CFG, $SESSION, $COURSE;
00420 
00421         // Catch invalid navigation & reloads
00422         if (empty($question->id) && empty($SESSION->calculated)) {
00423             redirect('edit.php?courseid=' . $COURSE->id,
00424                     'The page you are loading has expired. Cannot get next wizard form.', 3);
00425         }
00426         if (empty($question->id)) {
00427             $question = $SESSION->calculated->questionform;
00428         }
00429 
00430         // See where we're coming from
00431         switch($wizardnow) {
00432             case 'datasetdefinitions':
00433                 require("$CFG->dirroot/question/type/calculated/datasetdefinitions_form.php");
00434                 $mform = new question_dataset_dependent_definitions_form(
00435                         "$submiturl?wizardnow=datasetdefinitions", $question);
00436                 break;
00437             case 'datasetitems':
00438                 require("$CFG->dirroot/question/type/calculated/datasetitems_form.php");
00439                 $regenerate = optional_param('forceregeneration', false, PARAM_BOOL);
00440                 $mform = new question_dataset_dependent_items_form(
00441                         "$submiturl?wizardnow=datasetitems", $question, $regenerate);
00442                 break;
00443             default:
00444                 print_error('invalidwizardpage', 'question');
00445                 break;
00446         }
00447 
00448         return $mform;
00449     }
00450 
00459     public function display_question_editing_page($mform, $question, $wizardnow) {
00460         global $OUTPUT;
00461         switch ($wizardnow) {
00462             case '':
00463                 // On the first page, the default display is fine.
00464                 parent::display_question_editing_page($mform, $question, $wizardnow);
00465                 return;
00466 
00467             case 'datasetdefinitions':
00468                 echo $OUTPUT->heading_with_help(
00469                         get_string('choosedatasetproperties', 'qtype_calculated'),
00470                         'questiondatasets', 'qtype_calculated');
00471                 break;
00472 
00473             case 'datasetitems':
00474                 echo $OUTPUT->heading_with_help(get_string('editdatasets', 'qtype_calculated'),
00475                         'questiondatasets', 'qtype_calculated');
00476                 break;
00477         }
00478 
00479         $mform->display();
00480     }
00481 
00494     public function preparedatasets($form , $questionfromid = '0') {
00495         // the dataset names present in the edit_question_form and edit_calculated_form
00496         // are retrieved
00497         $possibledatasets = $this->find_dataset_names($form->questiontext);
00498         $mandatorydatasets = array();
00499         foreach ($form->answers as $answer) {
00500             $mandatorydatasets += $this->find_dataset_names($answer);
00501         }
00502         // if there are identical datasetdefs already saved in the original question.
00503         // either when editing a question or saving as new
00504         // they are retrieved using $questionfromid
00505         if ($questionfromid != '0') {
00506             $form->id = $questionfromid;
00507         }
00508         $datasets = array();
00509         $key = 0;
00510         // always prepare the mandatorydatasets present in the answers
00511         // the $options are not used here
00512         foreach ($mandatorydatasets as $datasetname) {
00513             if (!isset($datasets[$datasetname])) {
00514                 list($options, $selected) =
00515                     $this->dataset_options($form, $datasetname);
00516                 $datasets[$datasetname] = '';
00517                 $form->dataset[$key] = $selected;
00518                 $key++;
00519             }
00520         }
00521         // do not prepare possibledatasets when creating a question
00522         // they will defined and stored with datasetdefinitions_form.php
00523         // the $options are not used here
00524         if ($questionfromid != '0') {
00525 
00526             foreach ($possibledatasets as $datasetname) {
00527                 if (!isset($datasets[$datasetname])) {
00528                     list($options, $selected) =
00529                         $this->dataset_options($form, $datasetname, false);
00530                     $datasets[$datasetname] = '';
00531                     $form->dataset[$key] = $selected;
00532                     $key++;
00533                 }
00534             }
00535         }
00536         return $datasets;
00537     }
00538     public function addnamecategory(&$question) {
00539         global $DB;
00540         $categorydatasetdefs = $DB->get_records_sql(
00541             "SELECT  a.*
00542                FROM {question_datasets} b, {question_dataset_definitions} a
00543               WHERE a.id = b.datasetdefinition
00544                 AND a.type = '1'
00545                 AND a.category != 0
00546                 AND b.question = ?
00547            ORDER BY a.name ", array($question->id));
00548         $questionname = $question->name;
00549         $regs= array();
00550         if (preg_match('~#\{([^[:space:]]*)#~', $questionname , $regs)) {
00551             $questionname = str_replace($regs[0], '', $questionname);
00552         };
00553 
00554         if (!empty($categorydatasetdefs)) {
00555             // there is at least one with the same name
00556             $questionname = '#' . $questionname;
00557             foreach ($categorydatasetdefs as $def) {
00558                 if (strlen($def->name) + strlen($questionname) < 250) {
00559                     $questionname = '{' . $def->name . '}' . $questionname;
00560                 }
00561             }
00562             $questionname = '#' . $questionname;
00563         }
00564         $DB->set_field('question', 'name', $questionname, array('id' => $question->id));
00565     }
00566 
00586     public function save_question($question, $form) {
00587         global $DB;
00588         if ($this->wizardpagesnumber() == 1) {
00589                 $question = parent::save_question($question, $form);
00590             return $question;
00591         }
00592 
00593         $wizardnow =  optional_param('wizardnow', '', PARAM_ALPHA);
00594         $id = optional_param('id', 0, PARAM_INT); // question id
00595         // in case 'question'
00596         // for a new question $form->id is empty
00597         // when saving as new question
00598         //   $question->id = 0, $form is $data from question2.php
00599         //   and $data->makecopy is defined as $data->id is the initial question id
00600         // edit case. If it is a new question we don't necessarily need to
00601         // return a valid question object
00602 
00603         // See where we're coming from
00604         switch($wizardnow) {
00605             case '' :
00606             case 'question': // coming from the first page, creating the second
00607                 if (empty($form->id)) { // for a new question $form->id is empty
00608                     $question = parent::save_question($question, $form);
00609                     //prepare the datasets using default $questionfromid
00610                     $this->preparedatasets($form);
00611                     $form->id = $question->id;
00612                     $this->save_dataset_definitions($form);
00613                     if (isset($form->synchronize) && $form->synchronize == 2) {
00614                         $this->addnamecategory($question);
00615                     }
00616                 } else if (!empty($form->makecopy)) {
00617                     $questionfromid =  $form->id;
00618                     $question = parent::save_question($question, $form);
00619                     //prepare the datasets
00620                     $this->preparedatasets($form, $questionfromid);
00621                     $form->id = $question->id;
00622                     $this->save_as_new_dataset_definitions($form, $questionfromid);
00623                     if (isset($form->synchronize) && $form->synchronize == 2) {
00624                         $this->addnamecategory($question);
00625                     }
00626                 } else {
00627                     // editing a question
00628                     $question = parent::save_question($question, $form);
00629                     //prepare the datasets
00630                     $this->preparedatasets($form, $question->id);
00631                     $form->id = $question->id;
00632                     $this->save_dataset_definitions($form);
00633                     if (isset($form->synchronize) && $form->synchronize == 2) {
00634                         $this->addnamecategory($question);
00635                     }
00636                 }
00637                 break;
00638             case 'datasetdefinitions':
00639                 // calculated options
00640                 // it cannot go here without having done the first page
00641                 // so the question_calculated_options should exist
00642                 // only need to update the synchronize field
00643                 if (isset($form->synchronize)) {
00644                     $optionssynchronize = $form->synchronize;
00645                 } else {
00646                     $optionssynchronize = 0;
00647                 }
00648                 $DB->set_field('question_calculated_options', 'synchronize', $optionssynchronize,
00649                         array('question' => $question->id));
00650                 if (isset($form->synchronize) && $form->synchronize == 2) {
00651                     $this->addnamecategory($question);
00652                 }
00653 
00654                 $this->save_dataset_definitions($form);
00655                 break;
00656             case 'datasetitems':
00657                 $this->save_dataset_items($question, $form);
00658                 $this->save_question_calculated($question, $form);
00659                 break;
00660             default:
00661                 print_error('invalidwizardpage', 'question');
00662                 break;
00663         }
00664         return $question;
00665     }
00666 
00667     public function delete_question($questionid, $contextid) {
00668         global $DB;
00669 
00670         $DB->delete_records('question_calculated', array('question' => $questionid));
00671         $DB->delete_records('question_calculated_options', array('question' => $questionid));
00672         $DB->delete_records('question_numerical_units', array('question' => $questionid));
00673         if ($datasets = $DB->get_records('question_datasets', array('question' => $questionid))) {
00674             foreach ($datasets as $dataset) {
00675                 if (!$DB->get_records_select('question_datasets',
00676                         "question != ? AND datasetdefinition = ? ",
00677                         array($questionid, $dataset->datasetdefinition))) {
00678                     $DB->delete_records('question_dataset_definitions',
00679                             array('id' => $dataset->datasetdefinition));
00680                     $DB->delete_records('question_dataset_items',
00681                             array('definition' => $dataset->datasetdefinition));
00682                 }
00683             }
00684         }
00685         $DB->delete_records('question_datasets', array('question' => $questionid));
00686 
00687         parent::delete_question($questionid, $contextid);
00688     }
00689 
00690     public function get_random_guess_score($questiondata) {
00691         foreach ($questiondata->options->answers as $aid => $answer) {
00692             if ('*' == trim($answer->answer)) {
00693                 return max($answer->fraction - $questiondata->options->unitpenalty, 0);
00694             }
00695         }
00696         return 0;
00697     }
00698 
00699     public function supports_dataset_item_generation() {
00700         // Calcualted support generation of randomly distributed number data
00701         return true;
00702     }
00703 
00704     public function custom_generator_tools_part($mform, $idx, $j) {
00705 
00706         $minmaxgrp = array();
00707         $minmaxgrp[] = $mform->createElement('text', "calcmin[$idx]",
00708                 get_string('calcmin', 'qtype_calculated'));
00709         $minmaxgrp[] = $mform->createElement('text', "calcmax[$idx]",
00710                 get_string('calcmax', 'qtype_calculated'));
00711         $mform->addGroup($minmaxgrp, 'minmaxgrp',
00712                 get_string('minmax', 'qtype_calculated'), ' - ', false);
00713         $mform->setType("calcmin[$idx]", PARAM_NUMBER);
00714         $mform->setType("calcmax[$idx]", PARAM_NUMBER);
00715 
00716         $precisionoptions = range(0, 10);
00717         $mform->addElement('select', "calclength[$idx]",
00718                 get_string('calclength', 'qtype_calculated'), $precisionoptions);
00719 
00720         $distriboptions = array('uniform' => get_string('uniform', 'qtype_calculated'),
00721                 'loguniform' => get_string('loguniform', 'qtype_calculated'));
00722         $mform->addElement('select', "calcdistribution[$idx]",
00723                 get_string('calcdistribution', 'qtype_calculated'), $distriboptions);
00724     }
00725 
00726     public function custom_generator_set_data($datasetdefs, $formdata) {
00727         $idx = 1;
00728         foreach ($datasetdefs as $datasetdef) {
00729             if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
00730                     $datasetdef->options, $regs)) {
00731                 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
00732                 $formdata["calcdistribution[$idx]"] = $regs[1];
00733                 $formdata["calcmin[$idx]"] = $regs[2];
00734                 $formdata["calcmax[$idx]"] = $regs[3];
00735                 $formdata["calclength[$idx]"] = $regs[4];
00736             }
00737             $idx++;
00738         }
00739         return $formdata;
00740     }
00741 
00742     public function custom_generator_tools($datasetdef) {
00743         global $OUTPUT;
00744         if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
00745                 $datasetdef->options, $regs)) {
00746             $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
00747             for ($i = 0; $i<10; ++$i) {
00748                 $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
00749                     ? 'decimals'
00750                     : 'significantfigures'), 'qtype_calculated', $i);
00751             }
00752             $menu1 = html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
00753 
00754             $options = array('uniform' => get_string('uniformbit', 'qtype_calculated'),
00755                     'loguniform' => get_string('loguniformbit', 'qtype_calculated'));
00756             $menu2 = html_writer::select($options, 'calcdistribution[]', $regs[1], null);
00757             return '<input type="submit" onclick="'
00758                 . "getElementById('addform').regenerateddefid.value='$defid'; return true;"
00759                 .'" value="'. get_string('generatevalue', 'qtype_calculated') . '"/><br/>'
00760                 . '<input type="text" size="3" name="calcmin[]" '
00761                 . " value=\"$regs[2]\"/> &amp; <input name=\"calcmax[]\" "
00762                 . ' type="text" size="3" value="' . $regs[3] .'"/> '
00763                 . $menu1 . '<br/>'
00764                 . $menu2;
00765         } else {
00766             return '';
00767         }
00768     }
00769 
00770 
00771     public function update_dataset_options($datasetdefs, $form) {
00772         global $OUTPUT;
00773         // Do we have information about new options???
00774         if (empty($form->definition) || empty($form->calcmin)
00775                 || empty($form->calcmax) || empty($form->calclength)
00776                 || empty($form->calcdistribution)) {
00777             // I guess not
00778 
00779         } else {
00780             // Looks like we just could have some new information here
00781             $uniquedefs = array_values(array_unique($form->definition));
00782             foreach ($uniquedefs as $key => $defid) {
00783                 if (isset($datasetdefs[$defid])
00784                         && is_numeric($form->calcmin[$key+1])
00785                         && is_numeric($form->calcmax[$key+1])
00786                         && is_numeric($form->calclength[$key+1])) {
00787                     switch     ($form->calcdistribution[$key+1]) {
00788                         case 'uniform': case 'loguniform':
00789                             $datasetdefs[$defid]->options =
00790                                 $form->calcdistribution[$key+1] . ':'
00791                                 . $form->calcmin[$key+1] . ':'
00792                                 . $form->calcmax[$key+1] . ':'
00793                                 . $form->calclength[$key+1];
00794                             break;
00795                         default:
00796                             echo $OUTPUT->notification(
00797                                     "Unexpected distribution ".$form->calcdistribution[$key+1]);
00798                     }
00799                 }
00800             }
00801         }
00802 
00803         // Look for empty options, on which we set default values
00804         foreach ($datasetdefs as $defid => $def) {
00805             if (empty($def->options)) {
00806                 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1';
00807             }
00808         }
00809         return $datasetdefs;
00810     }
00811 
00812     public function save_question_calculated($question, $fromform) {
00813         global $DB;
00814 
00815         foreach ($question->options->answers as $key => $answer) {
00816             if ($options = $DB->get_record('question_calculated', array('answer' => $key))) {
00817                 $options->tolerance = trim($fromform->tolerance[$key]);
00818                 $options->tolerancetype  = trim($fromform->tolerancetype[$key]);
00819                 $options->correctanswerlength  = trim($fromform->correctanswerlength[$key]);
00820                 $options->correctanswerformat  = trim($fromform->correctanswerformat[$key]);
00821                 $DB->update_record('question_calculated', $options);
00822             }
00823         }
00824     }
00825 
00833     public function get_database_dataset_items($definition) {
00834         global $CFG, $DB;
00835         $databasedataitems = $DB->get_records_sql(// Use number as key!!
00836             " SELECT id , itemnumber, definition,  value
00837             FROM {question_dataset_items}
00838             WHERE definition = $definition order by id DESC ", array($definition));
00839         $dataitems = Array();
00840         foreach ($databasedataitems as $id => $dataitem) {
00841             if (!isset($dataitems[$dataitem->itemnumber])) {
00842                 $dataitems[$dataitem->itemnumber] = $dataitem;
00843             }
00844         }
00845         ksort($dataitems);
00846         return $dataitems;
00847     }
00848 
00849     public function save_dataset_items($question, $fromform) {
00850         global $CFG, $DB;
00851         $synchronize = false;
00852         if (isset($fromform->nextpageparam['forceregeneration'])) {
00853             $regenerate = $fromform->nextpageparam['forceregeneration'];
00854         } else {
00855             $regenerate = 0;
00856         }
00857         if (empty($question->options)) {
00858             $this->get_question_options($question);
00859         }
00860         if (!empty($question->options->synchronize)) {
00861             $synchronize = true;
00862         }
00863 
00864         //get the old datasets for this question
00865         $datasetdefs = $this->get_dataset_definitions($question->id, array());
00866         // Handle generator options...
00867         $olddatasetdefs = fullclone($datasetdefs);
00868         $datasetdefs = $this->update_dataset_options($datasetdefs, $fromform);
00869         $maxnumber = -1;
00870         foreach ($datasetdefs as $defid => $datasetdef) {
00871             if (isset($datasetdef->id)
00872                     && $datasetdef->options != $olddatasetdefs[$defid]->options) {
00873                 // Save the new value for options
00874                 $DB->update_record('question_dataset_definitions', $datasetdef);
00875 
00876             }
00877             // Get maxnumber
00878             if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) {
00879                 $maxnumber = $datasetdef->itemcount;
00880             }
00881         }
00882         // Handle adding and removing of dataset items
00883         $i = 1;
00884         if ($maxnumber > self::MAX_DATASET_ITEMS) {
00885             $maxnumber = self::MAX_DATASET_ITEMS;
00886         }
00887 
00888         ksort($fromform->definition);
00889         foreach ($fromform->definition as $key => $defid) {
00890             //if the delete button has not been pressed then skip the datasetitems
00891             //in the 'add item' part of the form.
00892             if ($i > count($datasetdefs)*$maxnumber) {
00893                 break;
00894             }
00895             $addeditem = new stdClass();
00896             $addeditem->definition = $datasetdefs[$defid]->id;
00897             $addeditem->value = $fromform->number[$i];
00898             $addeditem->itemnumber = ceil($i / count($datasetdefs));
00899 
00900             if ($fromform->itemid[$i]) {
00901                 // Reuse any previously used record
00902                 $addeditem->id = $fromform->itemid[$i];
00903                 $DB->update_record('question_dataset_items', $addeditem);
00904             } else {
00905                 $DB->insert_record('question_dataset_items', $addeditem);
00906             }
00907 
00908             $i++;
00909         }
00910         if (isset($addeditem->itemnumber) && $maxnumber < $addeditem->itemnumber
00911                 && $addeditem->itemnumber < self::MAX_DATASET_ITEMS) {
00912             $maxnumber = $addeditem->itemnumber;
00913             foreach ($datasetdefs as $key => $newdef) {
00914                 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
00915                     $newdef->itemcount = $maxnumber;
00916                     // Save the new value for options
00917                     $DB->update_record('question_dataset_definitions', $newdef);
00918                 }
00919             }
00920         }
00921         // adding supplementary items
00922         $numbertoadd = 0;
00923         if (isset($fromform->addbutton) && $fromform->selectadd > 0 &&
00924                 $maxnumber < self::MAX_DATASET_ITEMS) {
00925             $numbertoadd = $fromform->selectadd;
00926             if (self::MAX_DATASET_ITEMS - $maxnumber < $numbertoadd) {
00927                 $numbertoadd = self::MAX_DATASET_ITEMS - $maxnumber;
00928             }
00929             //add the other items.
00930             // Generate a new dataset item (or reuse an old one)
00931             foreach ($datasetdefs as $defid => $datasetdef) {
00932                 // in case that for category datasets some new items has been added
00933                 // get actual values
00934                 // fix regenerate for this datadefs
00935                 $defregenerate = 0;
00936                 if ($synchronize &&
00937                         !empty ($fromform->nextpageparam["datasetregenerate[$datasetdef->name"])) {
00938                     $defregenerate = 1;
00939                 } else if (!$synchronize &&
00940                         (($regenerate == 1 && $datasetdef->category == 0) ||$regenerate == 2)) {
00941                     $defregenerate = 1;
00942                 }
00943                 if (isset($datasetdef->id)) {
00944                     $datasetdefs[$defid]->items =
00945                             $this->get_database_dataset_items($datasetdef->id);
00946                 }
00947                 for ($numberadded = $maxnumber+1; $numberadded <= $maxnumber + $numbertoadd;
00948                         $numberadded++) {
00949                     if (isset($datasetdefs[$defid]->items[$numberadded])) {
00950                         // in case of regenerate it modifies the already existing record
00951                         if ($defregenerate) {
00952                             $datasetitem = new stdClass();
00953                             $datasetitem->id = $datasetdefs[$defid]->items[$numberadded]->id;
00954                             $datasetitem->definition = $datasetdef->id;
00955                             $datasetitem->itemnumber = $numberadded;
00956                             $datasetitem->value =
00957                                     $this->generate_dataset_item($datasetdef->options);
00958                             $DB->update_record('question_dataset_items', $datasetitem);
00959                         }
00960                         //if not regenerate do nothing as there is already a record
00961                     } else {
00962                         $datasetitem = new stdClass();
00963                         $datasetitem->definition = $datasetdef->id;
00964                         $datasetitem->itemnumber = $numberadded;
00965                         if ($this->supports_dataset_item_generation()) {
00966                             $datasetitem->value =
00967                                     $this->generate_dataset_item($datasetdef->options);
00968                         } else {
00969                             $datasetitem->value = '';
00970                         }
00971                         $DB->insert_record('question_dataset_items', $datasetitem);
00972                     }
00973                 }//for number added
00974             }// datasetsdefs end
00975             $maxnumber += $numbertoadd;
00976             foreach ($datasetdefs as $key => $newdef) {
00977                 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
00978                     $newdef->itemcount = $maxnumber;
00979                     // Save the new value for options
00980                     $DB->update_record('question_dataset_definitions', $newdef);
00981                 }
00982             }
00983         }
00984 
00985         if (isset($fromform->deletebutton)) {
00986             if (isset($fromform->selectdelete)) {
00987                 $newmaxnumber = $maxnumber-$fromform->selectdelete;
00988             } else {
00989                 $newmaxnumber = $maxnumber-1;
00990             }
00991             if ($newmaxnumber < 0) {
00992                 $newmaxnumber = 0;
00993             }
00994             foreach ($datasetdefs as $datasetdef) {
00995                 if ($datasetdef->itemcount == $maxnumber) {
00996                     $datasetdef->itemcount= $newmaxnumber;
00997                     $DB->update_record('question_dataset_definitions', $datasetdef);
00998                 }
00999             }
01000         }
01001     }
01002     public function generate_dataset_item($options) {
01003         if (!preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
01004                 $options, $regs)) {
01005             // Unknown options...
01006             return false;
01007         }
01008         if ($regs[1] == 'uniform') {
01009             $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
01010             return sprintf("%.".$regs[4].'f', $nbr);
01011 
01012         } else if ($regs[1] == 'loguniform') {
01013             $log0 = log(abs($regs[2])); // It would have worked the other way to
01014             $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
01015             return sprintf("%.".$regs[4].'f', $nbr);
01016 
01017         } else {
01018             print_error('disterror', 'question', '', $regs[1]);
01019         }
01020         return '';
01021     }
01022 
01023     public function comment_header($question) {
01024         $strheader = '';
01025         $delimiter = '';
01026 
01027         $answers = $question->options->answers;
01028 
01029         foreach ($answers as $key => $answer) {
01030             if (is_string($answer)) {
01031                 $strheader .= $delimiter.$answer;
01032             } else {
01033                 $strheader .= $delimiter.$answer->answer;
01034             }
01035             $delimiter = '<br/><br/><br/>';
01036         }
01037         return $strheader;
01038     }
01039 
01040     public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext,
01041             $answers, $data, $number) {
01042         global $DB;
01043         $comment = new stdClass();
01044         $comment->stranswers = array();
01045         $comment->outsidelimit = false;
01046         $comment->answers = array();
01047         // Find a default unit:
01048         if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units',
01049                 array('question' => $questionid, 'multiplier' => 1.0))) {
01050             $unit = $unit->unit;
01051         } else {
01052             $unit = '';
01053         }
01054 
01055         $answers = fullclone($answers);
01056         $errors = '';
01057         $delimiter = ': ';
01058         $virtualqtype =  $qtypeobj->get_virtual_qtype();
01059         foreach ($answers as $key => $answer) {
01060             $formula = $this->substitute_variables($answer->answer, $data);
01061             $formattedanswer = qtype_calculated_calculate_answer(
01062                 $answer->answer, $data, $answer->tolerance,
01063                 $answer->tolerancetype, $answer->correctanswerlength,
01064                 $answer->correctanswerformat, $unit);
01065             if ($formula === '*') {
01066                 $answer->min = ' ';
01067                 $formattedanswer->answer = $answer->answer;
01068             } else {
01069                 eval('$ansvalue = '.$formula.';');
01070                 $ans = new qtype_numerical_answer(0, $ansvalue, 0, '', 0, $answer->tolerance);
01071                 $ans->tolerancetype = $answer->tolerancetype;
01072                 list($answer->min, $answer->max) = $ans->get_tolerance_interval($answer);
01073             }
01074             if ($answer->min === '') {
01075                 // This should mean that something is wrong
01076                 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
01077             } else if ($formula === '*') {
01078                 $comment->stranswers[$key] = $formula . ' = ' .
01079                         get_string('anyvalue', 'qtype_calculated') . '<br/><br/><br/>';
01080             } else {
01081                 $comment->stranswers[$key] = $formula . ' = ' . $formattedanswer->answer . '<br/>';
01082                 $correcttrue->correct = $formattedanswer->answer;
01083                 $correcttrue->true = $answer->answer;
01084                 if ($formattedanswer->answer < $answer->min ||
01085                         $formattedanswer->answer > $answer->max) {
01086                     $comment->outsidelimit = true;
01087                     $comment->answers[$key] = $key;
01088                     $comment->stranswers[$key] .=
01089                             get_string('trueansweroutsidelimits', 'qtype_calculated', $correcttrue);
01090                 } else {
01091                     $comment->stranswers[$key] .=
01092                             get_string('trueanswerinsidelimits', 'qtype_calculated', $correcttrue);
01093                 }
01094                 $comment->stranswers[$key] .= '<br/>';
01095                 $comment->stranswers[$key] .= get_string('min', 'qtype_calculated') .
01096                         $delimiter . $answer->min . ' --- ';
01097                 $comment->stranswers[$key] .= get_string('max', 'qtype_calculated') .
01098                         $delimiter . $answer->max;
01099             }
01100         }
01101         return fullclone($comment);
01102     }
01103     public function multichoice_comment_on_datasetitems($questionid, $questiontext,
01104             $answers, $data, $number) {
01105         global $DB;
01106         $comment = new stdClass();
01107         $comment->stranswers = array();
01108         $comment->outsidelimit = false;
01109         $comment->answers = array();
01110         // Find a default unit:
01111         if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units',
01112                 array('question' => $questionid, 'multiplier' => 1.0))) {
01113             $unit = $unit->unit;
01114         } else {
01115             $unit = '';
01116         }
01117 
01118         $answers = fullclone($answers);
01119         $errors = '';
01120         $delimiter = ': ';
01121         foreach ($answers as $key => $answer) {
01122             $answer->answer = $this->substitute_variables($answer->answer, $data);
01123             //evaluate the equations i.e {=5+4)
01124             $qtext = '';
01125             $qtextremaining = $answer->answer;
01126             while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
01127                 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
01128                 $qtext = $qtext.$qtextsplits[0];
01129                 $qtextremaining = $qtextsplits[1];
01130                 if (empty($regs1[1])) {
01131                     $str = '';
01132                 } else {
01133                     if ($formulaerrors = qtype_calculated_find_formula_errors($regs1[1])) {
01134                         $str = $formulaerrors;
01135                     } else {
01136                         eval('$str = '.$regs1[1].';');
01137 
01138                         $texteval= qtype_calculated_calculate_answer(
01139                             $str, $data, $answer->tolerance,
01140                             $answer->tolerancetype, $answer->correctanswerlength,
01141                             $answer->correctanswerformat, '');
01142                         $str = $texteval->answer;
01143 
01144                     }
01145                 }
01146                 $qtext = $qtext.$str;
01147             }
01148             $answer->answer = $qtext.$qtextremaining;;
01149             $comment->stranswers[$key]= $answer->answer;
01150 
01151         }
01152         return fullclone($comment);
01153     }
01154 
01155     public function tolerance_types() {
01156         return array(
01157             '1' => get_string('relative', 'qtype_numerical'),
01158             '2' => get_string('nominal', 'qtype_numerical'),
01159             '3' => get_string('geometric', 'qtype_numerical')
01160         );
01161     }
01162 
01163     public function dataset_options($form, $name, $mandatory = true,
01164             $renameabledatasets = false) {
01165         // Takes datasets from the parent implementation but
01166         // filters options that are currently not accepted by calculated
01167         // It also determines a default selection...
01168         // $renameabledatasets not implemented anmywhere
01169         list($options, $selected) = $this->dataset_options_from_database(
01170                 $form, $name, '', 'qtype_calculated');
01171 
01172         foreach ($options as $key => $whatever) {
01173             if (!preg_match('~^1-~', $key) && $key != '0') {
01174                 unset($options[$key]);
01175             }
01176         }
01177         if (!$selected) {
01178             if ($mandatory) {
01179                 $selected =  "1-0-$name"; // Default
01180             } else {
01181                 $selected = '0'; // Default
01182             }
01183         }
01184         return array($options, $selected);
01185     }
01186 
01187     public function construct_dataset_menus($form, $mandatorydatasets,
01188             $optionaldatasets) {
01189         global $OUTPUT;
01190         $datasetmenus = array();
01191         foreach ($mandatorydatasets as $datasetname) {
01192             if (!isset($datasetmenus[$datasetname])) {
01193                 list($options, $selected) =
01194                     $this->dataset_options($form, $datasetname);
01195                 unset($options['0']); // Mandatory...
01196                 $datasetmenus[$datasetname] = html_writer::select(
01197                         $options, 'dataset[]', $selected, null);
01198             }
01199         }
01200         foreach ($optionaldatasets as $datasetname) {
01201             if (!isset($datasetmenus[$datasetname])) {
01202                 list($options, $selected) =
01203                     $this->dataset_options($form, $datasetname);
01204                 $datasetmenus[$datasetname] = html_writer::select(
01205                         $options, 'dataset[]', $selected, null);
01206             }
01207         }
01208         return $datasetmenus;
01209     }
01210 
01211     public function substitute_variables($str, $dataset) {
01212         global $OUTPUT;
01213         // testing for wrong numerical values
01214         // all calculations used this function so testing here should be OK
01215 
01216         foreach ($dataset as $name => $value) {
01217             $val = $value;
01218             if (! is_numeric($val)) {
01219                 $a = new stdClass();
01220                 $a->name = '{'.$name.'}';
01221                 $a->value = $value;
01222                 echo $OUTPUT->notification(get_string('notvalidnumber', 'qtype_calculated', $a));
01223                 $val = 1.0;
01224             }
01225             if ($val < 0) {
01226                 $str = str_replace('{'.$name.'}', '('.$val.')', $str);
01227             } else {
01228                 $str = str_replace('{'.$name.'}', $val, $str);
01229             }
01230         }
01231         return $str;
01232     }
01233 
01234     public function evaluate_equations($str, $dataset) {
01235         $formula = $this->substitute_variables($str, $dataset);
01236         if ($error = qtype_calculated_find_formula_errors($formula)) {
01237             return $error;
01238         }
01239         return $str;
01240     }
01241 
01242     public function substitute_variables_and_eval($str, $dataset) {
01243         $formula = $this->substitute_variables($str, $dataset);
01244         if ($error = qtype_calculated_find_formula_errors($formula)) {
01245             return $error;
01246         }
01247         // Calculate the correct answer
01248         if (empty($formula)) {
01249             $str = '';
01250         } else if ($formula === '*') {
01251             $str = '*';
01252         } else {
01253             eval('$str = '.$formula.';');
01254         }
01255         return $str;
01256     }
01257 
01258     public function get_dataset_definitions($questionid, $newdatasets) {
01259         global $DB;
01260         //get the existing datasets for this question
01261         $datasetdefs = array();
01262         if (!empty($questionid)) {
01263             global $CFG;
01264             $sql = "SELECT i.*
01265                       FROM {question_datasets} d, {question_dataset_definitions} i
01266                      WHERE d.question = ? AND d.datasetdefinition = i.id
01267                   ORDER BY i.id";
01268             if ($records = $DB->get_records_sql($sql, array($questionid))) {
01269                 foreach ($records as $r) {
01270                     $datasetdefs["$r->type-$r->category-$r->name"] = $r;
01271                 }
01272             }
01273         }
01274 
01275         foreach ($newdatasets as $dataset) {
01276             if (!$dataset) {
01277                 continue; // The no dataset case...
01278             }
01279 
01280             if (!isset($datasetdefs[$dataset])) {
01281                 //make new datasetdef
01282                 list($type, $category, $name) = explode('-', $dataset, 3);
01283                 $datasetdef = new stdClass();
01284                 $datasetdef->type = $type;
01285                 $datasetdef->name = $name;
01286                 $datasetdef->category  = $category;
01287                 $datasetdef->itemcount = 0;
01288                 $datasetdef->options   = 'uniform:1.0:10.0:1';
01289                 $datasetdefs[$dataset] = clone($datasetdef);
01290             }
01291         }
01292         return $datasetdefs;
01293     }
01294 
01295     public function save_dataset_definitions($form) {
01296         global $DB;
01297         // save synchronize
01298 
01299         if (empty($form->dataset)) {
01300             $form->dataset = array();
01301         }
01302         // Save datasets
01303         $datasetdefinitions = $this->get_dataset_definitions($form->id, $form->dataset);
01304         $tmpdatasets = array_flip($form->dataset);
01305         $defids = array_keys($datasetdefinitions);
01306         foreach ($defids as $defid) {
01307             $datasetdef = &$datasetdefinitions[$defid];
01308             if (isset($datasetdef->id)) {
01309                 if (!isset($tmpdatasets[$defid])) {
01310                     // This dataset is not used any more, delete it
01311                     $DB->delete_records('question_datasets',
01312                             array('question' => $form->id, 'datasetdefinition' => $datasetdef->id));
01313                     if ($datasetdef->category == 0) {
01314                         // Question local dataset
01315                         $DB->delete_records('question_dataset_definitions',
01316                                 array('id' => $datasetdef->id));
01317                         $DB->delete_records('question_dataset_items',
01318                                 array('definition' => $datasetdef->id));
01319                     }
01320                 }
01321                 // This has already been saved or just got deleted
01322                 unset($datasetdefinitions[$defid]);
01323                 continue;
01324             }
01325 
01326             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
01327 
01328             if (0 != $datasetdef->category) {
01329                 // We need to look for already existing
01330                 // datasets in the category.
01331                 // By first creating the datasetdefinition above we
01332                 // can manage to automatically take care of
01333                 // some possible realtime concurrence
01334                 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
01335                         'type = ? AND name = ? AND category = ? AND id < ?
01336                         ORDER BY id DESC',
01337                         array($datasetdef->type, $datasetdef->name,
01338                                 $datasetdef->category, $datasetdef->id))) {
01339 
01340                     while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
01341                         $DB->delete_records('question_dataset_definitions',
01342                                 array('id' => $datasetdef->id));
01343                         $datasetdef = $olderdatasetdef;
01344                     }
01345                 }
01346             }
01347 
01348             // Create relation to this dataset:
01349             $questiondataset = new stdClass();
01350             $questiondataset->question = $form->id;
01351             $questiondataset->datasetdefinition = $datasetdef->id;
01352             $DB->insert_record('question_datasets', $questiondataset);
01353             unset($datasetdefinitions[$defid]);
01354         }
01355 
01356         // Remove local obsolete datasets as well as relations
01357         // to datasets in other categories:
01358         if (!empty($datasetdefinitions)) {
01359             foreach ($datasetdefinitions as $def) {
01360                 $DB->delete_records('question_datasets',
01361                         array('question' => $form->id, 'datasetdefinition' => $def->id));
01362 
01363                 if ($def->category == 0) { // Question local dataset
01364                     $DB->delete_records('question_dataset_definitions',
01365                             array('id' => $def->id));
01366                     $DB->delete_records('question_dataset_items',
01367                             array('definition' => $def->id));
01368                 }
01369             }
01370         }
01371     }
01377     public function save_as_new_dataset_definitions($form, $initialid) {
01378         global $CFG, $DB;
01379         // Get the datasets from the intial question
01380         $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset);
01381         // $tmpdatasets contains those of the new question
01382         $tmpdatasets = array_flip($form->dataset);
01383         $defids = array_keys($datasetdefinitions);// new datasets
01384         foreach ($defids as $defid) {
01385             $datasetdef = &$datasetdefinitions[$defid];
01386             if (isset($datasetdef->id)) {
01387                 // This dataset exist in the initial question
01388                 if (!isset($tmpdatasets[$defid])) {
01389                     // do not exist in the new question so ignore
01390                     unset($datasetdefinitions[$defid]);
01391                     continue;
01392                 }
01393                 // create a copy but not for category one
01394                 if (0 == $datasetdef->category) {
01395                     $olddatasetid = $datasetdef->id;
01396                     $olditemcount = $datasetdef->itemcount;
01397                     $datasetdef->itemcount = 0;
01398                     $datasetdef->id = $DB->insert_record('question_dataset_definitions',
01399                             $datasetdef);
01400                     //copy the dataitems
01401                     $olditems = $this->get_database_dataset_items($olddatasetid);
01402                     if (count($olditems) > 0) {
01403                         $itemcount = 0;
01404                         foreach ($olditems as $item) {
01405                             $item->definition = $datasetdef->id;
01406                             $DB->insert_record('question_dataset_items', $item);
01407                             $itemcount++;
01408                         }
01409                         //update item count to olditemcount if
01410                         // at least this number of items has been recover from the database
01411                         if ($olditemcount <= $itemcount) {
01412                             $datasetdef->itemcount = $olditemcount;
01413                         } else {
01414                             $datasetdef->itemcount = $itemcount;
01415                         }
01416                         $DB->update_record('question_dataset_definitions', $datasetdef);
01417                     } // end of  copy the dataitems
01418                 }// end of  copy the datasetdef
01419                 // Create relation to the new question with this
01420                 // copy as new datasetdef from the initial question
01421                 $questiondataset = new stdClass();
01422                 $questiondataset->question = $form->id;
01423                 $questiondataset->datasetdefinition = $datasetdef->id;
01424                 $DB->insert_record('question_datasets', $questiondataset);
01425                 unset($datasetdefinitions[$defid]);
01426                 continue;
01427             }// end of datasetdefs from the initial question
01428             // really new one code similar to save_dataset_definitions()
01429             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
01430 
01431             if (0 != $datasetdef->category) {
01432                 // We need to look for already existing
01433                 // datasets in the category.
01434                 // By first creating the datasetdefinition above we
01435                 // can manage to automatically take care of
01436                 // some possible realtime concurrence
01437                 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
01438                         "type = ? AND name = ? AND category = ? AND id < ?
01439                         ORDER BY id DESC",
01440                         array($datasetdef->type, $datasetdef->name,
01441                                 $datasetdef->category, $datasetdef->id))) {
01442 
01443                     while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
01444                         $DB->delete_records('question_dataset_definitions',
01445                                 array('id' => $datasetdef->id));
01446                         $datasetdef = $olderdatasetdef;
01447                     }
01448                 }
01449             }
01450 
01451             // Create relation to this dataset:
01452             $questiondataset = new stdClass();
01453             $questiondataset->question = $form->id;
01454             $questiondataset->datasetdefinition = $datasetdef->id;
01455             $DB->insert_record('question_datasets', $questiondataset);
01456             unset($datasetdefinitions[$defid]);
01457         }
01458 
01459         // Remove local obsolete datasets as well as relations
01460         // to datasets in other categories:
01461         if (!empty($datasetdefinitions)) {
01462             foreach ($datasetdefinitions as $def) {
01463                 $DB->delete_records('question_datasets',
01464                         array('question' => $form->id, 'datasetdefinition' => $def->id));
01465 
01466                 if ($def->category == 0) { // Question local dataset
01467                     $DB->delete_records('question_dataset_definitions',
01468                             array('id' => $def->id));
01469                     $DB->delete_records('question_dataset_items',
01470                             array('definition' => $def->id));
01471                 }
01472             }
01473         }
01474     }
01475 
01476     // Dataset functionality
01477     public function pick_question_dataset($question, $datasetitem) {
01478         // Select a dataset in the following format:
01479         // An array indexed by the variable names (d.name) pointing to the value
01480         // to be substituted
01481         global $CFG, $DB;
01482         if (!$dataitems = $DB->get_records_sql(
01483                 "SELECT i.id, d.name, i.value
01484                    FROM {question_dataset_definitions} d,
01485                         {question_dataset_items} i,
01486                         {question_datasets} q
01487                   WHERE q.question = ?
01488                     AND q.datasetdefinition = d.id
01489                     AND d.id = i.definition
01490                     AND i.itemnumber = ?
01491                ORDER BY i.id DESC ", array($question->id, $datasetitem))) {
01492             $a = new stdClass();
01493             $a->id = $question->id;
01494             $a->item = $datasetitem;
01495             print_error('cannotgetdsfordependent', 'question', '', $a);
01496         }
01497         $dataset = Array();
01498         foreach ($dataitems as $id => $dataitem) {
01499             if (!isset($dataset[$dataitem->name])) {
01500                 $dataset[$dataitem->name] = $dataitem->value;
01501             }
01502         }
01503         return $dataset;
01504     }
01505 
01506     public function dataset_options_from_database($form, $name, $prefix = '',
01507             $langfile = 'qtype_calculated') {
01508         global $CFG, $DB;
01509         $type = 1; // only type = 1 (i.e. old 'LITERAL') has ever been used
01510 
01511         // First options - it is not a dataset...
01512         $options['0'] = get_string($prefix.'nodataset', $langfile);
01513         // new question no local
01514         if (!isset($form->id) || $form->id == 0) {
01515             $key = "$type-0-$name";
01516             $options[$key] = get_string($prefix."newlocal$type", $langfile);
01517             $currentdatasetdef = new stdClass();
01518             $currentdatasetdef->type = '0';
01519         } else {
01520 
01521             // Construct question local options
01522             $sql = "SELECT a.*
01523                 FROM {question_dataset_definitions} a, {question_datasets} b
01524                WHERE a.id = b.datasetdefinition AND a.type = '1' AND b.question = ? AND a.name = ?";
01525             $currentdatasetdef = $DB->get_record_sql($sql, array($form->id, $name));
01526             if (!$currentdatasetdef) {
01527                 $currentdatasetdef->type = '0';
01528             }
01529             $key = "$type-0-$name";
01530             if ($currentdatasetdef->type == $type
01531                     and $currentdatasetdef->category == 0) {
01532                 $options[$key] = get_string($prefix."keptlocal$type", $langfile);
01533             } else {
01534                 $options[$key] = get_string($prefix."newlocal$type", $langfile);
01535             }
01536         }
01537         // Construct question category options
01538         $categorydatasetdefs = $DB->get_records_sql(
01539             "SELECT b.question, a.*
01540             FROM {question_datasets} b,
01541             {question_dataset_definitions} a
01542             WHERE a.id = b.datasetdefinition
01543             AND a.type = '1'
01544             AND a.category = ?
01545             AND a.name = ?", array($form->category, $name));
01546         $type = 1;
01547         $key = "$type-$form->category-$name";
01548         if (!empty($categorydatasetdefs)) {
01549             // there is at least one with the same name
01550             if (isset($form->id) && isset($categorydatasetdefs[$form->id])) {
01551                 // it is already used by this question
01552                 $options[$key] = get_string($prefix."keptcategory$type", $langfile);
01553             } else {
01554                 $options[$key] = get_string($prefix."existingcategory$type", $langfile);
01555             }
01556         } else {
01557             $options[$key] = get_string($prefix."newcategory$type", $langfile);
01558         }
01559         // All done!
01560         return array($options, $currentdatasetdef->type
01561             ? "$currentdatasetdef->type-$currentdatasetdef->category-$name"
01562             : '');
01563     }
01564 
01565     public function find_dataset_names($text) {
01566         // Returns the possible dataset names found in the text as an array
01567         // The array has the dataset name for both key and value
01568         $datasetnames = array();
01569         while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) {
01570             $datasetnames[$regs[1]] = $regs[1];
01571             $text = str_replace($regs[0], '', $text);
01572         }
01573         return $datasetnames;
01574     }
01575 
01581     public function get_dataset_definitions_category($form) {
01582         global $CFG, $DB;
01583         $datasetdefs = array();
01584         $lnamemax = 30;
01585         if (!empty($form->category)) {
01586             $sql = "SELECT i.*, d.*
01587                       FROM {question_datasets} d, {question_dataset_definitions} i
01588                      WHERE i.id = d.datasetdefinition AND i.category = ?";
01589             if ($records = $DB->get_records_sql($sql, array($form->category))) {
01590                 foreach ($records as $r) {
01591                     if (!isset ($datasetdefs["$r->name"])) {
01592                         $datasetdefs["$r->name"] = $r->itemcount;
01593                     }
01594                 }
01595             }
01596         }
01597         return $datasetdefs;
01598     }
01599 
01607     public function print_dataset_definitions_category($form) {
01608         global $CFG, $DB;
01609         $datasetdefs = array();
01610         $lnamemax = 22;
01611         $namestr          = get_string('name');
01612         $rangeofvaluestr  = get_string('minmax', 'qtype_calculated');
01613         $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
01614         $itemscountstr    = get_string('itemscount', 'qtype_calculated');
01615         $text = '';
01616         if (!empty($form->category)) {
01617             list($category) = explode(',', $form->category);
01618             $sql = "SELECT i.*, d.*
01619                 FROM {question_datasets} d,
01620         {question_dataset_definitions} i
01621         WHERE i.id = d.datasetdefinition
01622         AND i.category = ?";
01623             if ($records = $DB->get_records_sql($sql, array($category))) {
01624                 foreach ($records as $r) {
01625                     $sql1 = "SELECT q.*
01626                                FROM {question} q
01627                               WHERE q.id = ?";
01628                     if (!isset ($datasetdefs["$r->type-$r->category-$r->name"])) {
01629                         $datasetdefs["$r->type-$r->category-$r->name"]= $r;
01630                     }
01631                     if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
01632                         $datasetdefs["$r->type-$r->category-$r->name"]->questions[
01633                                 $r->question]->name = $questionb[$r->question]->name;
01634                     }
01635                 }
01636             }
01637         }
01638         if (!empty ($datasetdefs)) {
01639 
01640             $text = "<table width=\"100%\" border=\"1\"><tr>
01641                     <th style=\"white-space:nowrap;\" class=\"header\"
01642                             scope=\"col\">$namestr</th>
01643                     <th style=\"white-space:nowrap;\" class=\"header\"
01644                             scope=\"col\">$rangeofvaluestr</th>
01645                     <th style=\"white-space:nowrap;\" class=\"header\"
01646                             scope=\"col\">$itemscountstr</th>
01647                     <th style=\"white-space:nowrap;\" class=\"header\"
01648                             scope=\"col\">$questionusingstr</th>
01649                     </tr>";
01650             foreach ($datasetdefs as $datasetdef) {
01651                 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4);
01652                 $text .= "<tr>
01653                         <td valign=\"top\" align=\"center\">$datasetdef->name</td>
01654                         <td align=\"center\" valign=\"top\">$min <strong>-</strong> $max</td>
01655                         <td align=\"right\" valign=\"top\">$datasetdef->itemcount&nbsp;&nbsp;</td>
01656                         <td align=\"left\">";
01657                 foreach ($datasetdef->questions as $qu) {
01658                     //limit the name length displayed
01659                     if (!empty($qu->name)) {
01660                         $qu->name = (strlen($qu->name) > $lnamemax) ?
01661                             substr($qu->name, 0, $lnamemax).'...' : $qu->name;
01662                     } else {
01663                         $qu->name = '';
01664                     }
01665                     $text .= " &nbsp;&nbsp; $qu->name <br/>";
01666                 }
01667                 $text .= "</td></tr>";
01668             }
01669             $text .= "</table>";
01670         } else {
01671             $text .= get_string('nosharedwildcard', 'qtype_calculated');
01672         }
01673         return $text;
01674     }
01675 
01684     public function print_dataset_definitions_category_shared($question, $datasetdefsq) {
01685         global $CFG, $DB;
01686         $datasetdefs = array();
01687         $lnamemax = 22;
01688         $namestr          = get_string('name', 'quiz');
01689         $rangeofvaluestr  = get_string('minmax', 'qtype_calculated');
01690         $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
01691         $itemscountstr    = get_string('itemscount', 'qtype_calculated');
01692         $text = '';
01693         if (!empty($question->category)) {
01694             list($category) = explode(',', $question->category);
01695             $sql = "SELECT i.*, d.*
01696                       FROM {question_datasets} d, {question_dataset_definitions} i
01697                      WHERE i.id = d.datasetdefinition AND i.category = ?";
01698             if ($records = $DB->get_records_sql($sql, array($category))) {
01699                 foreach ($records as $r) {
01700                     $sql1 = "SELECT q.*
01701                                FROM {question} q
01702                               WHERE q.id = ?";
01703                     if (!isset ($datasetdefs["$r->type-$r->category-$r->name"])) {
01704                         $datasetdefs["$r->type-$r->category-$r->name"]= $r;
01705                     }
01706                     if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
01707                         $datasetdefs["$r->type-$r->category-$r->name"]->questions[
01708                                 $r->question]->name = $questionb[$r->question]->name;
01709                         $datasetdefs["$r->type-$r->category-$r->name"]->questions[
01710                                 $r->question]->id = $questionb[$r->question]->id;
01711                     }
01712                 }
01713             }
01714         }
01715         if (!empty ($datasetdefs)) {
01716 
01717             $text  = "<table width=\"100%\" border=\"1\"><tr>
01718                     <th style=\"white-space:nowrap;\" class=\"header\"
01719                             scope=\"col\">$namestr</th>";
01720             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
01721                     scope=\"col\">$itemscountstr</th>";
01722             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
01723                     scope=\"col\">&nbsp;&nbsp;$questionusingstr &nbsp;&nbsp;</th>";
01724             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
01725                     scope=\"col\">Quiz</th>";
01726             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
01727                     scope=\"col\">Attempts</th></tr>";
01728             foreach ($datasetdefs as $datasetdef) {
01729                 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4);
01730                 $count = count($datasetdef->questions);
01731                 $text .= "<tr>
01732                         <td style=\"white-space:nowrap;\" valign=\"top\"
01733                                 align=\"center\" rowspan=\"$count\"> $datasetdef->name </td>
01734                         <td align=\"right\" valign=\"top\"
01735                                 rowspan=\"$count\">$datasetdef->itemcount</td>";
01736                 $line = 0;
01737                 foreach ($datasetdef->questions as $qu) {
01738                     //limit the name length displayed
01739                     if (!empty($qu->name)) {
01740                         $qu->name = (strlen($qu->name) > $lnamemax) ?
01741                             substr($qu->name, 0, $lnamemax).'...' : $qu->name;
01742                     } else {
01743                         $qu->name = '';
01744                     }
01745                     if ($line) {
01746                         $text .= "<tr>";
01747                     }
01748                     $line++;
01749                     $text .= "<td align=\"left\" style=\"white-space:nowrap;\">$qu->name</td>";
01750                     $nbofquiz = 0;
01751                     $nbofattempts= 0;
01752                     $usedinquiz = false;
01753                     if ($list = $DB->get_records('quiz_question_instances',
01754                             array('question' => $qu->id))) {
01755                         $usedinquiz = true;
01756                         foreach ($list as $key => $li) {
01757                             $nbofquiz ++;
01758                             if ($att = $DB->get_records('quiz_attempts',
01759                                     array('quiz' => $li->quiz, 'preview' => '0'))) {
01760                                 $nbofattempts+= count($att);
01761                             }
01762                         }
01763                     }
01764                     if ($usedinquiz) {
01765                         $text .= "<td align=\"center\">$nbofquiz</td>";
01766                     } else {
01767                         $text .= "<td align=\"center\">0</td>";
01768                     }
01769                     if ($usedinquiz) {
01770                         $text .= "<td align=\"center\">$nbofattempts";
01771                     } else {
01772                         $text .= "<td align=\"left\"><br/>";
01773                     }
01774 
01775                     $text .= "</td></tr>";
01776                 }
01777             }
01778             $text .= "</table>";
01779         } else {
01780             $text .= get_string('nosharedwildcard', 'qtype_calculated');
01781         }
01782         return $text;
01783     }
01784 
01785     public function find_math_equations($text) {
01786         // Returns the possible dataset names found in the text as an array
01787         // The array has the dataset name for both key and value
01788         $equations = array();
01789         while (preg_match('~\{=([^[:space:]}]*)}~', $text, $regs)) {
01790             $equations[] = $regs[1];
01791             $text = str_replace($regs[0], '', $text);
01792         }
01793         return $equations;
01794     }
01795 
01796     public function get_virtual_qtype() {
01797         return question_bank::get_qtype('numerical');
01798     }
01799 
01800     public function get_possible_responses($questiondata) {
01801         $responses = array();
01802 
01803         $virtualqtype = $this->get_virtual_qtype();
01804         $unit = $virtualqtype->get_default_numerical_unit($questiondata);
01805 
01806         $tolerancetypes = $this->tolerance_types();
01807 
01808         $starfound = false;
01809         foreach ($questiondata->options->answers as $aid => $answer) {
01810             $responseclass = $answer->answer;
01811 
01812             if ($responseclass === '*') {
01813                 $starfound = true;
01814             } else {
01815                 $a = new stdClass();
01816                 $a->answer = $virtualqtype->add_unit($questiondata, $responseclass, $unit);
01817                 $a->tolerance = $answer->tolerance;
01818                 $a->tolerancetype = $tolerancetypes[$answer->tolerancetype];
01819 
01820                 $responseclass = get_string('answerwithtolerance', 'qtype_calculated', $a);
01821             }
01822 
01823             $responses[$aid] = new question_possible_response($responseclass,
01824                     $answer->fraction);
01825         }
01826 
01827         if (!$starfound) {
01828             $responses[0] = new question_possible_response(
01829             get_string('didnotmatchanyanswer', 'question'), 0);
01830         }
01831 
01832         $responses[null] = question_possible_response::no_response();
01833 
01834         return array($questiondata->id => $responses);
01835     }
01836 
01837     public function move_files($questionid, $oldcontextid, $newcontextid) {
01838         $fs = get_file_storage();
01839 
01840         parent::move_files($questionid, $oldcontextid, $newcontextid);
01841         $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid);
01842     }
01843 
01844     protected function delete_files($questionid, $contextid) {
01845         $fs = get_file_storage();
01846 
01847         parent::delete_files($questionid, $contextid);
01848         $this->delete_files_in_answers($questionid, $contextid);
01849     }
01850 }
01851 
01852 
01853 function qtype_calculated_calculate_answer($formula, $individualdata,
01854     $tolerance, $tolerancetype, $answerlength, $answerformat = '1', $unit = '') {
01855     // The return value has these properties:
01856     // ->answer    the correct answer
01857     // ->min       the lower bound for an acceptable response
01858     // ->max       the upper bound for an accetpable response
01859 
01860     // Exchange formula variables with the correct values...
01861     $answer = question_bank::get_qtype('calculated')->substitute_variables_and_eval(
01862             $formula, $individualdata);
01863     if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */
01864         /*** Adjust to the correct number of decimals ***/
01865         if (stripos($answer, 'e')>0) {
01866             $answerlengthadd = strlen($answer)-stripos($answer, 'e');
01867         } else {
01868             $answerlengthadd = 0;
01869         }
01870         $calculated->answer = round(floatval($answer), $answerlength+$answerlengthadd);
01871 
01872         if ($answerlength) {
01873             /* Try to include missing zeros at the end */
01874 
01875             if (preg_match('~^(.*\\.)(.*)$~', $calculated->answer, $regs)) {
01876                 $calculated->answer = $regs[1] . substr(
01877                     $regs[2] . '00000000000000000000000000000000000000000x',
01878                     0, $answerlength)
01879                     . $unit;
01880             } else {
01881                 $calculated->answer .=
01882                     substr('.00000000000000000000000000000000000000000x',
01883                         0, $answerlength + 1) . $unit;
01884             }
01885         } else {
01886             /* Attach unit */
01887             $calculated->answer .= $unit;
01888         }
01889 
01890     } else if ($answer) { // Significant figures does only apply if the result is non-zero
01891 
01892         // Convert to positive answer...
01893         if ($answer < 0) {
01894             $answer = -$answer;
01895             $sign = '-';
01896         } else {
01897             $sign = '';
01898         }
01899 
01900         // Determine the format 0.[1-9][0-9]* for the answer...
01901         $p10 = 0;
01902         while ($answer < 1) {
01903             --$p10;
01904             $answer *= 10;
01905         }
01906         while ($answer >= 1) {
01907             ++$p10;
01908             $answer /= 10;
01909         }
01910         // ... and have the answer rounded of to the correct length
01911         $answer = round($answer, $answerlength);
01912 
01913         //if we rounded up to 1.0, place the answer back into 0.[1-9][0-9]* format
01914         if ($answer >= 1) {
01915             ++$p10;
01916             $answer /= 10;
01917         }
01918 
01919         // Have the answer written on a suitable format,
01920         // Either scientific or plain numeric
01921         if (-2 > $p10 || 4 < $p10) {
01922             // Use scientific format:
01923             $exponent = 'e'.--$p10;
01924             $answer *= 10;
01925             if (1 == $answerlength) {
01926                 $calculated->answer = $sign.$answer.$exponent.$unit;
01927             } else {
01928                 // Attach additional zeros at the end of $answer,
01929                 $answer .= (1 == strlen($answer) ? '.' : '')
01930                     . '00000000000000000000000000000000000000000x';
01931                 $calculated->answer = $sign
01932                     .substr($answer, 0, $answerlength +1).$exponent.$unit;
01933             }
01934         } else {
01935             // Stick to plain numeric format
01936             $answer *= "1e$p10";
01937             if (0.1 <= $answer / "1e$answerlength") {
01938                 $calculated->answer = $sign.$answer.$unit;
01939             } else {
01940                 // Could be an idea to add some zeros here
01941                 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '')
01942                     . '00000000000000000000000000000000000000000x';
01943                 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
01944                 $calculated->answer = $sign.substr($answer, 0, $oklen).$unit;
01945             }
01946         }
01947 
01948     } else {
01949         $calculated->answer = 0.0;
01950     }
01951 
01952     // Return the result
01953     return $calculated;
01954 }
01955 
01956 
01957 function qtype_calculated_find_formula_errors($formula) {
01958     // Validates the formula submitted from the question edit page.
01959     // Returns false if everything is alright.
01960     // Otherwise it constructs an error message
01961     // Strip away dataset names
01962     while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
01963         $formula = str_replace($regs[0], '1', $formula);
01964     }
01965 
01966     // Strip away empty space and lowercase it
01967     $formula = strtolower(str_replace(' ', '', $formula));
01968 
01969     $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */
01970     $operatorornumber = "[$safeoperatorchar.0-9eE]";
01971 
01972     while (preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)" .
01973             "\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~",
01974         $formula, $regs)) {
01975         switch ($regs[2]) {
01976             // Simple parenthesis
01977             case '':
01978                 if ((isset($regs[4]) && $regs[4]) || strlen($regs[3]) == 0) {
01979                     return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
01980                 }
01981                 break;
01982 
01983                 // Zero argument functions
01984             case 'pi':
01985                 if ($regs[3]) {
01986                     return get_string('functiontakesnoargs', 'qtype_calculated', $regs[2]);
01987                 }
01988                 break;
01989 
01990                 // Single argument functions (the most common case)
01991             case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
01992             case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
01993             case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
01994             case 'exp': case 'expm1': case 'floor': case 'is_finite':
01995             case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
01996             case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
01997             case 'tan': case 'tanh':
01998                 if (!empty($regs[4]) || empty($regs[3])) {
01999                     return get_string('functiontakesonearg', 'qtype_calculated', $regs[2]);
02000                 }
02001                 break;
02002 
02003                 // Functions that take one or two arguments
02004             case 'log': case 'round':
02005                 if (!empty($regs[5]) || empty($regs[3])) {
02006                     return get_string('functiontakesoneortwoargs', 'qtype_calculated', $regs[2]);
02007                 }
02008                 break;
02009 
02010                 // Functions that must have two arguments
02011             case 'atan2': case 'fmod': case 'pow':
02012                 if (!empty($regs[5]) || empty($regs[4])) {
02013                     return get_string('functiontakestwoargs', 'qtype_calculated', $regs[2]);
02014                 }
02015                 break;
02016 
02017                 // Functions that take two or more arguments
02018             case 'min': case 'max':
02019                 if (empty($regs[4])) {
02020                     return get_string('functiontakesatleasttwo', 'qtype_calculated', $regs[2]);
02021                 }
02022                 break;
02023 
02024             default:
02025                 return get_string('unsupportedformulafunction', 'qtype_calculated', $regs[2]);
02026         }
02027 
02028         // Exchange the function call with '1' and then chack for
02029         // another function call...
02030         if ($regs[1]) {
02031             // The function call is proceeded by an operator
02032             $formula = str_replace($regs[0], $regs[1] . '1', $formula);
02033         } else {
02034             // The function call starts the formula
02035             $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula);
02036         }
02037     }
02038 
02039     if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) {
02040         return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
02041     } else {
02042         // Formula just might be valid
02043         return false;
02044     }
02045 }
 All Data Structures Namespaces Files Functions Variables Enumerations