From ff43c30f0aeaa66afa9cafe27788de309ff3dedb Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 20 Aug 2012 12:46:32 +0100 Subject: [PATCH] MDL-31837 numerical tolerance: better handling of very small tolerances. The changes between Moodle 1.9 and 2.1 made the marking of very small answers like 10^-20 almost impossible. This change fixes it. This fix is almost entirely due the the careful research of Pierre Pichet, who carefully testing various proposals, and worked out that this one seemed best. --- question/type/numerical/question.php | 8 ++-- question/type/numerical/tests/answer_test.php | 42 ++++++++++++++++--- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/question/type/numerical/question.php b/question/type/numerical/question.php index 93d276af9ce..110cfdc02a9 100644 --- a/question/type/numerical/question.php +++ b/question/type/numerical/question.php @@ -320,10 +320,13 @@ class qtype_numerical_answer extends question_answer { throw new coding_exception('Cannot work out tolerance interval for answer *.'); } + // Smallest number that, when added to 1, is different from 1. + $epsilon = pow(10, -1 * ini_get('precision')); + // We need to add a tiny fraction depending on the set precision to make // the comparison work correctly, otherwise seemingly equal values can // yield false. See MDL-3225. - $tolerance = (float) $this->tolerance + pow(10, -1 * ini_get('precision')); + $tolerance = abs($this->tolerance) + $epsilon; switch ($this->tolerancetype) { case 1: case 'relative': @@ -331,8 +334,7 @@ class qtype_numerical_answer extends question_answer { return array($this->answer - $range, $this->answer + $range); case 2: case 'nominal': - $tolerance = $this->tolerance + pow(10, -1 * ini_get('precision')) * - max(1, abs($this->answer)); + $tolerance = $this->tolerance + $epsilon * max(abs($this->tolerance), abs($this->answer), $epsilon); return array($this->answer - $tolerance, $this->answer + $tolerance); case 3: case 'geometric': diff --git a/question/type/numerical/tests/answer_test.php b/question/type/numerical/tests/answer_test.php index c3d276ae641..a74fed3c924 100644 --- a/question/type/numerical/tests/answer_test.php +++ b/question/type/numerical/tests/answer_test.php @@ -25,8 +25,10 @@ */ global $CFG; +require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); require_once($CFG->dirroot . '/question/type/numerical/question.php'); + class qtype_numerical_answer_test extends advanced_testcase { public function test_within_tolerance_nominal() { $answer = new qtype_numerical_answer(13, 7.0, 1.0, '', FORMAT_MOODLE, 1.0); @@ -38,16 +40,46 @@ class qtype_numerical_answer_test extends advanced_testcase { $this->assertFalse($answer->within_tolerance(8.01)); } + public function test_within_tolerance_nominal_zero() { + // Either an answer or tolerance of 0 requires special care. We still + // don't want to end up comparing two floats for absolute equality. + + // Zero tol, non-zero answer. + $answer = new qtype_numerical_answer(13, 1e-20, 1.0, '', FORMAT_MOODLE, 0.0); + $this->assertFalse($answer->within_tolerance(0.9999999e-20)); + $this->assertTrue($answer->within_tolerance(1e-20)); + $this->assertFalse($answer->within_tolerance(1.0000001e-20)); + + // Non-zero tol, zero answer. + $answer = new qtype_numerical_answer(13, 0.0, 1.0, '', FORMAT_MOODLE, 1e-24); + $this->assertFalse($answer->within_tolerance(-2e-24)); + $this->assertTrue($answer->within_tolerance(-1e-24)); + $this->assertTrue($answer->within_tolerance(0)); + $this->assertTrue($answer->within_tolerance(1e-24)); + $this->assertFalse($answer->within_tolerance(2e-24)); + + // Zero tol, zero answer. + $answer = new qtype_numerical_answer(13, 0.0, 1.0, '', FORMAT_MOODLE, 1e-24); + $this->assertFalse($answer->within_tolerance(-1e-20)); + $this->assertTrue($answer->within_tolerance(-1e-35)); + $this->assertTrue($answer->within_tolerance(0)); + $this->assertTrue($answer->within_tolerance(1e-35)); + $this->assertFalse($answer->within_tolerance(1e-20)); + + // Non-zero tol, non-zero answer. + $answer = new qtype_numerical_answer(13, 1e-20, 1.0, '', FORMAT_MOODLE, 1e-24); + $this->assertFalse($answer->within_tolerance(1.0002e-20)); + $this->assertTrue($answer->within_tolerance(1.0001e-20)); + $this->assertTrue($answer->within_tolerance(1e-20)); + $this->assertTrue($answer->within_tolerance(1.0001e-20)); + $this->assertFalse($answer->within_tolerance(1.0002e-20)); + } + 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() {