Merge branch 's11_MDL-28121_split_master' of github.com:dongsheng/moodle
[moodle.git] / question / type / calculatedmulti / 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 multiple-choice question type.
19 *
20 * @package qtype
21 * @subpackage calculatedmulti
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 multiple-choice questions
32 * when upgrading 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_calculatedmulti_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 protected $order;
60
61 public function question_summary() {
62 return ''; // Done later, after we know which dataset is used.
63 }
64
65 public function right_answer() {
66 if ($this->question->options->single) {
67 foreach ($this->question->options->answers as $ans) {
68 if ($ans->fraction > 0.999) {
69 return $this->to_text($this->replace_expressions_in_text($ans->answer));
70 }
71 }
72
73 } else {
74 $rightbits = array();
75 foreach ($this->question->options->answers as $ans) {
76 if ($ans->fraction >= 0.000001) {
77 $rightbits[] = $this->to_text($this->replace_expressions_in_text($ans->answer));
78 }
79 }
80 return implode('; ', $rightbits);
81 }
82 }
83
84 protected function explode_answer($state) {
85 if (strpos($state->answer, '-') < 7) {
86 // Broken state, skip it.
87 throw new coding_exception("Brokes state {$state->id} for calcluatedmulti
88 question {$state->question}. (It did not specify a dataset.");
89 }
90 list($datasetbit, $answer) = explode('-', $state->answer, 2);
91 $selecteditem = substr($datasetbit, 7);
92
93 if (is_null($this->selecteditem)) {
94 $this->load_dataset($selecteditem);
95 } else if ($this->selecteditem != $selecteditem) {
96 $this->logger->log_assumption("Different states for calcluatedmulti question
97 {$state->question} used different dataset items. Ignoring the change
98 in state {$state->id} and coninuting to use item {$this->selecteditem}.");
99 }
100
101 if (strpos($answer, ':') !== false) {
102 list($order, $responses) = explode(':', $answer);
103 return $responses;
104 } else {
105 // Sometimes, a bug means that a state is missing the <order>: bit,
106 // We need to deal with that.
107 $this->logger->log_assumption("Dealing with missing order information
108 in attempt at multiple choice question {$this->question->id}");
109 return $answer;
110 }
111 }
112
113 public function response_summary($state) {
114 $responses = $this->explode_answer($state);
115 if ($this->question->options->single) {
116 if (is_numeric($responses)) {
117 if (array_key_exists($responses, $this->question->options->answers)) {
118 return $this->to_text($this->replace_expressions_in_text(
119 $this->question->options->answers[$responses]->answer));
120 } else {
121 $this->logger->log_assumption("Dealing with a place where the
122 student selected a choice that was later deleted for
123 multiple choice question {$this->question->id}");
124 return '[CHOICE THAT WAS LATER DELETED]';
125 }
126 } else {
127 return null;
128 }
129
130 } else {
131 if (!empty($responses)) {
132 $responses = explode(',', $responses);
133 $bits = array();
134 foreach ($responses as $response) {
135 if (array_key_exists($response, $this->question->options->answers)) {
136 $bits[] = $this->to_text($this->replace_expressions_in_text(
137 $this->question->options->answers[$response]->answer));
138 } else {
139 $this->logger->log_assumption("Dealing with a place where the
140 student selected a choice that was later deleted for
141 multiple choice question {$this->question->id}");
142 $bits[] = '[CHOICE THAT WAS LATER DELETED]';
143 }
144 }
145 return implode('; ', $bits);
146 } else {
147 return null;
148 }
149 }
150 }
151
152 public function was_answered($state) {
153 $responses = $this->explode_answer($state);
154 if ($this->question->options->single) {
155 return is_numeric($responses);
156 } else {
157 return !empty($responses);
158 }
159 }
160
161 public function set_first_step_data_elements($state, &$data) {
162 $this->explode_answer($state);
163 $this->updater->qa->questionsummary = $this->to_text(
164 $this->replace_expressions_in_text($this->question->questiontext));
165 $this->updater->qa->rightanswer = $this->right_answer($this->question);
166
667cdde3
TH
167 foreach ($this->values as $name => $value) {
168 $data['_var_' . $name] = $value;
169 }
170
171 list($datasetbit, $answer) = explode('-', $state->answer, 2);
172 list($order, $responses) = explode(':', $answer);
173 $data['_order'] = $order;
174 $this->order = explode(',', $order);
175 }
176
177 public function supply_missing_first_step_data(&$data) {
178 $data['_order'] = implode(',', array_keys($this->question->options->answers));
179 }
180
181 public function set_data_elements_for_step($state, &$data) {
182 $responses = $this->explode_answer($state);
183 if ($this->question->options->single) {
184 if (is_numeric($responses)) {
185 $flippedorder = array_combine(array_values($this->order), array_keys($this->order));
186 if (array_key_exists($responses, $flippedorder)) {
187 $data['answer'] = $flippedorder[$responses];
188 } else {
189 $data['answer'] = '-1';
190 }
191 }
192
193 } else {
194 $responses = explode(',', $responses);
195 foreach ($this->order as $key => $ansid) {
196 if (in_array($ansid, $responses)) {
197 $data['choice' . $key] = 1;
198 } else {
199 $data['choice' . $key] = 0;
200 }
201 }
202 }
203 }
204
205 public function load_dataset($selecteditem) {
206 $this->selecteditem = $selecteditem;
c014b989 207 $this->updater->qa->variant = $selecteditem;
667cdde3
TH
208 $this->values = $this->qeupdater->load_dataset(
209 $this->question->id, $selecteditem);
210
211 // Prepare an array for {@link substitute_values()}.
212 $this->search = array();
213 $this->safevalue = array();
214 $this->prettyvalue = array();
215 foreach ($this->values as $name => $value) {
216 if (!is_numeric($value)) {
217 $a = new stdClass();
218 $a->name = '{' . $name . '}';
219 $a->value = $value;
220 throw new moodle_exception('notvalidnumber', 'qtype_calculated', '', $a);
221 }
222
223 $this->search[] = '{' . $name . '}';
224 $this->safevalue[] = '(' . $value . ')';
225 $this->prettyvalue[] = $this->format_float($value);
226 }
227 }
228
229 /**
230 * Display a float properly formatted with a certain number of decimal places.
231 * @param $x
232 */
233 public function format_float($x, $length = null, $format = null) {
3aa15970 234 if (!is_null($length) && !is_null($format)) {
667cdde3
TH
235 if ($format == 1) {
236 // Decimal places.
237 $x = sprintf('%.' . $length . 'F', $x);
238 } else if ($format == 1) {
239 // Significant figures.
240 $x = sprintf('%.' . $length . 'g', $x);
241 $x = str_replace(',', '.', $x);
242 }
243 }
244 return $x;
245 }
246
247 /**
248 * Evaluate an expression using the variable values.
249 * @param string $expression the expression. A PHP expression with placeholders
250 * like {a} for where the variables need to go.
251 * @return float the computed result.
252 */
253 public function calculate($expression) {
254 return $this->calculate_raw($this->substitute_values_for_eval($expression));
255 }
256
257 /**
258 * Evaluate an expression after the variable values have been substituted.
259 * @param string $expression the expression. A PHP expression with placeholders
260 * like {a} for where the variables need to go.
261 * @return float the computed result.
262 */
263 protected function calculate_raw($expression) {
264 // This validation trick from http://php.net/manual/en/function.eval.php
265 if (!@eval('return true; $result = ' . $expression . ';')) {
266 return '[Invalid expression ' . $expression . ']';
267 }
268 return eval('return ' . $expression . ';');
269 }
270
271 /**
272 * Substitute variable placehodlers like {a} with their value wrapped in ().
273 * @param string $expression the expression. A PHP expression with placeholders
274 * like {a} for where the variables need to go.
275 * @return string the expression with each placeholder replaced by the
276 * corresponding value.
277 */
278 protected function substitute_values_for_eval($expression) {
279 return str_replace($this->search, $this->safevalue, $expression);
280 }
281
282 /**
283 * Substitute variable placehodlers like {a} with their value without wrapping
284 * the value in anything.
285 * @param string $text some content with placeholders
286 * like {a} for where the variables need to go.
287 * @return string the expression with each placeholder replaced by the
288 * corresponding value.
289 */
290 protected function substitute_values_pretty($text) {
291 return str_replace($this->search, $this->prettyvalue, $text);
292 }
293
294 /**
295 * Replace any embedded variables (like {a}) or formulae (like {={a} + {b}})
296 * in some text with the corresponding values.
297 * @param string $text the text to process.
298 * @return string the text with values substituted.
299 */
300 public function replace_expressions_in_text($text, $length = null, $format = null) {
301 $vs = $this; // Can't see to use $this in a PHP closure.
302 $text = preg_replace_callback('~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)}~',
303 function ($matches) use ($vs, $format, $length) {
304 return $vs->format_float($vs->calculate($matches[1]), $length, $format);
305 }, $text);
306 return $this->substitute_values_pretty($text);
307 }
308}