1 |
efrain |
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 |
/**
|
|
|
19 |
* Question type class for the numerical question type.
|
|
|
20 |
*
|
|
|
21 |
* @package qtype
|
|
|
22 |
* @subpackage numerical
|
|
|
23 |
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
|
|
|
24 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
25 |
*/
|
|
|
26 |
|
|
|
27 |
|
|
|
28 |
defined('MOODLE_INTERNAL') || die();
|
|
|
29 |
|
|
|
30 |
require_once($CFG->libdir . '/questionlib.php');
|
|
|
31 |
require_once($CFG->dirroot . '/question/type/numerical/question.php');
|
|
|
32 |
|
|
|
33 |
|
|
|
34 |
/**
|
|
|
35 |
* The numerical question type class.
|
|
|
36 |
*
|
|
|
37 |
* This class contains some special features in order to make the
|
|
|
38 |
* question type embeddable within a multianswer (cloze) question
|
|
|
39 |
*
|
|
|
40 |
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
|
|
|
41 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
42 |
*/
|
|
|
43 |
class qtype_numerical extends question_type {
|
|
|
44 |
const UNITINPUT = 0;
|
|
|
45 |
const UNITRADIO = 1;
|
|
|
46 |
const UNITSELECT = 2;
|
|
|
47 |
|
|
|
48 |
const UNITNONE = 3;
|
|
|
49 |
const UNITGRADED = 1;
|
|
|
50 |
const UNITOPTIONAL = 0;
|
|
|
51 |
|
|
|
52 |
const UNITGRADEDOUTOFMARK = 1;
|
|
|
53 |
const UNITGRADEDOUTOFMAX = 2;
|
|
|
54 |
|
|
|
55 |
/**
|
|
|
56 |
* Validate that a string is a number formatted correctly for the current locale.
|
|
|
57 |
* @param string $x a string
|
|
|
58 |
* @return bool whether $x is a number that the numerical question type can interpret.
|
|
|
59 |
*/
|
|
|
60 |
public static function is_valid_number(string $x): bool {
|
|
|
61 |
$ap = new qtype_numerical_answer_processor(array());
|
|
|
62 |
list($value, $unit) = $ap->apply_units($x);
|
|
|
63 |
return !is_null($value) && !$unit;
|
|
|
64 |
}
|
|
|
65 |
|
|
|
66 |
public function get_question_options($question) {
|
|
|
67 |
global $CFG, $DB, $OUTPUT;
|
|
|
68 |
parent::get_question_options($question);
|
|
|
69 |
// Get the question answers and their respective tolerances
|
|
|
70 |
// Note: question_numerical is an extension of the answer table rather than
|
|
|
71 |
// the question table as is usually the case for qtype
|
|
|
72 |
// specific tables.
|
|
|
73 |
if (!$question->options->answers = $DB->get_records_sql(
|
|
|
74 |
"SELECT a.*, n.tolerance " .
|
|
|
75 |
"FROM {question_answers} a, " .
|
|
|
76 |
" {question_numerical} n " .
|
|
|
77 |
"WHERE a.question = ? " .
|
|
|
78 |
" AND a.id = n.answer " .
|
|
|
79 |
"ORDER BY a.id ASC", array($question->id))) {
|
|
|
80 |
echo $OUTPUT->notification('Error: Missing question answer for numerical question ' .
|
|
|
81 |
$question->id . '!');
|
|
|
82 |
return false;
|
|
|
83 |
}
|
|
|
84 |
|
|
|
85 |
$question->hints = $DB->get_records('question_hints',
|
|
|
86 |
array('questionid' => $question->id), 'id ASC');
|
|
|
87 |
|
|
|
88 |
$this->get_numerical_units($question);
|
|
|
89 |
// Get_numerical_options() need to know if there are units
|
|
|
90 |
// to set correctly default values.
|
|
|
91 |
$this->get_numerical_options($question);
|
|
|
92 |
|
|
|
93 |
// If units are defined we strip off the default unit from the answer, if
|
|
|
94 |
// it is present. (Required for compatibility with the old code and DB).
|
|
|
95 |
if ($defaultunit = $this->get_default_numerical_unit($question)) {
|
|
|
96 |
foreach ($question->options->answers as $key => $val) {
|
|
|
97 |
$answer = trim($val->answer);
|
|
|
98 |
$length = strlen($defaultunit->unit);
|
|
|
99 |
if ($length && substr($answer, -$length) == $defaultunit->unit) {
|
|
|
100 |
$question->options->answers[$key]->answer =
|
|
|
101 |
substr($answer, 0, strlen($answer)-$length);
|
|
|
102 |
}
|
|
|
103 |
}
|
|
|
104 |
}
|
|
|
105 |
|
|
|
106 |
return true;
|
|
|
107 |
}
|
|
|
108 |
|
|
|
109 |
public function get_numerical_units(&$question) {
|
|
|
110 |
global $DB;
|
|
|
111 |
|
|
|
112 |
if ($units = $DB->get_records('question_numerical_units',
|
|
|
113 |
array('question' => $question->id), 'id ASC')) {
|
|
|
114 |
$units = array_values($units);
|
|
|
115 |
} else {
|
|
|
116 |
$units = array();
|
|
|
117 |
}
|
|
|
118 |
foreach ($units as $key => $unit) {
|
|
|
119 |
$units[$key]->multiplier = clean_param($unit->multiplier, PARAM_FLOAT);
|
|
|
120 |
}
|
|
|
121 |
$question->options->units = $units;
|
|
|
122 |
return true;
|
|
|
123 |
}
|
|
|
124 |
|
|
|
125 |
public function get_default_numerical_unit($question) {
|
|
|
126 |
if (isset($question->options->units[0])) {
|
|
|
127 |
foreach ($question->options->units as $unit) {
|
|
|
128 |
if (abs($unit->multiplier - 1.0) < '1.0e-' . ini_get('precision')) {
|
|
|
129 |
return $unit;
|
|
|
130 |
}
|
|
|
131 |
}
|
|
|
132 |
}
|
|
|
133 |
return false;
|
|
|
134 |
}
|
|
|
135 |
|
|
|
136 |
public function get_numerical_options($question) {
|
|
|
137 |
global $DB;
|
|
|
138 |
if (!$options = $DB->get_record('question_numerical_options',
|
|
|
139 |
array('question' => $question->id))) {
|
|
|
140 |
// Old question, set defaults.
|
|
|
141 |
$question->options->unitgradingtype = 0;
|
|
|
142 |
$question->options->unitpenalty = 0.1;
|
|
|
143 |
if ($defaultunit = $this->get_default_numerical_unit($question)) {
|
|
|
144 |
$question->options->showunits = self::UNITINPUT;
|
|
|
145 |
} else {
|
|
|
146 |
$question->options->showunits = self::UNITNONE;
|
|
|
147 |
}
|
|
|
148 |
$question->options->unitsleft = 0;
|
|
|
149 |
|
|
|
150 |
} else {
|
|
|
151 |
$question->options->unitgradingtype = $options->unitgradingtype;
|
|
|
152 |
$question->options->unitpenalty = $options->unitpenalty;
|
|
|
153 |
$question->options->showunits = $options->showunits;
|
|
|
154 |
$question->options->unitsleft = $options->unitsleft;
|
|
|
155 |
}
|
|
|
156 |
|
|
|
157 |
return true;
|
|
|
158 |
}
|
|
|
159 |
|
|
|
160 |
public function save_defaults_for_new_questions(stdClass $fromform): void {
|
|
|
161 |
parent::save_defaults_for_new_questions($fromform);
|
|
|
162 |
$this->set_default_value('unitrole', $fromform->unitrole);
|
|
|
163 |
$this->set_default_value('unitpenalty', $fromform->unitpenalty);
|
|
|
164 |
$this->set_default_value('unitgradingtypes', $fromform->unitgradingtypes);
|
|
|
165 |
$this->set_default_value('multichoicedisplay', $fromform->multichoicedisplay);
|
|
|
166 |
$this->set_default_value('unitsleft', $fromform->unitsleft);
|
|
|
167 |
}
|
|
|
168 |
|
|
|
169 |
/**
|
|
|
170 |
* Save the units and the answers associated with this question.
|
|
|
171 |
*/
|
|
|
172 |
public function save_question_options($question) {
|
|
|
173 |
global $DB;
|
|
|
174 |
$context = $question->context;
|
|
|
175 |
|
|
|
176 |
// Get old versions of the objects.
|
|
|
177 |
$oldanswers = $DB->get_records('question_answers',
|
|
|
178 |
array('question' => $question->id), 'id ASC');
|
|
|
179 |
$oldoptions = $DB->get_records('question_numerical',
|
|
|
180 |
array('question' => $question->id), 'answer ASC');
|
|
|
181 |
|
|
|
182 |
// Save the units.
|
|
|
183 |
$result = $this->save_units($question);
|
|
|
184 |
if (isset($result->error)) {
|
|
|
185 |
return $result;
|
|
|
186 |
} else {
|
|
|
187 |
$units = $result->units;
|
|
|
188 |
}
|
|
|
189 |
|
|
|
190 |
// Insert all the new answers.
|
|
|
191 |
foreach ($question->answer as $key => $answerdata) {
|
|
|
192 |
// Check for, and ingore, completely blank answer from the form.
|
|
|
193 |
if (trim($answerdata) == '' && $question->fraction[$key] == 0 &&
|
|
|
194 |
html_is_blank($question->feedback[$key]['text'])) {
|
|
|
195 |
continue;
|
|
|
196 |
}
|
|
|
197 |
|
|
|
198 |
// Update an existing answer if possible.
|
|
|
199 |
$answer = array_shift($oldanswers);
|
|
|
200 |
if (!$answer) {
|
|
|
201 |
$answer = new stdClass();
|
|
|
202 |
$answer->question = $question->id;
|
|
|
203 |
$answer->answer = '';
|
|
|
204 |
$answer->feedback = '';
|
|
|
205 |
$answer->id = $DB->insert_record('question_answers', $answer);
|
|
|
206 |
}
|
|
|
207 |
|
|
|
208 |
if (trim($answerdata) === '*') {
|
|
|
209 |
$answer->answer = '*';
|
|
|
210 |
} else {
|
|
|
211 |
$answer->answer = $this->apply_unit($answerdata, $units,
|
|
|
212 |
!empty($question->unitsleft));
|
|
|
213 |
if ($answer->answer === false) {
|
|
|
214 |
$result->notice = get_string('invalidnumericanswer', 'qtype_numerical');
|
|
|
215 |
}
|
|
|
216 |
}
|
|
|
217 |
$answer->fraction = $question->fraction[$key];
|
|
|
218 |
$answer->feedback = $this->import_or_save_files($question->feedback[$key],
|
|
|
219 |
$context, 'question', 'answerfeedback', $answer->id);
|
|
|
220 |
$answer->feedbackformat = $question->feedback[$key]['format'];
|
|
|
221 |
$DB->update_record('question_answers', $answer);
|
|
|
222 |
|
|
|
223 |
// Set up the options object.
|
|
|
224 |
if (!$options = array_shift($oldoptions)) {
|
|
|
225 |
$options = new stdClass();
|
|
|
226 |
}
|
|
|
227 |
$options->question = $question->id;
|
|
|
228 |
$options->answer = $answer->id;
|
|
|
229 |
if (trim($question->tolerance[$key]) == '') {
|
|
|
230 |
$options->tolerance = '';
|
|
|
231 |
} else {
|
|
|
232 |
$options->tolerance = $this->apply_unit($question->tolerance[$key],
|
|
|
233 |
$units, !empty($question->unitsleft));
|
|
|
234 |
if ($options->tolerance === false) {
|
|
|
235 |
$result->notice = get_string('invalidnumerictolerance', 'qtype_numerical');
|
|
|
236 |
}
|
|
|
237 |
$options->tolerance = (string)$options->tolerance;
|
|
|
238 |
}
|
|
|
239 |
if (isset($options->id)) {
|
|
|
240 |
$DB->update_record('question_numerical', $options);
|
|
|
241 |
} else {
|
|
|
242 |
$DB->insert_record('question_numerical', $options);
|
|
|
243 |
}
|
|
|
244 |
}
|
|
|
245 |
|
|
|
246 |
// Delete any left over old answer records.
|
|
|
247 |
$fs = get_file_storage();
|
|
|
248 |
foreach ($oldanswers as $oldanswer) {
|
|
|
249 |
$fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id);
|
|
|
250 |
$DB->delete_records('question_answers', array('id' => $oldanswer->id));
|
|
|
251 |
}
|
|
|
252 |
foreach ($oldoptions as $oldoption) {
|
|
|
253 |
$DB->delete_records('question_numerical', array('id' => $oldoption->id));
|
|
|
254 |
}
|
|
|
255 |
|
|
|
256 |
$result = $this->save_unit_options($question);
|
|
|
257 |
if (!empty($result->error) || !empty($result->notice)) {
|
|
|
258 |
return $result;
|
|
|
259 |
}
|
|
|
260 |
|
|
|
261 |
$this->save_hints($question);
|
|
|
262 |
|
|
|
263 |
return true;
|
|
|
264 |
}
|
|
|
265 |
|
|
|
266 |
/**
|
|
|
267 |
* The numerical options control the display and the grading of the unit
|
|
|
268 |
* part of the numerical question and related types (calculateds)
|
|
|
269 |
* Questions previous to 2.0 do not have this table as multianswer questions
|
|
|
270 |
* in all versions including 2.0. The default values are set to give the same grade
|
|
|
271 |
* as old question.
|
|
|
272 |
*
|
|
|
273 |
*/
|
|
|
274 |
public function save_unit_options($question) {
|
|
|
275 |
global $DB;
|
|
|
276 |
$result = new stdClass();
|
|
|
277 |
|
|
|
278 |
$update = true;
|
|
|
279 |
$options = $DB->get_record('question_numerical_options',
|
|
|
280 |
array('question' => $question->id));
|
|
|
281 |
if (!$options) {
|
|
|
282 |
$options = new stdClass();
|
|
|
283 |
$options->question = $question->id;
|
|
|
284 |
$options->id = $DB->insert_record('question_numerical_options', $options);
|
|
|
285 |
}
|
|
|
286 |
|
|
|
287 |
if (isset($question->unitpenalty)) {
|
|
|
288 |
$options->unitpenalty = $question->unitpenalty;
|
|
|
289 |
} else {
|
|
|
290 |
// Either an old question or a close question type.
|
|
|
291 |
$options->unitpenalty = 1;
|
|
|
292 |
}
|
|
|
293 |
|
|
|
294 |
$options->unitgradingtype = 0;
|
|
|
295 |
if (isset($question->unitrole)) {
|
|
|
296 |
// Saving the editing form.
|
|
|
297 |
$options->showunits = $question->unitrole;
|
|
|
298 |
if ($question->unitrole == self::UNITGRADED) {
|
|
|
299 |
$options->unitgradingtype = $question->unitgradingtypes;
|
|
|
300 |
$options->showunits = $question->multichoicedisplay;
|
|
|
301 |
}
|
|
|
302 |
|
|
|
303 |
} else if (isset($question->showunits)) {
|
|
|
304 |
// Updated import, e.g. Moodle XML.
|
|
|
305 |
$options->showunits = $question->showunits;
|
|
|
306 |
if (isset($question->unitgradingtype)) {
|
|
|
307 |
$options->unitgradingtype = $question->unitgradingtype;
|
|
|
308 |
}
|
|
|
309 |
} else {
|
|
|
310 |
// Legacy import.
|
|
|
311 |
if ($defaultunit = $this->get_default_numerical_unit($question)) {
|
|
|
312 |
$options->showunits = self::UNITINPUT;
|
|
|
313 |
} else {
|
|
|
314 |
$options->showunits = self::UNITNONE;
|
|
|
315 |
}
|
|
|
316 |
}
|
|
|
317 |
|
|
|
318 |
$options->unitsleft = !empty($question->unitsleft);
|
|
|
319 |
|
|
|
320 |
$DB->update_record('question_numerical_options', $options);
|
|
|
321 |
|
|
|
322 |
// Report any problems.
|
|
|
323 |
if (!empty($result->notice)) {
|
|
|
324 |
return $result;
|
|
|
325 |
}
|
|
|
326 |
|
|
|
327 |
return true;
|
|
|
328 |
}
|
|
|
329 |
|
|
|
330 |
public function save_units($question) {
|
|
|
331 |
global $DB;
|
|
|
332 |
$result = new stdClass();
|
|
|
333 |
|
|
|
334 |
// Delete the units previously saved for this question.
|
|
|
335 |
$DB->delete_records('question_numerical_units', array('question' => $question->id));
|
|
|
336 |
|
|
|
337 |
// Nothing to do.
|
|
|
338 |
if (!isset($question->multiplier)) {
|
|
|
339 |
$result->units = array();
|
|
|
340 |
return $result;
|
|
|
341 |
}
|
|
|
342 |
|
|
|
343 |
// Save the new units.
|
|
|
344 |
$units = array();
|
|
|
345 |
$unitalreadyinsert = array();
|
|
|
346 |
foreach ($question->multiplier as $i => $multiplier) {
|
|
|
347 |
// Discard any unit which doesn't specify the unit or the multiplier.
|
|
|
348 |
if (!empty($question->multiplier[$i]) && !empty($question->unit[$i]) &&
|
|
|
349 |
!array_key_exists($question->unit[$i], $unitalreadyinsert)) {
|
|
|
350 |
$unitalreadyinsert[$question->unit[$i]] = 1;
|
|
|
351 |
$units[$i] = new stdClass();
|
|
|
352 |
$units[$i]->question = $question->id;
|
|
|
353 |
$units[$i]->multiplier = $this->apply_unit($question->multiplier[$i],
|
|
|
354 |
array(), false);
|
|
|
355 |
$units[$i]->unit = $question->unit[$i];
|
|
|
356 |
$DB->insert_record('question_numerical_units', $units[$i]);
|
|
|
357 |
}
|
|
|
358 |
}
|
|
|
359 |
unset($question->multiplier, $question->unit);
|
|
|
360 |
|
|
|
361 |
$result->units = &$units;
|
|
|
362 |
return $result;
|
|
|
363 |
}
|
|
|
364 |
|
|
|
365 |
protected function initialise_question_instance(question_definition $question, $questiondata) {
|
|
|
366 |
parent::initialise_question_instance($question, $questiondata);
|
|
|
367 |
$this->initialise_numerical_answers($question, $questiondata);
|
|
|
368 |
$question->unitdisplay = $questiondata->options->showunits;
|
|
|
369 |
$question->unitgradingtype = $questiondata->options->unitgradingtype;
|
|
|
370 |
$question->unitpenalty = $questiondata->options->unitpenalty;
|
|
|
371 |
$question->unitsleft = $questiondata->options->unitsleft;
|
|
|
372 |
$question->ap = $this->make_answer_processor($questiondata->options->units,
|
|
|
373 |
$questiondata->options->unitsleft);
|
|
|
374 |
}
|
|
|
375 |
|
|
|
376 |
public function initialise_numerical_answers(question_definition $question, $questiondata) {
|
|
|
377 |
$question->answers = array();
|
|
|
378 |
if (empty($questiondata->options->answers)) {
|
|
|
379 |
return;
|
|
|
380 |
}
|
|
|
381 |
foreach ($questiondata->options->answers as $a) {
|
|
|
382 |
$question->answers[$a->id] = new qtype_numerical_answer($a->id, $a->answer,
|
|
|
383 |
$a->fraction, $a->feedback, $a->feedbackformat, $a->tolerance);
|
|
|
384 |
}
|
|
|
385 |
}
|
|
|
386 |
|
|
|
387 |
public function make_answer_processor($units, $unitsleft) {
|
|
|
388 |
if (empty($units)) {
|
|
|
389 |
return new qtype_numerical_answer_processor(array());
|
|
|
390 |
}
|
|
|
391 |
|
|
|
392 |
$cleanedunits = array();
|
|
|
393 |
foreach ($units as $unit) {
|
|
|
394 |
$cleanedunits[$unit->unit] = $unit->multiplier;
|
|
|
395 |
}
|
|
|
396 |
|
|
|
397 |
return new qtype_numerical_answer_processor($cleanedunits, $unitsleft);
|
|
|
398 |
}
|
|
|
399 |
|
|
|
400 |
public function delete_question($questionid, $contextid) {
|
|
|
401 |
global $DB;
|
|
|
402 |
$DB->delete_records('question_numerical', array('question' => $questionid));
|
|
|
403 |
$DB->delete_records('question_numerical_options', array('question' => $questionid));
|
|
|
404 |
$DB->delete_records('question_numerical_units', array('question' => $questionid));
|
|
|
405 |
|
|
|
406 |
parent::delete_question($questionid, $contextid);
|
|
|
407 |
}
|
|
|
408 |
|
|
|
409 |
public function get_random_guess_score($questiondata) {
|
|
|
410 |
foreach ($questiondata->options->answers as $aid => $answer) {
|
|
|
411 |
if ('*' == trim($answer->answer)) {
|
|
|
412 |
return max($answer->fraction - $questiondata->options->unitpenalty, 0);
|
|
|
413 |
}
|
|
|
414 |
}
|
|
|
415 |
return 0;
|
|
|
416 |
}
|
|
|
417 |
|
|
|
418 |
/**
|
|
|
419 |
* Add a unit to a response for display.
|
|
|
420 |
* @param object $questiondata the data defining the quetsion.
|
|
|
421 |
* @param string $answer a response.
|
|
|
422 |
* @param object $unit a unit. If null, {@link get_default_numerical_unit()}
|
|
|
423 |
* is used.
|
|
|
424 |
*/
|
|
|
425 |
public function add_unit($questiondata, $answer, $unit = null) {
|
|
|
426 |
if (is_null($unit)) {
|
|
|
427 |
$unit = $this->get_default_numerical_unit($questiondata);
|
|
|
428 |
}
|
|
|
429 |
|
|
|
430 |
if (!$unit) {
|
|
|
431 |
return $answer;
|
|
|
432 |
}
|
|
|
433 |
|
|
|
434 |
if (!empty($questiondata->options->unitsleft)) {
|
|
|
435 |
return $unit->unit . ' ' . $answer;
|
|
|
436 |
} else {
|
|
|
437 |
return $answer . ' ' . $unit->unit;
|
|
|
438 |
}
|
|
|
439 |
}
|
|
|
440 |
|
|
|
441 |
public function get_possible_responses($questiondata) {
|
|
|
442 |
$responses = array();
|
|
|
443 |
|
|
|
444 |
$unit = $this->get_default_numerical_unit($questiondata);
|
|
|
445 |
|
|
|
446 |
$starfound = false;
|
|
|
447 |
foreach ($questiondata->options->answers as $aid => $answer) {
|
|
|
448 |
$responseclass = $answer->answer;
|
|
|
449 |
|
|
|
450 |
if ($responseclass === '*') {
|
|
|
451 |
$starfound = true;
|
|
|
452 |
} else {
|
|
|
453 |
$responseclass = $this->add_unit($questiondata, $responseclass, $unit);
|
|
|
454 |
|
|
|
455 |
$ans = new qtype_numerical_answer($answer->id, $answer->answer, $answer->fraction,
|
|
|
456 |
$answer->feedback, $answer->feedbackformat, $answer->tolerance);
|
|
|
457 |
list($min, $max) = $ans->get_tolerance_interval();
|
|
|
458 |
$responseclass .= " ({$min}..{$max})";
|
|
|
459 |
}
|
|
|
460 |
|
|
|
461 |
$responses[$aid] = new question_possible_response($responseclass,
|
|
|
462 |
$answer->fraction);
|
|
|
463 |
}
|
|
|
464 |
|
|
|
465 |
if (!$starfound) {
|
|
|
466 |
$responses[0] = new question_possible_response(
|
|
|
467 |
get_string('didnotmatchanyanswer', 'question'), 0);
|
|
|
468 |
}
|
|
|
469 |
|
|
|
470 |
$responses[null] = question_possible_response::no_response();
|
|
|
471 |
|
|
|
472 |
return array($questiondata->id => $responses);
|
|
|
473 |
}
|
|
|
474 |
|
|
|
475 |
/**
|
|
|
476 |
* Checks if the $rawresponse has a unit and applys it if appropriate.
|
|
|
477 |
*
|
|
|
478 |
* @param string $rawresponse The response string to be converted to a float.
|
|
|
479 |
* @param array $units An array with the defined units, where the
|
|
|
480 |
* unit is the key and the multiplier the value.
|
|
|
481 |
* @return float The rawresponse with the unit taken into
|
|
|
482 |
* account as a float.
|
|
|
483 |
*/
|
|
|
484 |
public function apply_unit($rawresponse, $units, $unitsleft) {
|
|
|
485 |
$ap = $this->make_answer_processor($units, $unitsleft);
|
|
|
486 |
list($value, $unit, $multiplier) = $ap->apply_units($rawresponse);
|
|
|
487 |
if (!is_null($multiplier)) {
|
|
|
488 |
$value *= $multiplier;
|
|
|
489 |
}
|
|
|
490 |
return $value;
|
|
|
491 |
}
|
|
|
492 |
|
|
|
493 |
public function move_files($questionid, $oldcontextid, $newcontextid) {
|
|
|
494 |
$fs = get_file_storage();
|
|
|
495 |
|
|
|
496 |
parent::move_files($questionid, $oldcontextid, $newcontextid);
|
|
|
497 |
$this->move_files_in_answers($questionid, $oldcontextid, $newcontextid);
|
|
|
498 |
$this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
|
|
|
499 |
}
|
|
|
500 |
|
|
|
501 |
protected function delete_files($questionid, $contextid) {
|
|
|
502 |
$fs = get_file_storage();
|
|
|
503 |
|
|
|
504 |
parent::delete_files($questionid, $contextid);
|
|
|
505 |
$this->delete_files_in_answers($questionid, $contextid);
|
|
|
506 |
$this->delete_files_in_hints($questionid, $contextid);
|
|
|
507 |
}
|
|
|
508 |
}
|
|
|
509 |
|
|
|
510 |
|
|
|
511 |
/**
|
|
|
512 |
* This class processes numbers with units.
|
|
|
513 |
*
|
|
|
514 |
* @copyright 2010 The Open University
|
|
|
515 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
516 |
*/
|
|
|
517 |
class qtype_numerical_answer_processor {
|
|
|
518 |
/** @var array unit name => multiplier. */
|
|
|
519 |
protected $units;
|
|
|
520 |
/** @var string character used as decimal point. */
|
|
|
521 |
protected $decsep;
|
|
|
522 |
/** @var string character used as thousands separator. */
|
|
|
523 |
protected $thousandssep;
|
|
|
524 |
/** @var boolean whether the units come before or after the number. */
|
|
|
525 |
protected $unitsbefore;
|
|
|
526 |
|
|
|
527 |
protected $regex = null;
|
|
|
528 |
|
|
|
529 |
public function __construct($units, $unitsbefore = false, $decsep = null,
|
|
|
530 |
$thousandssep = null) {
|
|
|
531 |
if (is_null($decsep)) {
|
|
|
532 |
$decsep = get_string('decsep', 'langconfig');
|
|
|
533 |
}
|
|
|
534 |
$this->decsep = $decsep;
|
|
|
535 |
|
|
|
536 |
if (is_null($thousandssep)) {
|
|
|
537 |
$thousandssep = get_string('thousandssep', 'langconfig');
|
|
|
538 |
}
|
|
|
539 |
$this->thousandssep = $thousandssep;
|
|
|
540 |
|
|
|
541 |
$this->units = $units;
|
|
|
542 |
$this->unitsbefore = $unitsbefore;
|
|
|
543 |
}
|
|
|
544 |
|
|
|
545 |
/**
|
|
|
546 |
* Set the decimal point and thousands separator character that should be used.
|
|
|
547 |
* @param string $decsep
|
|
|
548 |
* @param string $thousandssep
|
|
|
549 |
*/
|
|
|
550 |
public function set_characters($decsep, $thousandssep) {
|
|
|
551 |
$this->decsep = $decsep;
|
|
|
552 |
$this->thousandssep = $thousandssep;
|
|
|
553 |
$this->regex = null;
|
|
|
554 |
}
|
|
|
555 |
|
|
|
556 |
/** @return string the decimal point character used. */
|
|
|
557 |
public function get_point() {
|
|
|
558 |
return $this->decsep;
|
|
|
559 |
}
|
|
|
560 |
|
|
|
561 |
/** @return string the thousands separator character used. */
|
|
|
562 |
public function get_separator() {
|
|
|
563 |
return $this->thousandssep;
|
|
|
564 |
}
|
|
|
565 |
|
|
|
566 |
/**
|
|
|
567 |
* @return bool If the student's response contains a '.' or a ',' that
|
|
|
568 |
* matches the thousands separator in the current locale. In this case, the
|
|
|
569 |
* parsing in apply_unit can give a result that the student did not expect.
|
|
|
570 |
*/
|
|
|
571 |
public function contains_thousands_seaparator($value) {
|
|
|
572 |
if (!in_array($this->thousandssep, array('.', ','))) {
|
|
|
573 |
return false;
|
|
|
574 |
}
|
|
|
575 |
|
|
|
576 |
return strpos($value, $this->thousandssep) !== false;
|
|
|
577 |
}
|
|
|
578 |
|
|
|
579 |
/**
|
|
|
580 |
* Create the regular expression that {@link parse_response()} requires.
|
|
|
581 |
* @return string
|
|
|
582 |
*/
|
|
|
583 |
protected function build_regex() {
|
|
|
584 |
if (!is_null($this->regex)) {
|
|
|
585 |
return $this->regex;
|
|
|
586 |
}
|
|
|
587 |
|
|
|
588 |
$decsep = preg_quote($this->decsep, '/');
|
|
|
589 |
$thousandssep = preg_quote($this->thousandssep, '/');
|
|
|
590 |
$beforepointre = '([+-]?[' . $thousandssep . '\d]*)';
|
|
|
591 |
$decimalsre = $decsep . '(\d*)';
|
|
|
592 |
$exponentre = '(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)';
|
|
|
593 |
|
|
|
594 |
$numberbit = "{$beforepointre}(?:{$decimalsre})?(?:{$exponentre})?";
|
|
|
595 |
|
|
|
596 |
if ($this->unitsbefore) {
|
|
|
597 |
$this->regex = "/{$numberbit}$/";
|
|
|
598 |
} else {
|
|
|
599 |
$this->regex = "/^{$numberbit}/";
|
|
|
600 |
}
|
|
|
601 |
return $this->regex;
|
|
|
602 |
}
|
|
|
603 |
|
|
|
604 |
/**
|
|
|
605 |
* This method can be used for more locale-strict parsing of repsonses. At the
|
|
|
606 |
* moment we don't use it, and instead use the more lax parsing in apply_units.
|
|
|
607 |
* This is just a note that this funciton was used in the past, so if you are
|
|
|
608 |
* intersted, look through version control history.
|
|
|
609 |
*
|
|
|
610 |
* Take a string which is a number with or without a decimal point and exponent,
|
|
|
611 |
* and possibly followed by one of the units, and split it into bits.
|
|
|
612 |
* @param string $response a value, optionally with a unit.
|
|
|
613 |
* @return array four strings (some of which may be blank) the digits before
|
|
|
614 |
* and after the decimal point, the exponent, and the unit. All four will be
|
|
|
615 |
* null if the response cannot be parsed.
|
|
|
616 |
*/
|
|
|
617 |
protected function parse_response($response) {
|
|
|
618 |
if (!preg_match($this->build_regex(), $response, $matches)) {
|
|
|
619 |
return array(null, null, null, null);
|
|
|
620 |
}
|
|
|
621 |
|
|
|
622 |
$matches += array('', '', '', ''); // Fill in any missing matches.
|
|
|
623 |
list($matchedpart, $beforepoint, $decimals, $exponent) = $matches;
|
|
|
624 |
|
|
|
625 |
// Strip out thousands separators.
|
|
|
626 |
$beforepoint = str_replace($this->thousandssep, '', $beforepoint);
|
|
|
627 |
|
|
|
628 |
// Must be either something before, or something after the decimal point.
|
|
|
629 |
// (The only way to do this in the regex would make it much more complicated.)
|
|
|
630 |
if ($beforepoint === '' && $decimals === '') {
|
|
|
631 |
return array(null, null, null, null);
|
|
|
632 |
}
|
|
|
633 |
|
|
|
634 |
if ($this->unitsbefore) {
|
|
|
635 |
$unit = substr($response, 0, -strlen($matchedpart));
|
|
|
636 |
} else {
|
|
|
637 |
$unit = substr($response, strlen($matchedpart));
|
|
|
638 |
}
|
|
|
639 |
$unit = trim($unit);
|
|
|
640 |
|
|
|
641 |
return array($beforepoint, $decimals, $exponent, $unit);
|
|
|
642 |
}
|
|
|
643 |
|
|
|
644 |
/**
|
|
|
645 |
* Takes a number in almost any localised form, and possibly with a unit
|
|
|
646 |
* after it. It separates off the unit, if present, and converts to the
|
|
|
647 |
* default unit, by using the given unit multiplier.
|
|
|
648 |
*
|
|
|
649 |
* @param string $response a value, optionally with a unit.
|
|
|
650 |
* @return array(numeric, string, multiplier) the value with the unit stripped, and normalised
|
|
|
651 |
* by the unit multiplier, if any, and the unit string, for reference.
|
|
|
652 |
*/
|
|
|
653 |
public function apply_units($response, $separateunit = null): array {
|
|
|
654 |
if ($response === null || trim($response) === '') {
|
|
|
655 |
return [null, null, null];
|
|
|
656 |
}
|
|
|
657 |
|
|
|
658 |
// Strip spaces (which may be thousands separators) and change other forms
|
|
|
659 |
// of writing e to e.
|
|
|
660 |
$response = str_replace(' ', '', $response);
|
|
|
661 |
$response = preg_replace('~(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)~', 'e$1', $response);
|
|
|
662 |
|
|
|
663 |
// If a . is present or there are multiple , (i.e. 2,456,789 ) assume ,
|
|
|
664 |
// is a thouseands separator, and strip it, else assume it is a decimal
|
|
|
665 |
// separator, and change it to ..
|
|
|
666 |
if (strpos($response, '.') !== false || substr_count($response, ',') > 1) {
|
|
|
667 |
$response = str_replace(',', '', $response);
|
|
|
668 |
} else {
|
|
|
669 |
$response = str_replace([$this->thousandssep, $this->decsep, ','], ['', '.', '.'], $response);
|
|
|
670 |
}
|
|
|
671 |
|
|
|
672 |
$regex = '[+-]?(?:\d+(?:\\.\d*)?|\\.\d+)(?:e[-+]?\d+)?';
|
|
|
673 |
if ($this->unitsbefore) {
|
|
|
674 |
$regex = "/{$regex}$/";
|
|
|
675 |
} else {
|
|
|
676 |
$regex = "/^{$regex}/";
|
|
|
677 |
}
|
|
|
678 |
if (!preg_match($regex, $response, $matches)) {
|
|
|
679 |
return array(null, null, null);
|
|
|
680 |
}
|
|
|
681 |
|
|
|
682 |
$numberstring = $matches[0];
|
|
|
683 |
if ($this->unitsbefore) {
|
|
|
684 |
// Substr returns false when it means '', so cast back to string.
|
|
|
685 |
$unit = (string) substr($response, 0, -strlen($numberstring));
|
|
|
686 |
} else {
|
|
|
687 |
$unit = (string) substr($response, strlen($numberstring));
|
|
|
688 |
}
|
|
|
689 |
|
|
|
690 |
if (!is_null($separateunit)) {
|
|
|
691 |
$unit = $separateunit;
|
|
|
692 |
}
|
|
|
693 |
|
|
|
694 |
if ($this->is_known_unit($unit)) {
|
|
|
695 |
$multiplier = 1 / $this->units[$unit];
|
|
|
696 |
} else {
|
|
|
697 |
$multiplier = null;
|
|
|
698 |
}
|
|
|
699 |
|
|
|
700 |
return array($numberstring + 0, $unit, $multiplier); // The + 0 is to convert to number.
|
|
|
701 |
}
|
|
|
702 |
|
|
|
703 |
/**
|
|
|
704 |
* @return string the default unit.
|
|
|
705 |
*/
|
|
|
706 |
public function get_default_unit() {
|
|
|
707 |
reset($this->units);
|
|
|
708 |
return key($this->units);
|
|
|
709 |
}
|
|
|
710 |
|
|
|
711 |
/**
|
|
|
712 |
* @param string $answer a response.
|
|
|
713 |
* @param string $unit a unit.
|
|
|
714 |
*/
|
|
|
715 |
public function add_unit($answer, $unit = null) {
|
|
|
716 |
if (is_null($unit)) {
|
|
|
717 |
$unit = $this->get_default_unit();
|
|
|
718 |
}
|
|
|
719 |
|
|
|
720 |
if (!$unit) {
|
|
|
721 |
return $answer;
|
|
|
722 |
}
|
|
|
723 |
|
|
|
724 |
if ($this->unitsbefore) {
|
|
|
725 |
return $unit . ' ' . $answer;
|
|
|
726 |
} else {
|
|
|
727 |
return $answer . ' ' . $unit;
|
|
|
728 |
}
|
|
|
729 |
}
|
|
|
730 |
|
|
|
731 |
/**
|
|
|
732 |
* Is this unit recognised.
|
|
|
733 |
* @param string $unit the unit
|
|
|
734 |
* @return bool whether this is a unit we recognise.
|
|
|
735 |
*/
|
|
|
736 |
public function is_known_unit($unit) {
|
|
|
737 |
return array_key_exists($unit, $this->units);
|
|
|
738 |
}
|
|
|
739 |
|
|
|
740 |
/**
|
|
|
741 |
* Whether the units go before or after the number.
|
|
|
742 |
* @return true = before, false = after.
|
|
|
743 |
*/
|
|
|
744 |
public function are_units_before() {
|
|
|
745 |
return $this->unitsbefore;
|
|
|
746 |
}
|
|
|
747 |
|
|
|
748 |
/**
|
|
|
749 |
* Get the units as an array suitably for passing to html_writer::select.
|
|
|
750 |
* @return array of unit choices.
|
|
|
751 |
*/
|
|
|
752 |
public function get_unit_options() {
|
|
|
753 |
$options = array();
|
|
|
754 |
foreach ($this->units as $unit => $notused) {
|
|
|
755 |
$options[$unit] = $unit;
|
|
|
756 |
}
|
|
|
757 |
return $options;
|
|
|
758 |
}
|
|
|
759 |
}
|