From 0ff173d3f2e41c359ca7dd576b0e20e4d071bcf7 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 21 Mar 2011 17:55:02 +0000 Subject: [PATCH] MDL-20636 Fix numerical unit tests. --- question/type/numerical/questiontype.php | 171 ++++++++++++++++++ .../type/numerical/simpletest/testanswer.php | 74 ++++++++ .../simpletest/testanswerprocessor.php | 112 ++++++++++++ .../numerical/simpletest/testquestion.php | 116 ++++++++++++ .../numerical/simpletest/testquestiontype.php | 12 +- 5 files changed, 481 insertions(+), 4 deletions(-) create mode 100644 question/type/numerical/simpletest/testanswer.php create mode 100644 question/type/numerical/simpletest/testanswerprocessor.php create mode 100644 question/type/numerical/simpletest/testquestion.php diff --git a/question/type/numerical/questiontype.php b/question/type/numerical/questiontype.php index 4580e9202a1..9027b77b686 100644 --- a/question/type/numerical/questiontype.php +++ b/question/type/numerical/questiontype.php @@ -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); + } +} diff --git a/question/type/numerical/simpletest/testanswer.php b/question/type/numerical/simpletest/testanswer.php new file mode 100644 index 00000000000..efc79d2f84a --- /dev/null +++ b/question/type/numerical/simpletest/testanswer.php @@ -0,0 +1,74 @@ +. + + +/** + * 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)); + } +} diff --git a/question/type/numerical/simpletest/testanswerprocessor.php b/question/type/numerical/simpletest/testanswerprocessor.php new file mode 100644 index 00000000000..1a57e07be35 --- /dev/null +++ b/question/type/numerical/simpletest/testanswerprocessor.php @@ -0,0 +1,112 @@ +. + + +/** + * 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')); + } + +} diff --git a/question/type/numerical/simpletest/testquestion.php b/question/type/numerical/simpletest/testquestion.php new file mode 100644 index 00000000000..1bda5d3518d --- /dev/null +++ b/question/type/numerical/simpletest/testquestion.php @@ -0,0 +1,116 @@ +. + + +/** + * 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' => ''))); + } +} diff --git a/question/type/numerical/simpletest/testquestiontype.php b/question/type/numerical/simpletest/testquestiontype.php index 2bc0f96449c..341f061eeba 100644 --- a/question/type/numerical/simpletest/testquestiontype.php +++ b/question/type/numerical/simpletest/testquestiontype.php @@ -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)); }