[ Index ]

PHP Cross Reference of Unnamed Project

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1