[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/question/type/numerical/ -> question.php (source)

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Numerical question definition class.
  19   *
  20   * @package    qtype
  21   * @subpackage numerical
  22   * @copyright  2009 The Open University
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Represents a numerical question.
  31   *
  32   * @copyright  2009 The Open University
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class qtype_numerical_question extends question_graded_automatically {
  36      /** @var array of question_answer. */
  37      public $answers = array();
  38  
  39      /** @var int one of the constants UNITNONE, UNITRADIO, UNITSELECT or UNITINPUT. */
  40      public $unitdisplay;
  41      /** @var int one of the constants UNITGRADEDOUTOFMARK or UNITGRADEDOUTOFMAX. */
  42      public $unitgradingtype;
  43      /** @var number the penalty for a missing or unrecognised unit. */
  44      public $unitpenalty;
  45  
  46      /** @var qtype_numerical_answer_processor */
  47      public $ap;
  48  
  49      public function get_expected_data() {
  50          $expected = array('answer' => PARAM_RAW_TRIMMED);
  51          if ($this->has_separate_unit_field()) {
  52              $expected['unit'] = PARAM_RAW_TRIMMED;
  53          }
  54          return $expected;
  55      }
  56  
  57      public function has_separate_unit_field() {
  58          return $this->unitdisplay == qtype_numerical::UNITRADIO ||
  59                  $this->unitdisplay == qtype_numerical::UNITSELECT;
  60      }
  61  
  62      public function start_attempt(question_attempt_step $step, $variant) {
  63          $step->set_qt_var('_separators',
  64                  $this->ap->get_point() . '$' . $this->ap->get_separator());
  65      }
  66  
  67      public function apply_attempt_state(question_attempt_step $step) {
  68          list($point, $separator) = explode('$', $step->get_qt_var('_separators'));
  69                  $this->ap->set_characters($point, $separator);
  70      }
  71  
  72      public function summarise_response(array $response) {
  73          if (isset($response['answer'])) {
  74              $resp = $response['answer'];
  75          } else {
  76              $resp = null;
  77          }
  78  
  79          if ($this->has_separate_unit_field() && !empty($response['unit'])) {
  80              $resp = $this->ap->add_unit($resp, $response['unit']);
  81          }
  82  
  83          return $resp;
  84      }
  85  
  86      public function is_gradable_response(array $response) {
  87          return array_key_exists('answer', $response) &&
  88                  ($response['answer'] || $response['answer'] === '0' || $response['answer'] === 0);
  89      }
  90  
  91      public function is_complete_response(array $response) {
  92          if (!$this->is_gradable_response($response)) {
  93              return false;
  94          }
  95  
  96          list($value, $unit) = $this->ap->apply_units($response['answer']);
  97          if (is_null($value)) {
  98              return false;
  99          }
 100  
 101          if ($this->unitdisplay != qtype_numerical::UNITINPUT && $unit) {
 102              return false;
 103          }
 104  
 105          if ($this->has_separate_unit_field() && empty($response['unit'])) {
 106              return false;
 107          }
 108  
 109          if ($this->ap->contains_thousands_seaparator($response['answer'])) {
 110              return false;
 111          }
 112  
 113          return true;
 114      }
 115  
 116      public function get_validation_error(array $response) {
 117          if (!$this->is_gradable_response($response)) {
 118              return get_string('pleaseenterananswer', 'qtype_numerical');
 119          }
 120  
 121          list($value, $unit) = $this->ap->apply_units($response['answer']);
 122          if (is_null($value)) {
 123              return get_string('invalidnumber', 'qtype_numerical');
 124          }
 125  
 126          if ($this->unitdisplay != qtype_numerical::UNITINPUT && $unit) {
 127              return get_string('invalidnumbernounit', 'qtype_numerical');
 128          }
 129  
 130          if ($this->has_separate_unit_field() && empty($response['unit'])) {
 131              return get_string('unitnotselected', 'qtype_numerical');
 132          }
 133  
 134          if ($this->ap->contains_thousands_seaparator($response['answer'])) {
 135              return get_string('pleaseenteranswerwithoutthousandssep', 'qtype_numerical',
 136                      $this->ap->get_separator());
 137          }
 138  
 139          return '';
 140      }
 141  
 142      public function is_same_response(array $prevresponse, array $newresponse) {
 143          if (!question_utils::arrays_same_at_key_missing_is_blank(
 144                  $prevresponse, $newresponse, 'answer')) {
 145              return false;
 146          }
 147  
 148          if ($this->has_separate_unit_field()) {
 149              return question_utils::arrays_same_at_key_missing_is_blank(
 150                  $prevresponse, $newresponse, 'unit');
 151          }
 152  
 153          return true;
 154      }
 155  
 156      public function get_correct_response() {
 157          $answer = $this->get_correct_answer();
 158          if (!$answer) {
 159              return array();
 160          }
 161  
 162          $response = array('answer' => str_replace('.', $this->ap->get_point(), $answer->answer));
 163  
 164          if ($this->has_separate_unit_field()) {
 165              $response['unit'] = $this->ap->get_default_unit();
 166          } else if ($this->unitdisplay == qtype_numerical::UNITINPUT) {
 167              $response['answer'] = $this->ap->add_unit($answer->answer);
 168          }
 169  
 170          return $response;
 171      }
 172  
 173      /**
 174       * Get an answer that contains the feedback and fraction that should be
 175       * awarded for this response.
 176       * @param number $value the numerical value of a response.
 177       * @param number $multiplier for the unit the student gave, if any. When no
 178       *      unit was given, or an unrecognised unit was given, $multiplier will be null.
 179       * @return question_answer the matching answer.
 180       */
 181      public function get_matching_answer($value, $multiplier) {
 182          if (is_null($value) || $value === '') {
 183              return null;
 184          }
 185  
 186          if (!is_null($multiplier)) {
 187              $scaledvalue = $value * $multiplier;
 188          } else {
 189              $scaledvalue = $value;
 190          }
 191          foreach ($this->answers as $answer) {
 192              if ($answer->within_tolerance($scaledvalue)) {
 193                  $answer->unitisright = !is_null($multiplier);
 194                  return $answer;
 195              } else if ($answer->within_tolerance($value)) {
 196                  $answer->unitisright = false;
 197                  return $answer;
 198              }
 199          }
 200  
 201          return null;
 202      }
 203  
 204      public function get_correct_answer() {
 205          foreach ($this->answers as $answer) {
 206              $state = question_state::graded_state_for_fraction($answer->fraction);
 207              if ($state == question_state::$gradedright) {
 208                  return $answer;
 209              }
 210          }
 211          return null;
 212      }
 213  
 214      /**
 215       * Adjust the fraction based on whether the unit was correct.
 216       * @param number $fraction
 217       * @param bool $unitisright
 218       * @return number
 219       */
 220      public function apply_unit_penalty($fraction, $unitisright) {
 221          if ($unitisright) {
 222              return $fraction;
 223          }
 224  
 225          if ($this->unitgradingtype == qtype_numerical::UNITGRADEDOUTOFMARK) {
 226              $fraction -= $this->unitpenalty * $fraction;
 227          } else if ($this->unitgradingtype == qtype_numerical::UNITGRADEDOUTOFMAX) {
 228              $fraction -= $this->unitpenalty;
 229          }
 230          return max($fraction, 0);
 231      }
 232  
 233      public function grade_response(array $response) {
 234          if ($this->has_separate_unit_field()) {
 235              $selectedunit = $response['unit'];
 236          } else {
 237              $selectedunit = null;
 238          }
 239          list($value, $unit, $multiplier) = $this->ap->apply_units(
 240                  $response['answer'], $selectedunit);
 241  
 242          $answer = $this->get_matching_answer($value, $multiplier);
 243          if (!$answer) {
 244              return array(0, question_state::$gradedwrong);
 245          }
 246  
 247          $fraction = $this->apply_unit_penalty($answer->fraction, $answer->unitisright);
 248          return array($fraction, question_state::graded_state_for_fraction($fraction));
 249      }
 250  
 251      public function classify_response(array $response) {
 252          if (!$this->is_gradable_response($response)) {
 253              return array($this->id => question_classified_response::no_response());
 254          }
 255  
 256          if ($this->has_separate_unit_field()) {
 257              $selectedunit = $response['unit'];
 258          } else {
 259              $selectedunit = null;
 260          }
 261          list($value, $unit, $multiplier) = $this->ap->apply_units($response['answer'], $selectedunit);
 262          $ans = $this->get_matching_answer($value, $multiplier);
 263  
 264          $resp = $response['answer'];
 265          if ($this->has_separate_unit_field()) {
 266              $resp = $this->ap->add_unit($resp, $unit);
 267          }
 268  
 269          if ($value === null) {
 270              // Invalid response shown as no response (but show actual response).
 271              return array($this->id => new question_classified_response(null, $resp, 0));
 272          } else if (!$ans) {
 273              // Does not match any answer.
 274              return array($this->id => new question_classified_response(0, $resp, 0));
 275          }
 276  
 277          return array($this->id => new question_classified_response($ans->id,
 278                  $resp,
 279                  $this->apply_unit_penalty($ans->fraction, $ans->unitisright)));
 280      }
 281  
 282      public function check_file_access($qa, $options, $component, $filearea, $args,
 283              $forcedownload) {
 284          if ($component == 'question' && $filearea == 'answerfeedback') {
 285              $currentanswer = $qa->get_last_qt_var('answer');
 286              if ($this->has_separate_unit_field()) {
 287                  $selectedunit = $qa->get_last_qt_var('unit');
 288              } else {
 289                  $selectedunit = null;
 290              }
 291              list($value, $unit, $multiplier) = $this->ap->apply_units(
 292                      $currentanswer, $selectedunit);
 293              $answer = $this->get_matching_answer($value, $multiplier);
 294              $answerid = reset($args); // Itemid is answer id.
 295              return $options->feedback && $answer && $answerid == $answer->id;
 296  
 297          } else if ($component == 'question' && $filearea == 'hint') {
 298              return $this->check_hint_file_access($qa, $options, $args);
 299  
 300          } else {
 301              return parent::check_file_access($qa, $options, $component, $filearea,
 302                      $args, $forcedownload);
 303          }
 304      }
 305  }
 306  
 307  
 308  /**
 309   * Subclass of {@link question_answer} with the extra information required by
 310   * the numerical question type.
 311   *
 312   * @copyright  2009 The Open University
 313   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 314   */
 315  class qtype_numerical_answer extends question_answer {
 316      /** @var float allowable margin of error. */
 317      public $tolerance;
 318      /** @var integer|string see {@link get_tolerance_interval()} for the meaning of this value. */
 319      public $tolerancetype = 2;
 320  
 321      public function __construct($id, $answer, $fraction, $feedback, $feedbackformat, $tolerance) {
 322          parent::__construct($id, $answer, $fraction, $feedback, $feedbackformat);
 323          $this->tolerance = abs($tolerance);
 324      }
 325  
 326      public function get_tolerance_interval() {
 327          if ($this->answer === '*') {
 328              throw new coding_exception('Cannot work out tolerance interval for answer *.');
 329          }
 330  
 331          // Smallest number that, when added to 1, is different from 1.
 332          $epsilon = pow(10, -1 * ini_get('precision'));
 333  
 334          // We need to add a tiny fraction depending on the set precision to make
 335          // the comparison work correctly, otherwise seemingly equal values can
 336          // yield false. See MDL-3225.
 337          $tolerance = abs($this->tolerance) + $epsilon;
 338  
 339          switch ($this->tolerancetype) {
 340              case 1: case 'relative':
 341                  $range = abs($this->answer) * $tolerance;
 342                  return array($this->answer - $range, $this->answer + $range);
 343  
 344              case 2: case 'nominal':
 345                  $tolerance = $this->tolerance + $epsilon * max(abs($this->tolerance), abs($this->answer), $epsilon);
 346                  return array($this->answer - $tolerance, $this->answer + $tolerance);
 347  
 348              case 3: case 'geometric':
 349                  $quotient = 1 + abs($tolerance);
 350                  return array($this->answer / $quotient, $this->answer * $quotient);
 351  
 352              default:
 353                  throw new coding_exception('Unknown tolerance type ' . $this->tolerancetype);
 354          }
 355      }
 356  
 357      public function within_tolerance($value) {
 358          if ($this->answer === '*') {
 359              return true;
 360          }
 361          list($min, $max) = $this->get_tolerance_interval();
 362          return $min <= $value && $value <= $max;
 363      }
 364  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1