Skip to content

Commit

Permalink
MDL-27410 qtype_calculated works in my unit tests.
Browse files Browse the repository at this point in the history
Probably does not work through the Moodle UI yet.
  • Loading branch information
timhunt committed May 18, 2011
1 parent f580e0e commit 1da4060
Show file tree
Hide file tree
Showing 6 changed files with 439 additions and 40 deletions.
125 changes: 89 additions & 36 deletions question/type/calculated/question.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,25 @@
*/
class qtype_calculated_question extends qtype_numerical_question {
/** @var qtype_calculated_dataset_loader helper for loading the dataset. */
protected $datasetloader;
public $datasetloader;
/** @var qtype_calculated_variable_substituter stores the dataset we are using. */
protected $vs;
public $vs;

public function start_attempt(question_attempt_step $step) {
$maxnumber = $this->datasetloader->get_number_of_datasets();
$maxnumber = $this->datasetloader->get_number_of_items();
$setnumber = rand(1, $maxnumber);
// TODO implement the $synchronizecalculated bit from create_session_and_responses.

$this->vs = $this->datasetloader->load_dataset($setnumber);
$this->vs = new qtype_calculated_variable_substituter(
$this->datasetloader->get_values($setnumber),
get_string('decsep', 'langconfig'));
$this->calculate_all_expressions();

$step->set_qt_var('_dataset', $setnumber);
foreach ($this->vs->get_values() as $name => $value) {
$step->set_qt_var('_var_' . $name, $value);
}

$this->calculate_all_expressions();

parent::start_attempt($step);
}

Expand All @@ -65,8 +66,9 @@ public function apply_attempt_state(question_attempt_step $step) {
$values[substr($name, 5)] = $value;
}
}
$this->vs = new qtype_calculated_variable_substituter($values);

$this->vs = new qtype_calculated_variable_substituter(
$values, get_string('decsep', 'langconfig'));
$this->calculate_all_expressions();

parent::apply_attempt_state($step);
Expand Down Expand Up @@ -134,31 +136,37 @@ public function get_number_of_items() {
return $this->itemsavailable;
}

