moodle-qtype_formulas icon indicating copy to clipboard operation
moodle-qtype_formulas copied to clipboard

Function to determine number of significant figures (e.g. in answer)

Open FloMiLe opened this issue 3 years ago • 5 comments

Description of proposed feature

I think it would be helpful to have a function that returns the number of significant figures of some given number, e.g. an answer. This should probably be done after converting to a string in order to catch trailing zeros.

As significant figures convey meaning in physics, I would like to attribute points for rounding numeric answers to a meaningful number of significant figures.

How can the new feature be used?

Say the function was called numsigfig, then it should be used as:

numsigfig(1234) => 4
numsigfig(1.234) => 4
numsigfig(0200) => 3
numsigfig(0.200) => 3

Additional comments

I currently use the following code to determine the number of significant figures nSigFigAns in an answer:

origAns = _0;

expAns = floor( log10(abs(origAns)));
mantAns = origAns / ( 10**( expAns ));
nSigFigAns = 1;
for (i:[0:6]) {
    shiftedNum = round( 10**(i) * abs(mantAns) , 6 );
    divTest = round( shiftedNum - floor(shiftedNum) , 6);
    nSigFigAns = ( divTest > 1e-5 ) ? nSigFigAns+1 : nSigFigAns ;
}

I don't think, it would be doable using Formula's current string functions.

FloMiLe avatar Nov 29 '22 14:11 FloMiLe

I would like to suggest the following function to be added to the source code:

/**
 * Calculates the number of significant figures in a numerical string.
 * Handles standard notation and scientific notation (e.g., 1.23E4).
 * Preserves trailing zeros after a decimal point.
 *
 * @param mixed $x The number as a string or number type. Will be cast to string.
 * @return int The number of significant figures. Returns 1 for zero by convention.
 */
function get_sigfig($x): int
{
    // Cast the input to a string to preserve original formatting (especially trailing zeros).
    $num_str = (string) $x;

    // 1. Handle Scientific Notation
    // Look for 'e' or 'E'. The exponent part does NOT affect the number of sig figs.
    $parts = explode('e', str_ireplace('E', 'e', $num_str)); // Use str_ireplace for case-insensitivity
    $mantissa_str = $parts[0];
    // $exponent_str = isset($parts[1]) ? $parts[1] : null; // Exponent is ignored for sig figs

    // 2. Pre-process the Mantissa String
    // Remove leading/trailing whitespace.
    $mantissa_str = trim($mantissa_str);

    // Remove leading plus/minus signs, as they are not significant figures.
    if (substr($mantissa_str, 0, 1) === '+' || substr($mantissa_str, 0, 1) === '-') {
        $mantissa_str = substr($mantissa_str, 1);
    }

    // Check if the mantissa contains a decimal point. This affects trailing zero significance.
    $decimal_found = strpos($mantissa_str, '.') !== false;

    // Create a string containing only the digits for easier processing.
    $digits_only = str_replace('.', '', $mantissa_str);

    // 3. Handle the case where the number is zero (or effectively zero like 0.000)
    // If the digits-only string is all zeros after trimming, it's zero.
    if (trim($digits_only, '0') === '') {
        return 1; // By convention, zero has 1 significant figure.
    }

    // 4. Find the first and last non-zero digits in the digits-only string
    // strspn counts the length of the initial segment that consists of characters from '0'.
    $first_non_zero_index = strspn($digits_only, '0');

    // Find the index of the last non-zero digit by searching backwards.
    $last_non_zero_index = strlen($digits_only) - 1;
    while ($last_non_zero_index > $first_non_zero_index && $digits_only[$last_non_zero_index] === '0') {
        $last_non_zero_index--;
    }

    // 5. Calculate the number of significant figures
    if ($decimal_found) {
        // If a decimal point is present, all digits from the first non-zero digit
        // to the very end of the digits-only string are significant.
        // This includes trailing zeros after the last non-zero digit.
        $sig_figs = strlen($digits_only) - $first_non_zero_index;
    } else {
        // If no decimal point is present, only the digits from the first non-zero
        // digit up to the last non-zero digit are considered significant.
        $sig_figs = $last_non_zero_index - $first_non_zero_index + 1;
    }

    return $sig_figs;
}

It works in all of the following test cases:

echo "15.440E3: " . get_sigfig("15.440E3") . " = 5 ?<br>";
echo "15.440: " . get_sigfig("15.440") . " = 5 ?<br>";
echo "15440: " . get_sigfig("15440") . " = 4 ?<br>";
echo "15440.0: " . get_sigfig("15440.0") . " = 6 ?<br>";
echo "0.00123: " . get_sigfig("0.00123") . " = 3 ? (leading zeros are not significant)<br>";
echo "0.00120: " . get_sigfig("0.00120") . " = 3 ? (trailing zero after decimal is significant)<br>";
echo "101: " . get_sigfig("101") . " = 3 ? (zero between non-zeros is significant)<br>";
echo "100: " . get_sigfig("100") . " = 1 ? (trailing zeros without decimal not significant)<br>";
echo "100.: " . get_sigfig("100.") . " = 3 ? (trailing zeros with decimal are significant)<br>";
echo "1.00: " . get_sigfig("1.00") . " = 3 ?<br>";
echo "1.0E1: " . get_sigfig("1.0E1") . " = 2 ? (from 1.0)<br>";
echo "10: " . get_sigfig("10") . " = 1 ?<br>";
echo "0: " . get_sigfig("0") . " = 1 ? (convention)<br>";
echo "0.0: " . get_sigfig("0.0") . " = 1 ? (convention)<br>";
echo "-5.2: " . get_sigfig("-5.2") . " = 2 ? (sign ignored)<br>";
echo "+123: " . get_sigfig("+123") . " = 3 ? (sign ignored)<br>";
echo "12300000000: " . get_sigfig("12300000000") . " = 3 ?<br>";

FloMiLe avatar May 03 '25 15:05 FloMiLe

Thanks @FloMiLe

I will take care of that, and I hope to be able to do it sooner rather than later. I will keep you posted.

PhilippImhof avatar May 03 '25 15:05 PhilippImhof