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/>.
20 * Numerical question definition class.
22 * @package qtype_numerical
23 * @copyright 2009 The Open University
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
31 * Represents a numerical question.
33 * @copyright 2009 The Open University
34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 class qtype_numerical_question extends question_graded_by_strategy
37 implements question_response_answer_comparer {
38 /** @var array of question_answer. */
39 public $answers = array();
40 /** @var qtype_numerical_answer_processor */
43 public function __construct() {
44 parent::__construct(new question_first_matching_answer_grading_strategy($this));
47 public function get_expected_data() {
48 return array('answer' => PARAM_RAW_TRIMMED);
51 public function init_first_step(question_attempt_step $step) {
52 if ($step->has_qt_var('_separators')) {
53 list($point, $separator) = explode('$', $step->get_qt_var('_separators'));
54 $this->ap->set_characters($point, $separator);
56 $step->set_qt_var('_separators',
57 $this->ap->get_point() . '$' . $this->ap->get_separator());
61 public function summarise_response(array $response) {
62 if (isset($response['answer'])) {
63 return $response['answer'];
69 public function is_complete_response(array $response) {
70 return array_key_exists('answer', $response) &&
71 ($response['answer'] || $response['answer'] === '0' || $response['answer'] === 0);
74 public function get_validation_error(array $response) {
75 if ($this->is_gradable_response($response)) {
78 return get_string('pleaseenterananswer', 'qtype_numerical');
81 public function is_same_response(array $prevresponse, array $newresponse) {
82 return question_utils::arrays_same_at_key_missing_is_blank(
83 $prevresponse, $newresponse, 'answer');
86 public function get_answers() {
87 return $this->answers;
90 public function compare_response_with_answer(array $response, question_answer $answer) {
91 list($value, $unit) = $this->ap->apply_units($response['answer']);
92 return $answer->within_tolerance($value);
98 * Subclass of {@link question_answer} with the extra information required by
99 * the numerical question type.
101 * @copyright 2009 The Open University
102 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
104 class qtype_numerical_answer extends question_answer {
105 /** @var float allowable margin of error. */
107 /** @var integer|string see {@link get_tolerance_interval()} for the meaning of this value. */
108 public $tolerancetype = 2;
110 public function __construct($id, $answer, $fraction, $feedback, $feedbackformat, $tolerance) {
111 parent::__construct($id, $answer, $fraction, $feedback, $feedbackformat);
112 $this->tolerance = abs($tolerance);
115 public function get_tolerance_interval() {
116 if ($this->answer === '*') {
117 throw new Exception('Cannot work out tolerance interval for answer *.');
120 // We need to add a tiny fraction depending on the set precision to make
121 // the comparison work correctly, otherwise seemingly equal values can
122 // yield false. See MDL-3225.
123 $tolerance = (float) $this->tolerance + pow(10, -1 * ini_get('precision'));
125 switch ($this->tolerancetype) {
126 case 1: case 'relative':
127 $range = abs($this->answer) * $tolerance;
128 return array($this->answer - $range, $this->answer + $range);
130 case 2: case 'nominal':
131 $tolerance = $this->tolerance + pow(10, -1 * ini_get('precision')) *
132 max(1, abs($this->answer));
133 return array($this->answer - $tolerance, $this->answer + $tolerance);
135 case 3: case 'geometric':
136 $quotient = 1 + abs($tolerance);
137 return array($this->answer / $quotient, $this->answer * $quotient);
140 throw new Exception('Unknown tolerance type ' . $this->tolerancetype);
144 public function within_tolerance($value) {
145 if ($this->answer === '*') {
148 list($min, $max) = $this->get_tolerance_interval();
149 return $min <= $value && $value <= $max;