/**
* Actually query the database for the values.
* @param int $itemnumber which set of values to load.
* @return array name => value;
*/
protected function load_values($itemnumber) {
return $DB->get_records_sql('
SELECT qdd.name, qdi.value
FROM {question_dataset_items} qdi
JOIN {question_dataset_definitions} qdd ON qdd.id = qdi.definition
JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition
WHERE qd.question = ?
AND qdi.itemnumber = ?
', array($this->questionid, $itemnumber));
}

/**
* Load a particular set of values for each dataset used by this question.
* @param int $itemnumber which set of values to load.
* 0 < $itemnumber <= {@link get_number_of_items()}.
* @return qtype_calculated_variable_substituter with the correct variable
* -> value substitutions set up.
* @return array name => value.
*/
public function load_values($itemnumber) {
public function get_values($itemnumber) {
if ($itemnumber <= 0 || $itemnumber > $this->get_number_of_items()) {
$a = new stdClass();
$a->id = $this->questionid;
$a->item = $itemnumber;
throw new moodle_exception('cannotgetdsfordependent', 'question', '', $a);
}

$values = $DB->get_records_sql('
SELECT qdd.name, qdi.value
FROM {question_dataset_items} qdi
JOIN {question_dataset_definitions} qdd ON qdd.id = qdi.definition
JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition
WHERE qd.question = ?
AND qdi.itemnumber = ?
', array($this->questionid, $itemnumber));

return new qtype_calculated_variable_substituter($values);
return $this->load_values($itemnumber);
}
}

Expand All @@ -177,21 +185,31 @@ class qtype_calculated_variable_substituter {
/** @var array variable name => value */
protected $values;

/** @var string character to use for the decimal point in displayed numbers. */
protected $decimalpoint;

/** @var array variable names wrapped in {...}. Used by {@link substitute_values()}. */
protected $search;

/**
* @var array variable values, with negative numbers wrapped in (...).
* Used by {@link substitute_values()}.
*/
protected $replace;
protected $safevalue;

/**
* @var array variable values, with negative numbers wrapped in (...).
* Used by {@link substitute_values()}.
*/
protected $prettyvalue;

/**
* Constructor
* @param array $values variable name => value.
*/
public function __construct(array $values) {
public function __construct(array $values, $decimalpoint) {
$this->values = $values;
$this->decimalpoint = $decimalpoint;

// Prepare an array for {@link substitute_values()}.
$this->search = array();
Expand All @@ -205,20 +223,25 @@ public function __construct(array $values) {
}

$this->search[] = '{' . $name . '}';
if ($value < 0) {
$this->replace[] = '(' . $value . ')';
} else {
$this->replace[] = $value;
}
$this->safevalue[] = '(' . $value . ')';
$this->prettyvalue[] = $this->format_float($value);
}
}

/**
* Display a float properly formatted with a certain number of decimal places.
* @param $x
*/
public function format_float($x) {
return str_replace('.', $this->decimalpoint, $x);
}

/**
* Return an array of the variables and their values.
* @return array name => value.
*/
public function get_values() {
return clone($this->values);
return $this->values;
}

/**
Expand All @@ -228,28 +251,58 @@ public function get_values() {
* @return float the computed result.
*/
public function calculate($expression) {
$exp = $this->substitute_values($expression);
return $this->calculate_raw($this->substitute_values_for_eval($expression));
}

/**
* Evaluate an expression after the variable values have been substituted.
* @param string $expression the expression. A PHP expression with placeholders
* like {a} for where the variables need to go.
* @return float the computed result.
*/
protected function calculate_raw($expression) {
// This validation trick from http://php.net/manual/en/function.eval.php
if (!@eval('return true; $result = ' . $exp . ';')) {
if (!@eval('return true; $result = ' . $expression . ';')) {
throw new moodle_exception('illegalformulasyntax', 'qtype_calculated', '', $expression);
}
return eval('return ' . $exp . ';');
return eval('return ' . $expression . ';');
}

/**
* Substitute variable placehodlers like {a} with their value.
* Substitute variable placehodlers like {a} with their value wrapped in ().
* @param string $expression the expression. A PHP expression with placeholders
* like {a} for where the variables need to go.
* @return string the expression with each placeholder replaced by the
* corresponding value.
*/
protected function substitute_values($expression) {
return str_replace($this->search, $this->replace, $expression);
protected function substitute_values_for_eval($expression) {
return str_replace($this->search, $this->safevalue, $expression);
}

/**
* Substitute variable placehodlers like {a} with their value without wrapping
* the value in anything.
* @param string $text some content with placeholders
* like {a} for where the variables need to go.
* @return string the expression with each placeholder replaced by the
* corresponding value.
*/
protected function substitute_values_pretty($text) {
return str_replace($this->search, $this->prettyvalue, $text);
}

/**
* Replace any embedded variables (like {a}) or formulae (like {={a} + {b}})
* in some text with the corresponding values.
* @param string $text the text to process.
* @return string the text with values substituted.
*/
public function replace_expressions_in_text($text) {
// TODO
return $text;
$vs = $this; // Can't see to use $this in a PHP closure.
$text = preg_replace_callback('~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)}~', function ($matches) use ($vs) {
return $vs->format_float($vs->calculate($matches[1]));
}, $text);
return $this->substitute_values_pretty($text);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions question/type/calculated/renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

defined('MOODLE_INTERNAL') || die();

require_once($CFG->dirroot . '/question/type/numerical/renderer.php');


/**
* Generates the output for calculated questions.
Expand Down
96 changes: 96 additions & 0 deletions question/type/calculated/simpletest/helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Test helpers for the calculated question type.
*
* @package qtype
* @subpackage calculated
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/


defined('MOODLE_INTERNAL') || die();


/**
* Test helper class for the calculated question type.
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_calculated_test_helper extends question_test_helper {
public function get_test_questions() {
return array('sum');
}

/**
* Makes a calculated question with correct ansewer 3.14, and various incorrect
* answers with different feedback.
* @return qtype_calculated_question
*/
public function make_calculated_question_sum() {
question_bank::load_question_definition_classes('calculated');
$q = new qtype_calculated_question();
test_question_maker::initialise_a_question($q);
$q->name = 'Simple sum';
$q->questiontext = 'What is {a} + {b}?';
$q->generalfeedback = 'Generalfeedback: {={a} + {b}} is the right answer.';
$q->answers = array(
13 => new qtype_numerical_answer(13, '{a} + {b}', 1.0, 'Very good.', FORMAT_HTML, 0),
14 => new qtype_numerical_answer(14, '{a} - {b}', 0.0, 'Add. not subtract!.', FORMAT_HTML, 0),
17 => new qtype_numerical_answer(17, '*', 0.0, 'Completely wrong.', FORMAT_HTML, 0),
);
$q->qtype = question_bank::get_qtype('calculated');
$q->unitdisplay = qtype_numerical::UNITNONE;
$q->unitgradingtype = 0;
$q->unitpenalty = 0;
$q->ap = new qtype_numerical_answer_processor(array());

$q->datasetloader = new qtype_calculated_test_dataset_loader(0, array(
array('a' => 1, 'b' => 5),
array('a' => 3, 'b' => 4),
));

return $q;
}
}


/**
* Test implementation of {@link qtype_calculated_dataset_loader}. Gets the values
* from an array passed to the constructor, rather than querying the database.
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_calculated_test_dataset_loader extends qtype_calculated_dataset_loader{
protected $valuesets;

public function __construct($questionid, array $valuesets) {
parent::__construct($questionid);
$this->valuesets = $valuesets;
}

public function get_number_of_items() {
return count($this->valuesets);
}

public function load_values($itemnumber) {
return $this->valuesets[$itemnumber - 1];
}
}
Loading

0 comments on commit 1da4060

Please sign in to comment.