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