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() {