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 * Class for converting attempt data for calculated questions when upgrading 19 * attempts to the new question engine. 20 * 21 * This class is used by the code in question/engine/upgrade/upgradelib.php. 22 * 23 * @package qtype_calculated 24 * @copyright 2011 The Open University 25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 */ 27 class qtype_calculated_qe2_attempt_updater extends question_qtype_attempt_updater { 28 protected $selecteditem = null; 29 /** @var array variable name => value */ 30 protected $values = []; 31 32 /** @var array variable names wrapped in {...}. Used by {@link substitute_values()}. */ 33 protected $search = []; 34 35 /** 36 * @var array variable values, with negative numbers wrapped in (...). 37 * Used by {@link substitute_values()}. 38 */ 39 protected $safevalue = []; 40 41 /** 42 * @var array variable values, with negative numbers wrapped in (...). 43 * Used by {@link substitute_values()}. 44 */ 45 protected $prettyvalue = []; 46 47 public function question_summary() { 48 return ''; // Done later, after we know which dataset is used. 49 } 50 51 public function right_answer() { 52 foreach ($this->question->options->answers as $ans) { 53 if ($ans->fraction > 0.999) { 54 $right = $this->calculate($ans->answer); 55 56 if (empty($this->question->options->units)) { 57 return $right; 58 } 59 60 $unit = reset($this->question->options->units); 61 $unit = $unit->unit; 62 if (!empty($this->question->options->unitsleft)) { 63 return $unit . ' ' . $right; 64 } else { 65 return $right . ' ' . $unit; 66 } 67 } 68 } 69 } 70 71 protected function parse_response($state) { 72 if (strpos($state->answer, '-') < 7) { 73 // Broken state, skip it. 74 throw new coding_exception("Brokes state {$state->id} for calculated 75 question {$state->question}. (It did not specify a dataset."); 76 } 77 [$datasetbit, $realanswer] = explode('-', $state->answer, 2); 78 $selecteditem = substr($datasetbit, 7); 79 80 if (is_null($this->selecteditem)) { 81 $this->load_dataset($selecteditem); 82 } else if ($this->selecteditem != $selecteditem) { 83 $this->logger->log_assumption("Different states for calculated question 84 {$state->question} used different dataset items. Ignoring the change 85 in state {$state->id} and coninuting to use item {$this->selecteditem}."); 86 } 87 88 if (!$realanswer) { 89 return ['', '']; 90 } 91 92 if (strpos($realanswer, '|||||') === false) { 93 $answer = $realanswer; 94 $unit = ''; 95 } else { 96 [$answer, $unit] = explode('|||||', $realanswer, 2); 97 } 98 99 return [$answer, $unit]; 100 } 101 102 public function response_summary($state) { 103 [$answer, $unit] = $this->parse_response($state); 104 105 if (empty($answer) && empty($unit)) { 106 $resp = null; 107 } else { 108 $resp = $answer; 109 } 110 111 if (!empty($unit)) { 112 if (!empty($this->question->options->unitsleft)) { 113 $resp = trim($unit . ' ' . $resp); 114 } else { 115 $resp = trim($resp . ' ' . $unit); 116 } 117 } 118 119 return $resp; 120 } 121 122 public function was_answered($state) { 123 return !empty($state->answer); 124 } 125 126 public function set_first_step_data_elements($state, &$data) { 127 $this->parse_response($state); 128 $this->updater->qa->questionsummary = $this->to_text( 129 $this->replace_expressions_in_text($this->question->questiontext)); 130 $this->updater->qa->rightanswer = $this->right_answer($this->question); 131 132 foreach ($this->values as $name => $value) { 133 $data['_var_' . $name] = $value; 134 } 135 136 $data['_separators'] = '.$,'; 137 } 138 139 public function supply_missing_first_step_data(&$data) { 140 $data['_separators'] = '.$,'; 141 } 142 143 public function set_data_elements_for_step($state, &$data) { 144 if (empty($state->answer)) { 145 return; 146 } 147 148 [$answer, $unit] = $this->parse_response($state); 149 if ( 150 !empty($this->question->options->showunits) && 151 $this->question->options->showunits == 1 152 ) { 153 // Multichoice units. 154 $data['answer'] = $answer; 155 $data['unit'] = $unit; 156 } else if (!empty($this->question->options->unitsleft)) { 157 if (!empty($unit)) { 158 $data['answer'] = $unit . ' ' . $answer; 159 } else { 160 $data['answer'] = $answer; 161 } 162 } else { 163 if (!empty($unit)) { 164 $data['answer'] = $answer . ' ' . $unit; 165 } else { 166 $data['answer'] = $answer; 167 } 168 } 169 } 170 171 public function load_dataset($selecteditem) { 172 $this->selecteditem = $selecteditem; 173 $this->updater->qa->variant = $selecteditem; 174 $this->values = $this->qeupdater->load_dataset( 175 $this->question->id, $selecteditem); 176 177 // Prepare an array for {@link substitute_values()}. 178 $this->search = []; 179 $this->safevalue = []; 180 $this->prettyvalue = []; 181 foreach ($this->values as $name => $value) { 182 if (!is_numeric($value)) { 183 $a = new stdClass(); 184 $a->name = '{' . $name . '}'; 185 $a->value = $value; 186 throw new moodle_exception('notvalidnumber', 'qtype_calculated', '', $a); 187 } 188 189 $this->search[] = '{' . $name . '}'; 190 $this->safevalue[] = '(' . $value . ')'; 191 $this->prettyvalue[] = $this->format_float($value); 192 } 193 } 194 195 /** 196 * This function should be identical to 197 * {@link qtype_calculated_variable_substituter::format_float()}. Except that 198 * we do not try to do locale-aware replacement of the decimal point. 199 * 200 * Having to copy it here is a pain, but it is the standard rule about not 201 * using library code (which may change in future) in upgrade code, which 202 * exists at a point in time. 203 * 204 * Display a float properly formatted with a certain number of decimal places. 205 * @param number $x the number to format 206 * @param int $length restrict to this many decimal places or significant 207 * figures. If null, the number is not rounded. 208 * @param int format 1 => decimalformat, 2 => significantfigures. 209 * @return string formtted number. 210 */ 211 public function format_float($x, $length = null, $format = null) { 212 if (!is_null($length) && !is_null($format)) { 213 if ($format == 1) { 214 // Decimal places. 215 $x = sprintf('%.' . $length . 'F', $x); 216 } else if ($format == 2) { 217 // Significant figures. 218 $x = sprintf('%.' . $length . 'g', $x); 219 } 220 } 221 return $x; 222 } 223 224 /** 225 * Evaluate an expression using the variable values. 226 * @param string $expression the expression. A PHP expression with placeholders 227 * like {a} for where the variables need to go. 228 * @return float the computed result. 229 */ 230 public function calculate($expression) { 231 return $this->calculate_raw($this->substitute_values_for_eval($expression)); 232 } 233 234 /** 235 * Evaluate an expression after the variable values have been substituted. 236 * @param string $expression the expression. A PHP expression with placeholders 237 * like {a} for where the variables need to go. 238 * @return float the computed result. 239 */ 240 protected function calculate_raw($expression) { 241 try { 242 // In older PHP versions this this is a way to validate code passed to eval. 243 // The trick came from http://php.net/manual/en/function.eval.php. 244 if (@eval('return true; $result = ' . $expression . ';')) { 245 return eval('return ' . $expression . ';'); 246 } 247 } catch (Throwable $e) { 248 // PHP7 and later now throws ParseException and friends from eval(), 249 // which is much better. 250 } 251 // In either case of an invalid $expression, we end here. 252 return '[Invalid expression ' . $expression . ']'; 253 } 254 255 /** 256 * Substitute variable placehodlers like {a} with their value wrapped in (). 257 * @param string $expression the expression. A PHP expression with placeholders 258 * like {a} for where the variables need to go. 259 * @return string the expression with each placeholder replaced by the 260 * corresponding value. 261 */ 262 protected function substitute_values_for_eval($expression) { 263 return str_replace($this->search, $this->safevalue, $expression ?? ''); 264 } 265 266 /** 267 * Substitute variable placehodlers like {a} with their value without wrapping 268 * the value in anything. 269 * @param string $text some content with placeholders 270 * like {a} for where the variables need to go. 271 * @return string the expression with each placeholder replaced by the 272 * corresponding value. 273 */ 274 protected function substitute_values_pretty($text) { 275 return str_replace($this->search, $this->prettyvalue, $text ?? ''); 276 } 277 278 /** 279 * Replace any embedded variables (like {a}) or formulae (like {={a} + {b}}) 280 * in some text with the corresponding values. 281 * @param string $text the text to process. 282 * @return string the text with values substituted. 283 */ 284 public function replace_expressions_in_text($text, $length = null, $format = null) { 285 if ($text === null || $text === '') { 286 return $text; 287 } 288 $vs = $this; // Can't see to use $this in a PHP closure. 289 $text = preg_replace_callback( 290 '~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)}~', 291 function ($matches) use ($vs, $format, $length) { 292 return $vs->format_float($vs->calculate($matches[1]), $length, $format); 293 }, 294 $text 295 ); 296 return $this->substitute_values_pretty($text); 297 } 298 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body