3 // This file is part of Moodle - http://moodle.org/
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.
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/>.
19 * Numerical question definition class.
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');
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. */
45 /** @var int one of the constants UNITGRADEDOUTOFMARK or UNITGRADEDOUTOFMAX. */
46 public $unitgradingtype;
47 /** @var number the penalty for a missing or unrecognised unit. */
50 /** @var qtype_numerical_answer_processor */
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'];
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)) {
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();
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)) {
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) {
131 protected function apply_unit_penalty($fraction, $unit) {
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);
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);
163 return array($this->id
=> question_classified_response
::no_response());
165 return array($this->id
=> new question_classified_response($ans->id
,
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. */
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);
215 throw new coding_exception('Unknown tolerance type ' . $this->tolerancetype
);
219 public function within_tolerance($value) {
220 if ($this->answer
=== '*') {
223 list($min, $max) = $this->get_tolerance_interval();
224 return $min <= $value && $value <= $max;