MDL-20636 Fix numerical unit tests.

This commit is contained in:
Tim Hunt
2011-04-28 20:06:29 +01:00
parent 50c0296698
commit 0ff173d3f2
5 changed files with 481 additions and 4 deletions
+171
View File
@@ -838,6 +838,42 @@ class qtype_numerical extends question_type {
return $correct;
}
public function get_random_guess_score($questiondata) {
foreach ($questiondata->options->answers as $aid => $answer) {
if ('*' == trim($answer->answer)) {
return $answer->fraction;
}
}
return 0;
}
public function get_possible_responses($questiondata) {
$responses = array();
$unit = $this->get_default_numerical_unit($questiondata);
foreach ($questiondata->options->answers as $aid => $answer) {
$responseclass = $answer->answer;
if ($responseclass != '*') {
if ($unit) {
$responseclass .= ' ' . $unit->unit;
}
$ans = new qtype_numerical_answer($answer->id, $answer->answer, $answer->fraction,
$answer->feedback, $answer->feedbackformat, $answer->tolerance);
list($min, $max) = $ans->get_tolerance_interval();
$responseclass .= " ($min..$max)";
}
$responses[$aid] = new question_possible_response($responseclass,
$answer->fraction);
}
$responses[null] = question_possible_response::no_response();
return array($questiondata->id => $responses);
}
function get_tolerance_interval(&$answer) {
// No tolerance
if (empty($answer->tolerance)) {
@@ -1325,3 +1361,138 @@ class qtype_numerical extends question_type {
}
}
}
/**
* This class processes numbers with units.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_numerical_answer_processor {
/** @var array unit name => multiplier. */
protected $units;
/** @var string character used as decimal point. */
protected $decsep;
/** @var string character used as thousands separator. */
protected $thousandssep;
protected $regex = null;
public function __construct($units, $decsep = null, $thousandssep = null) {
if (is_null($decsep)) {
$decsep = get_string('decsep', 'langconfig');
}
$this->decsep = $decsep;
if (is_null($thousandssep)) {
$thousandssep = get_string('thousandssep', 'langconfig');
}
$this->thousandssep = $thousandssep;
$this->units = $units;
}
/**
* Set the decimal point and thousands separator character that should be used.
* @param string $decsep
* @param string $thousandssep
*/
public function set_characters($decsep, $thousandssep) {
$this->decsep = $decsep;
$this->thousandssep = $thousandssep;
$this->regex = null;
}
/** @return string the decimal point character used. */
public function get_point() {
return $this->decsep;
}
/** @return string the thousands separator character used. */
public function get_separator() {
return $this->thousandssep;
}
/**
* Create the regular expression that {@link parse_response()} requires.
* @return string
*/
protected function build_regex() {
if (!is_null($this->regex)) {
return $this->regex;
}
$beforepointre = '([+-]?[' . preg_quote($this->thousandssep, '/') . '\d]*)';
$decimalsre = preg_quote($this->decsep, '/') . '(\d*)';
$exponentre = '(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)';
$escapedunits = array();
foreach ($this->units as $unit => $notused) {
$escapedunits[] = preg_quote($unit, '/');
}
$unitre = '(' . implode('|', $escapedunits) . ')';
$this->regex = "/^$beforepointre(?:$decimalsre)?(?:$exponentre)?\s*(?:$unitre)?$/U";
return $this->regex;
}
/**
* Take a string which is a number with or without a decimal point and exponent,
* and possibly followed by one of the units, and split it into bits.
* @param string $response a value, optionally with a unit.
* @return array four strings (some of which may be blank) the digits before
* and after the decimal point, the exponent, and the unit. All four will be
* null if the response cannot be parsed.
*/
protected function parse_response($response) {
if (!preg_match($this->build_regex(), $response, $matches)) {
return array(null, null, null, null);
}
$matches += array('', '', '', '', ''); // Fill in any missing matches.
list($notused, $beforepoint, $decimals, $exponent, $unit) = $matches;
// Strip out thousands separators.
$beforepoint = str_replace($this->thousandssep, '', $beforepoint);
// Must be either something before, or something after the decimal point.
// (The only way to do this in the regex would make it much more complicated.)
if ($beforepoint === '' && $decimals === '') {
return array(null, null, null, null);
}
return array($beforepoint, $decimals, $exponent, $unit);
}
/**
* Takes a number in localised form, that is, using the decsep and thousandssep
* defined in the lanuage pack, and possibly with a unit after it. It separates
* off the unit, if present, and converts to the default unit, by using the
* given unit multiplier.
*
* @param string $response a value, optionally with a unit.
* @return array(numeric, sting) the value with the unit stripped, and normalised
* by the unit multiplier, if any, and the unit string, for reference.
*/
public function apply_units($response) {
list($beforepoint, $decimals, $exponent, $unit) = $this->parse_response($response);
if (is_null($beforepoint)) {
return array(null, null);
}
$numberstring = $beforepoint . '.' . $decimals;
if ($exponent) {
$numberstring .= 'e' . $exponent;
}
if ($unit) {
$value = $numberstring * $this->units[$unit];
} else {
$value = $numberstring * 1;
}
return array($value, $unit);
}
}
@@ -0,0 +1,74 @@
<?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/>.
/**
* Unit tests for the numerical question definition class.
*
* @package moodlecore
* @subpackage questiontypes
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once($CFG->dirroot . '/question/type/numerical/question.php');
class qtype_numerical_answer_test extends UnitTestCase {
public function test_within_tolerance_nominal() {
$answer = new qtype_numerical_answer(13, 7.0, 1.0, '', FORMAT_MOODLE, 1.0);
$this->assertFalse($answer->within_tolerance(5.99));
$this->assertTrue($answer->within_tolerance(6));
$this->assertTrue($answer->within_tolerance(7));
$this->assertTrue($answer->within_tolerance(8));
$this->assertFalse($answer->within_tolerance(8.01));
}
public function test_within_tolerance_blank() {
$answer = new qtype_numerical_answer(13, 1234, 1.0, '', FORMAT_MOODLE, '');
$this->assertTrue($answer->within_tolerance(1234));
$this->assertFalse($answer->within_tolerance(1234.000001));
$this->assertFalse($answer->within_tolerance(0));
$answer = new qtype_numerical_answer(13, 0, 1.0, '', FORMAT_MOODLE, '');
$this->assertTrue($answer->within_tolerance(0));
$this->assertFalse($answer->within_tolerance(pow(10, -1 * ini_get('precision') + 1)));
$this->assertTrue($answer->within_tolerance(pow(10, -1 * ini_get('precision'))));
}
public function test_within_tolerance_relative() {
$answer = new qtype_numerical_answer(13, 7.0, 1.0, '', FORMAT_MOODLE, 0.1);
$answer->tolerancetype = 1;
$this->assertFalse($answer->within_tolerance(6.29));
$this->assertTrue($answer->within_tolerance(6.3));
$this->assertTrue($answer->within_tolerance(7));
$this->assertTrue($answer->within_tolerance(7.7));
$this->assertFalse($answer->within_tolerance(7.71));
}
public function test_within_tolerance_geometric() {
$answer = new qtype_numerical_answer(13, 7.0, 1.0, '', FORMAT_MOODLE, 1.0);
$answer->tolerancetype = 3;
$this->assertFalse($answer->within_tolerance(3.49));
$this->assertTrue($answer->within_tolerance(3.5));
$this->assertTrue($answer->within_tolerance(7));
$this->assertTrue($answer->within_tolerance(14));
$this->assertFalse($answer->within_tolerance(14.01));
}
}
@@ -0,0 +1,112 @@
<?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/>.
/**
* Unit tests for the numerical question definition class.
*
* @package moodlecore
* @subpackage questiontypes
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
class testable_qtype_numerical_answer_processor extends qtype_numerical_answer_processor {
public function parse_response($response) {
return parent::parse_response($response);
}
}
class qtype_numerical_answer_processor_test extends UnitTestCase {
public function test_parse_response() {
$ap = new testable_qtype_numerical_answer_processor(
array('m' => 1, 'cm' => 0.01), '.', ',');
$this->assertEqual(array('3', '142', '', ''), $ap->parse_response('3.142'));
$this->assertEqual(array('', '2', '', ''), $ap->parse_response('.2'));
$this->assertEqual(array('1', '', '', ''), $ap->parse_response('1.'));
$this->assertEqual(array('1', '0', '', ''), $ap->parse_response('1.0'));
$this->assertEqual(array('-1', '', '', ''), $ap->parse_response('-1.'));
$this->assertEqual(array('+1', '0', '', ''), $ap->parse_response('+1.0'));
$this->assertEqual(array('1', '', '4', ''), $ap->parse_response('1e4'));
$this->assertEqual(array('3', '142', '-4', ''), $ap->parse_response('3.142E-4'));
$this->assertEqual(array('', '2', '+2', ''), $ap->parse_response('.2e+2'));
$this->assertEqual(array('1', '', '-1', ''), $ap->parse_response('1.e-1'));
$this->assertEqual(array('1', '0', '0', ''), $ap->parse_response('1.0e0'));
$this->assertEqual(array('3', '', '8', ''), $ap->parse_response('3x10^8'));
$this->assertEqual(array('3', '', '8', ''), $ap->parse_response('3×10^8'));
$this->assertEqual(array('3', '0', '8', ''), $ap->parse_response('3.0*10^8'));
$this->assertEqual(array('3', '00', '-8', ''), $ap->parse_response('3.00x10**-8'));
$this->assertEqual(array('0', '001', '7', ''), $ap->parse_response('0.001×10**7'));
$this->assertEqual(array('1', '', '', 'm'), $ap->parse_response('1m'));
$this->assertEqual(array('3', '142', '', 'm'), $ap->parse_response('3.142 m'));
$this->assertEqual(array('', '2', '', 'm'), $ap->parse_response('.2m'));
$this->assertEqual(array('1', '', '', 'cm'), $ap->parse_response('1.cm'));
$this->assertEqual(array('1', '0', '', 'cm'), $ap->parse_response('1.0 cm'));
$this->assertEqual(array('-1', '', '', 'm'), $ap->parse_response('-1.m'));
$this->assertEqual(array('+1', '0', '', 'cm'), $ap->parse_response('+1.0cm'));
$this->assertEqual(array('1', '', '4', 'm'), $ap->parse_response('1e4 m'));
$this->assertEqual(array('3', '142', '-4', 'cm'), $ap->parse_response('3.142E-4 cm'));
$this->assertEqual(array('', '2', '+2', 'm'), $ap->parse_response('.2e+2m'));
$this->assertEqual(array('1', '', '-1', 'm'), $ap->parse_response('1.e-1 m'));
$this->assertEqual(array('1', '0', '0', 'cm'), $ap->parse_response('1.0e0cm'));
$this->assertEqual(array('1000000', '', '', ''), $ap->parse_response('1,000,000'));
$this->assertEqual(array('1000', '00', '', 'm'), $ap->parse_response('1,000.00 m'));
$this->assertEqual(array(null, null, null, null), $ap->parse_response('frog'));
$this->assertEqual(array(null, null, null, null), $ap->parse_response('3 frogs'));
$this->assertEqual(array(null, null, null, null), $ap->parse_response('. m'));
$this->assertEqual(array(null, null, null, null), $ap->parse_response('.e8 m'));
$this->assertEqual(array(null, null, null, null), $ap->parse_response(','));
}
public function test_apply_units() {
$ap = new qtype_numerical_answer_processor(
array('m/s' => 1, 'c' => 299792458, 'mph' => 0.44704), '.', ',');
$this->assertEqual(array(3e8, 'm/s'), $ap->apply_units('3x10^8 m/s'));
$this->assertEqual(array(3e8, ''), $ap->apply_units('3x10^8'));
$this->assertEqual(array(299792458, 'c'), $ap->apply_units('1c'));
$this->assertEqual(array(0.44704, 'mph'), $ap->apply_units('0001.000 mph'));
$this->assertEqual(array(null, null), $ap->apply_units('1 frogs'));
$this->assertEqual(array(null, null), $ap->apply_units('. m/s'));
}
public function test_euro_style() {
$ap = new qtype_numerical_answer_processor(array(), ',', ' ');
$this->assertEqual(array(-1000, ''), $ap->apply_units('-1 000'));
$this->assertEqual(array(3.14159, ''), $ap->apply_units('3,14159'));
}
public function test_percent() {
$ap = new qtype_numerical_answer_processor(array('%' => 0.01), '.', ',');
$this->assertEqual(array('0.03', '%'), $ap->apply_units('3%'));
$this->assertEqual(array('1e-8', '%'), $ap->apply_units('1e-6 %'));
$this->assertEqual(array('100', ''), $ap->apply_units('100'));
}
}
@@ -0,0 +1,116 @@
<?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/>.
/**
* Unit tests for the numerical question definition class.
*
* @package qtype_numerical
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once($CFG->dirroot . '/question/engine/simpletest/helpers.php');
/**
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_numerical_question_test extends UnitTestCase {
public function test_is_complete_response() {
$question = test_question_maker::make_a_numerical_question();
$this->assertFalse($question->is_complete_response(array()));
$this->assertTrue($question->is_complete_response(array('answer' => '0')));
$this->assertTrue($question->is_complete_response(array('answer' => 0)));
$this->assertTrue($question->is_complete_response(array('answer' => 'test')));
}
public function test_is_gradable_response() {
$question = test_question_maker::make_a_numerical_question();
$this->assertFalse($question->is_gradable_response(array()));
$this->assertTrue($question->is_gradable_response(array('answer' => '0')));
$this->assertTrue($question->is_gradable_response(array('answer' => 0)));
$this->assertTrue($question->is_gradable_response(array('answer' => 'test')));
}
public function test_grading() {
$question = test_question_maker::make_a_numerical_question();
$this->assertEqual(array(0, question_state::$gradedwrong),
$question->grade_response(array('answer' => '1.0')));
$this->assertEqual(array(1, question_state::$gradedright),
$question->grade_response(array('answer' => '3.14')));
}
public function test_grading_with_units() {
$question = test_question_maker::make_a_numerical_question();
$question->ap = new qtype_numerical_answer_processor(
array('m' => 1, 'cm' => 0.01), '.', ',');
$this->assertEqual(array(0, question_state::$gradedwrong),
$question->grade_response(array('answer' => '3.14 frogs')));
$this->assertEqual(array(1, question_state::$gradedright),
$question->grade_response(array('answer' => '3.14')));
$this->assertEqual(array(1, question_state::$gradedright),
$question->grade_response(array('answer' => '3.14 m')));
$this->assertEqual(array(1, question_state::$gradedright),
$question->grade_response(array('answer' => '314cm')));
$this->assertEqual(array(1, question_state::$gradedright),
$question->grade_response(array('answer' => '314000000x10^-8m')));
}
public function test_get_correct_response() {
$question = test_question_maker::make_a_numerical_question();
$this->assertEqual(array('answer' => '3.14'),
$question->get_correct_response());
}
public function test_get_question_summary() {
$num = test_question_maker::make_a_numerical_question();
$qsummary = $num->get_question_summary();
$this->assertEqual('What is pi to two d.p.?', $qsummary);
}
public function test_summarise_response() {
$num = test_question_maker::make_a_numerical_question();
$summary = $num->summarise_response(array('answer' => '3.1'));
$this->assertEqual('3.1', $summary);
}
public function test_classify_response() {
$num = test_question_maker::make_a_numerical_question();
$num->start_attempt(new question_attempt_step());
$this->assertEqual(array(
new question_classified_response(15, '3.1', 0.0)),
$num->classify_response(array('answer' => '3.1')));
$this->assertEqual(array(
new question_classified_response(17, '42', 0.0)),
$num->classify_response(array('answer' => '42')));
$this->assertEqual(array(
new question_classified_response(13, '3.14', 1.0)),
$num->classify_response(array('answer' => '3.14')));
$this->assertEqual(array(
question_classified_response::no_response()),
$num->classify_response(array('answer' => '')));
}
}
@@ -52,16 +52,20 @@ class qtype_numerical_test extends UnitTestCase {
protected function get_test_question_data() {
$q = new stdClass;
$q->id = 1;
$q->options->answers[1] = (object) array(
$q->options->answers[13] = (object) array(
'id' => 13,
'answer' => 42,
'fraction' => 1,
'feedback' => 'yes',
'feedbackformat' => FORMAT_MOODLE,
'tolerance' => 0.5
);
$q->options->answers[2] = (object) array(
$q->options->answers[14] = (object) array(
'id' => 14,
'answer' => '*',
'fraction' => 0.1,
'feedback' => 'no',
'feedbackformat' => FORMAT_MOODLE,
'tolerance' => ''
);
@@ -91,8 +95,8 @@ class qtype_numerical_test extends UnitTestCase {
$this->assertEqual(array(
$q->id => array(
1 => new question_possible_response('42 m (41.5..42.5)', 1),
2 => new question_possible_response('*', 0.1),
13 => new question_possible_response('42 m (41.5..42.5)', 1),
14 => new question_possible_response('*', 0.1),
null => question_possible_response::no_response()),
), $this->qtype->get_possible_responses($q));
}