MDL-20636 Merge branch 'master' into qe2_wip
[moodle.git] / question / type / numerical / question.php
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.
14 //
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 /**
20  * Numerical question definition class.
21  *
22  * @package qtype_numerical
23  * @copyright 2009 The Open University
24  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
30 /**
31  * Represents a numerical question.
32  *
33  * @copyright 2009 The Open University
34  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35  */
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 */
41     public $ap;
43     public function __construct() {
44         parent::__construct(new question_first_matching_answer_grading_strategy($this));
45     }
47     public function get_expected_data() {
48         return array('answer' => PARAM_RAW_TRIMMED);
49     }
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);
55         } else {
56             $step->set_qt_var('_separators',
57                     $this->ap->get_point() . '$' . $this->ap->get_separator());
58         }
59     }
61     public function summarise_response(array $response) {
62         if (isset($response['answer'])) {
63             return $response['answer'];
64         } else {
65             return null;
66         }
67     }
69     public function is_complete_response(array $response) {
70         return array_key_exists('answer', $response) &&
71                 ($response['answer'] || $response['answer'] === '0' || $response['answer'] === 0);
72     }
74     public function get_validation_error(array $response) {
75         if ($this->is_gradable_response($response)) {
76             return '';
77         }
78         return get_string('pleaseenterananswer', 'qtype_numerical');
79     }
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');
84     }
86     public function get_answers() {
87         return $this->answers;
88     }
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);
93     }
94 }
97 /**
98  * Subclass of {@link question_answer} with the extra information required by
99  * the numerical question type.
100  *
101  * @copyright 2009 The Open University
102  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
103  */
104 class qtype_numerical_answer extends question_answer {
105     /** @var float allowable margin of error. */
106     public $tolerance;
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);
113     }
115     public function get_tolerance_interval() {
116         if ($this->answer === '*') {
117             throw new Exception('Cannot work out tolerance interval for answer *.');
118         }
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);
139             default:
140                 throw new Exception('Unknown tolerance type ' . $this->tolerancetype);
141         }
142     }
144     public function within_tolerance($value) {
145         if ($this->answer === '*') {
146             return true;
147         }
148         list($min, $max) = $this->get_tolerance_interval();
149         return $min <= $value && $value <= $max;
150     }