Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
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
}