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