Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body