diff --git a/classes/local/evaluator.php b/classes/local/evaluator.php index c2247829..ff674704 100644 --- a/classes/local/evaluator.php +++ b/classes/local/evaluator.php @@ -163,9 +163,9 @@ public function substitute_variables_in_text(string $text, bool $skiplists = tru if ($skiplists && in_array($result->type, [token::LIST, token::SET])) { continue; } - // If the result is a number, we try to localize it, unless the admin settings do not + // If the result is a number or a numeric string, we try to localize it, unless the admin settings do not // allow the decimal comma. - if ($result->type === token::NUMBER) { + if ($result->type === token::NUMBER || ($result->type === token::STRING && is_numeric($result->value))) { $result = qtype_formulas::format_float($result->value); } diff --git a/classes/local/functions.php b/classes/local/functions.php index 487961fe..52885da0 100644 --- a/classes/local/functions.php +++ b/classes/local/functions.php @@ -636,6 +636,7 @@ public static function sigfig($number, $precision): string { get_string('error_func_second_posint', 'qtype_formulas', 'sigfig()'), self::POSITIVE | self::INTEGER ); + $originalnumber = $number; $number = floatval($number); $precision = intval($precision); @@ -657,7 +658,22 @@ public static function sigfig($number, $precision): string { // We only request decimals if $digitsafter is greater than zero. $digitsafter = max(0, $digitsafter); - return number_format($number, $digitsafter, '.', ''); + // If the absolute value of the *original* number is >= 1e-4 and < 1e14, we can format + // it and return a string. The same is true if the rounded number is zero. + if ($number == 0 || (abs($originalnumber) >= 1e-4 && abs($originalnumber) < 1e14)) { + return number_format($number, $digitsafter, '.', ''); + } + + // If the absolute value of the original number was below 1e-4, but just reached that + // limit after rounding, it should be output as 0.0001, plus possibly some trailing zeroes. + if (abs($originalnumber) < 1e-4 && abs($number) == 1e-4) { + return sprintf("%.{$precision}H", $number); + } + + // For all other cases, we force scientific notation. Note that we have to subtract 1 + // from the requested precision, because with the E format, the parameter indicates + // the number of places *after* the decimal comma. + return sprintf('%.' . ($precision - 1) . 'E', $number); } /** diff --git a/tests/evaluator_test.php b/tests/evaluator_test.php index 92e64d30..d972f547 100644 --- a/tests/evaluator_test.php +++ b/tests/evaluator_test.php @@ -1348,6 +1348,33 @@ public function test_substitute_variables_in_text(): void { self::assertEquals($expected, $output); } + public function test_substitute_variables_in_text_with_sigfig(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Define, parse and evaluate some variables. + $vars = 'a=0.3; b=sigfig(a, 2); c=0.000005; d=sigfig(c, 8); e=fact(19); f=sigfig(e, 3)'; + $parser = new parser($vars); + $statements = $parser->get_statements(); + $evaluator = new evaluator(); + $evaluator->evaluate($statements); + + // We only test the correct replacement of the numbers; correct interpretation of + // placeholders is covered in another test. + $text = '{a} -- {b} -- {c} -- {d} -- {e} -- {f}'; + $output = $evaluator->substitute_variables_in_text($text); + $expected = '0.3 -- 0.30 -- 5.0E-6 -- 5.0000000E-6 -- 1.2164510040883E+17 -- 1.22E+17'; + self::assertEquals($expected, $output); + + // Setting the localised decimal separator, but disallow the decimal comma in the admin settings. + qtype_formulas_test_helper::define_local_decimal_separator(); + set_config('allowdecimalcomma', 1, 'qtype_formulas'); + self::assertEquals('1', get_config('qtype_formulas', 'allowdecimalcomma')); + $output = $evaluator->substitute_variables_in_text($text); + $expected = '0,3 -- 0,30 -- 5,0E-6 -- 5,0000000E-6 -- 1,2164510040883E+17 -- 1,22E+17'; + self::assertEquals($expected, $output); + } + public function test_substitute_variables_in_algebraic_formula(): void { // Define, parse and evaluate some variables. $vars = 'a=1; b=[2,3,4]; c={1,2,3}; x={1:10}; y={1:10}; k = [[1,2],[3,4]];'; diff --git a/tests/functions_test.php b/tests/functions_test.php index 1e305a51..8c74b01a 100644 --- a/tests/functions_test.php +++ b/tests/functions_test.php @@ -627,6 +627,49 @@ public static function provide_sigfig_expressions(): array { ['-0.005', 'sigfig(-.005, 1)'], ['-0.0050', 'sigfig(-.005, 2)'], ['-0.00500', 'sigfig(-.005, 3)'], + + ['9.99E-5', 'sigfig(0.0000999, 3)'], + ['9.990E-5', 'sigfig(0.0000999, 4)'], + ['1.00E-5', 'sigfig(0.0000100, 3)'], + ['1.000E-5', 'sigfig(0.0000100, 4)'], + ['-9.99E-5', 'sigfig(-0.0000999, 3)'], + ['-9.990E-5', 'sigfig(-0.0000999, 4)'], + ['-1.00E-5', 'sigfig(-0.0000100, 3)'], + ['-1.000E-5', 'sigfig(-0.0000100, 4)'], + ['1.23E-6', 'sigfig(0.000001234, 3)'], + ['1.2340E-6', 'sigfig(0.000001234, 5)'], + ['-1.23E-5', 'sigfig(-0.0000123, 3)'], + ['-1.230E-5', 'sigfig(-0.0000123, 4)'], + + ['0.000100', 'sigfig(0.0001, 3)'], + ['0.0001000', 'sigfig(0.0001, 4)'], + ['0.0001', 'sigfig(0.00009995, 3)'], + ['9.995E-5', 'sigfig(0.00009995, 4)'], + ['-0.000100', 'sigfig(-0.0001, 3)'], + ['-0.0001000', 'sigfig(-0.0001, 4)'], + ['-0.0001', 'sigfig(-0.00009995, 3)'], + ['-9.995E-5', 'sigfig(-0.00009995, 4)'], + + ['1.00E+14', 'sigfig(1e14, 3)'], + ['1.0000E+14', 'sigfig(1e14, 5)'], + ['1.23E+14', 'sigfig(1.23e14, 3)'], + ['1.2300E+14', 'sigfig(1.23e14, 5)'], + ['1.20E+14', 'sigfig(1.2e14, 3)'], + ['9.99E+14', 'sigfig(9.99e14, 3)'], + ['-1.230E+14', 'sigfig(-1.23e14, 4)'], + + ['100000000000000', 'sigfig(99999999999999, 3)'], + ['100000000000000', 'sigfig(99999999999999, 5)'], + ['99900000000000', 'sigfig(99900000000000, 3)'], + ['-100000000000000', 'sigfig(-99999999999999, 3)'], + ['-100000000000000', 'sigfig(-99999999999999, 5)'], + ['-99900000000000', 'sigfig(-99900000000000, 3)'], + + ['0.300', 'sigfig(0.3, 3)'], + ['123.00', 'sigfig(123, 5)'], + ['99999.0', 'sigfig(99999, 6)'], + ['-0.00123', 'sigfig(-0.00123, 3)'], + ['0.00', 'sigfig(0, 3)'], ]; } diff --git a/tests/renderer_test.php b/tests/renderer_test.php index 82a52e6b..e9ac96e8 100644 --- a/tests/renderer_test.php +++ b/tests/renderer_test.php @@ -336,6 +336,32 @@ public function test_substitution_of_local_variables(): void { $this->check_output_does_not_contain_stray_placeholders(); } + public function test_right_answer_feedback_uses_appropriate_decimal_separator(): void { + // Setting the localised decimal separator, but disallow the decimal comma in the admin settings. + qtype_formulas_test_helper::define_local_decimal_separator(); + self::assertEquals('0', get_config('qtype_formulas', 'allowdecimalcomma')); + + $q = $this->get_test_formulas_question('testsinglenum'); + $q->parts[0]->answer = '3.5'; + + $this->start_attempt_at_question($q, 'immediatefeedback', 1); + $this->process_submission(['0_0' => '42', '-submit' => 1]); + $this->check_output_contains_lang_string('correctansweris', 'qtype_formulas', '3.5'); + + // Now allowing the decimal comma to be used. + set_config('allowdecimalcomma', 1, 'qtype_formulas'); + $this->start_attempt_at_question($q, 'immediatefeedback', 1); + $this->process_submission(['0_0' => '42', '-submit' => 1]); + $this->check_output_contains_lang_string('correctansweris', 'qtype_formulas', '3,5'); + + // Make sure the decimal comma is also applied for numbers that are string tokens. + $q = $this->get_test_formulas_question('testsinglenum'); + $q->parts[0]->answer = 'sigfig(3.5, 3)'; + $this->start_attempt_at_question($q, 'immediatefeedback', 1); + $this->process_submission(['0_0' => '42', '-submit' => 1]); + $this->check_output_contains_lang_string('correctansweris', 'qtype_formulas', '3,50'); + } + public function test_render_question_with_separate_unit_field(): void { $q = $this->get_test_formulas_question('testsinglenumunitsep'); $q->parts[0]->unitpenalty = 0.5;