MDL-20636 Finish implementing answer processing.
[moodle.git] / question / type / numerical / question.php
blob3a10e1622eac7f96fc57aa21852867606b752ec8
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 /**
19 * Numerical question definition class.
21 * @package qtype
22 * @subpackage numerical
23 * @copyright 2009 The Open University
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 defined('MOODLE_INTERNAL') || die();
30 require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
33 /**
34 * Represents a numerical question.
36 * @copyright 2009 The Open University
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 class qtype_numerical_question extends question_graded_automatically {
40 /** @var array of question_answer. */
41 public $answers = array();
43 /** @var int one of the constants UNITNONE, UNITDISPLAY, UNITSELECT or UNITINPUT. */
44 public $unitdisplay;
45 /** @var int one of the constants UNITGRADEDOUTOFMARK or UNITGRADEDOUTOFMAX. */
46 public $unitgradingtype;
47 /** @var number the penalty for a missing or unrecognised unit. */
48 public $unitpenalty;
50 /** @var qtype_numerical_answer_processor */
51 public $ap;
53 public function __construct() {
54 parent::__construct();
57 public function get_expected_data() {
58 return array('answer' => PARAM_RAW_TRIMMED);
61 public function start_attempt(question_attempt_step $step) {
62 $step->set_qt_var('_separators',
63 $this->ap->get_point() . '$' . $this->ap->get_separator());
66 public function apply_attempt_state(question_attempt_step $step) {
67 list($point, $separator) = explode('$', $step->get_qt_var('_separators'));
68 $this->ap->set_characters($point, $separator);
71 public function summarise_response(array $response) {
72 if (isset($response['answer'])) {
73 return $response['answer'];
74 } else {
75 return null;
79 public function is_complete_response(array $response) {
80 return array_key_exists('answer', $response) &&
81 ($response['answer'] || $response['answer'] === '0' || $response['answer'] === 0);
84 public function get_validation_error(array $response) {
85 if ($this->is_gradable_response($response)) {
86 return '';
88 return get_string('pleaseenterananswer', 'qtype_numerical');
91 public function is_same_response(array $prevresponse, array $newresponse) {
92 return question_utils::arrays_same_at_key_missing_is_blank(
93 $prevresponse, $newresponse, 'answer');
96 public function get_correct_response() {
97 $answer = $this->get_correct_answer();
98 if (!$answer) {
99 return array();
102 return array('answer' => $this->ap->add_unit($answer->answer));
106 * Get an answer that contains the feedback and fraction that should be
107 * awarded for this resonse.
108 * @param number $value the numerical value of a response.
109 * @return question_answer the matching answer.
111 public function get_matching_answer($value) {
112 foreach ($this->answers as $aid => $answer) {
113 if ($answer->within_tolerance($value)) {
114 $answer->id = $aid;
115 return $answer;
118 return null;
121 public function get_correct_answer() {
122 foreach ($this->answers as $answer) {
123 $state = question_state::graded_state_for_fraction($answer->fraction);
124 if ($state == question_state::$gradedright) {
125 return $answer;
128 return null;
131 protected function apply_unit_penalty($fraction, $unit) {
132 if (!empty($unit)) {
133 return $fraction;
136 if ($this->unitgradingtype == qtype_numerical::UNITGRADEDOUTOFMARK) {
137 $fraction -= $this->unitpenalty * $fraction;
138 } else if ($this->unitgradingtype == qtype_numerical::UNITGRADEDOUTOFMAX) {
139 $fraction -= $this->unitpenalty;
141 return max($fraction, 0);
144 public function grade_response(array $response) {
145 list($value, $unit) = $this->ap->apply_units($response['answer']);
146 $answer = $this->get_matching_answer($value);
147 if (!$answer) {
148 return array(0, question_state::$gradedwrong);
151 $fraction = $this->apply_unit_penalty($answer->fraction, $unit);
152 return array($fraction, question_state::graded_state_for_fraction($fraction));
155 public function classify_response(array $response) {
156 if (empty($response['answer'])) {
157 return array($this->id => question_classified_response::no_response());
160 list($value, $unit) = $this->ap->apply_units($response['answer']);
161 $ans = $this->get_matching_answer($value);
162 if (!$ans) {
163 return array($this->id => question_classified_response::no_response());
165 return array($this->id => new question_classified_response($ans->id,
166 $response['answer'],
167 $this->apply_unit_penalty($ans->fraction, $unit)));
173 * Subclass of {@link question_answer} with the extra information required by
174 * the numerical question type.
176 * @copyright 2009 The Open University
177 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
179 class qtype_numerical_answer extends question_answer {
180 /** @var float allowable margin of error. */
181 public $tolerance;
182 /** @var integer|string see {@link get_tolerance_interval()} for the meaning of this value. */
183 public $tolerancetype = 2;
185 public function __construct($id, $answer, $fraction, $feedback, $feedbackformat, $tolerance) {
186 parent::__construct($id, $answer, $fraction, $feedback, $feedbackformat);
187 $this->tolerance = abs($tolerance);
190 public function get_tolerance_interval() {
191 if ($this->answer === '*') {
192 throw new coding_exception('Cannot work out tolerance interval for answer *.');
195 // We need to add a tiny fraction depending on the set precision to make
196 // the comparison work correctly, otherwise seemingly equal values can
197 // yield false. See MDL-3225.
198 $tolerance = (float) $this->tolerance + pow(10, -1 * ini_get('precision'));
200 switch ($this->tolerancetype) {
201 case 1: case 'relative':
202 $range = abs($this->answer) * $tolerance;
203 return array($this->answer - $range, $this->answer + $range);
205 case 2: case 'nominal':
206 $tolerance = $this->tolerance + pow(10, -1 * ini_get('precision')) *
207 max(1, abs($this->answer));
208 return array($this->answer - $tolerance, $this->answer + $tolerance);
210 case 3: case 'geometric':
211 $quotient = 1 + abs($tolerance);
212 return array($this->answer / $quotient, $this->answer * $quotient);
214 default:
215 throw new coding_exception('Unknown tolerance type ' . $this->tolerancetype);
219 public function within_tolerance($value) {
220 if ($this->answer === '*') {
221 return true;
223 list($min, $max) = $this->get_tolerance_interval();
224 return $min <= $value && $value <= $max;