Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Question Engine 2:Numerical formats

From MoodleDocs

The Question Engine 2 structure allows implementation of new features for numerical question types ( numerical and calculated).

This page describes a possible implementation and its rationale.

The text should be readed as a personal summary of the work in progress and not as a textbook on computer language and real numbers representation.

It is related to an initial quiz forum discussion http://moodle.org/mod/forum/discuss.php?d=172211#p755927

Please put your comments on the forum. http://moodle.org/mod/forum/discuss.php?d=174387

See also http://tracker.moodle.org/browse/MDL-27363


Numbers as written by human and readed by computer language

The main feature of numerical question type is to ask the student to give a numerical answer i.e. a number. Most often this means a numerical value that is not an integer ( dates are a current example of integer value response) but a real number which value is expressed most often as a decimal number i.e 1.234 .

Computer languages ( i.e PHP used in Moodle) store real numbers in a different way than human do (decimal part and exponent similar to 1.234 E00) and humans do not expressed real nmumbers in an universal format.

The separator between the unit and the decimal fraction is often either a . or a ,  
1.234   1,234

Furthermore to help reading large numbers, most language add another separator for thousands often space or , if it is not used already as unit separator.

123 456.78     123,456.78   123 456,78 

PHP as a computer language use space to separate the language components so cannot use space as a thousand separator.

, is also used to separate variables so PHP use a simpler syntax 123456.78.

As this is stored in the computer as 2 parts (number and exponent).

1.2345678+E05 will be a structure that is well recognized by a computer language as PHP.

Setting the answer when creating the question

In Moodle 1,9 and 2,0 in the edit_numerical_form.php, the number enter by the teacher (although student could be allowed to create question, I will use teacher for text clarity) must be conform to the PHP syntax (no thousand separator or space and . as decimal separator. E syntax is allowed.

                if (!(is_numeric($trimmedanswer) || $trimmedanswer == '*')) {
                   $errors['answer[' . $key . ']'] =
                           get_string('answermustbenumberorstar', 'qtype_numerical');
               }

So the teacher must know the PHP specific number syntax.

In the numerical/questiontype.php function save_question_options($question) there is an additional verification mostly for numerical questions imported through various formats or inside a Cloze numerical multianswer question.

            
               $answer->answer = $this->apply_unit($answerdata, $units);
               if ($answer->answer === false) {
                   $result->notice = get_string('invalidnumericanswer', 'quiz');
               }
   

the Moodle 2,0 apply_unit

   
   /**
    * Checks if the $rawresponse has a unit and applys it if appropriate.
    *
    * @param string $rawresponse  The response string to be converted to a float.
    * @param array $units         An array with the defined units, where the
    *                             unit is the key and the multiplier the value.
    * @return float               The rawresponse with the unit taken into
    *                             account as a float.
    */
   function apply_unit($rawresponse, $units) {
       // Make units more useful
       $tmpunits = array();
       foreach ($units as $unit) {
           $tmpunits[$unit->unit] = $unit->multiplier;
       }
       // remove spaces and normalise decimal places.
       $rawresponse = trim($rawresponse) ;
       $search  = array(' ', ',');
       // test if a . is present or there are multiple , (i.e. 2,456,789 ) so that we don't need spaces and ,
       if ( strpos($rawresponse,'.' ) !== false || substr_count($rawresponse,',') > 1 ) {
           $replace = array(, );
       }else { // remove spaces and normalise , to a . .
           $replace = array(, '.');
       }
       $rawresponse = str_replace($search, $replace, $rawresponse);


       // Apply any unit that is present.
       if (ereg('^([+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$',
               $rawresponse, $responseparts)) {

echo"

responseparts

";print_r($responseparts) ;echo"

";

           if (!empty($responseparts[5])) {
               if (isset($tmpunits[$responseparts[5]])) {
                   // Valid number with unit.
                   return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
               } else {
                   // Valid number with invalid unit. Must be wrong.
                   return false;
               }
           } else {
               // Valid number without unit.
               return (float)$responseparts[1];
           }
       }
       // Invalid number. Must be wrong.
       return false;
   }

The 2,0 apply_unit allows more number formats than the test in the editing form.

  • regular numbers 13500.67 : 13 500.67 : 13500,67: 13 500,67
  • if you use , as thousand separator *always* put the decimal . as in 13,500.67 : 13,500.
  • for exponent form, say 1.350067 * 104, use 1.350067 E4 : 1.350067 E04 ';

The 1,9 apply_unit is more restrictive as it does not support , as decimal separator.

More formats were allowed in 2,0 as the main objective in a numerical question is the numerical value. More about this further in the page (todo)

Retrieving the numerical value from the student response

As the function apply_unit() is also used to analyze the student response the number formats allowed are the same as the formats allowed for NON edit_numerical_form.php numerical i.e. import or Cloze.

However to help students know what are the number formats allowed an help icon aside the number text input element is shown on Moodle 2,0 numerical questions created either by edit_numerical_form.php or import but not for Cloze.

Numerical question grading

There are 3 elements that can be graded in a numerical question

  1. the numeric value
  2. the unit used in relation to the numeric value
  3. the number format used to express the value

1 and 2 are addressed in Moodle 2,0 for questions created from edit_numerical_form.php.

3 can give a 0 grade if the student does not use one of the number formats allowed but there is no specific grading designed as the unit penalty since Moodle 2,0.

Designing a number format penalty

We have already 2 ways to grade the student response available in the edit_numerical_form.php. i.e.

  • the answer-tolerance combination
  • the detailed unit grading

We need flexibility to take in account that number format can vary even at a same location i.e. in Canada there are two locales as there are two official languages (french and english).

Teacher sometimes ask for specific number formats as fraction that cannot be allowed simultanuously to other grading.

The most universal solution is to associate a grade to a specific answer numerical format as the tolerance is used.

For example in calculated the tolerance can be set as relative or absolute.

So my proposal is to add the number format as a new answer parameter.

To an official locales list of available formats we could add

  • specific Moodle formats as the one used for 1,9 2,0 ,
  • a fraction format,
  • time format,
  • degree, minute, second
  • etc.

Feasability of adding number format as an additional numerical answer table

The answer field being set as TEXT can easily store a numerical value in any format that can be validated easily in edit_numerical_form.php and the


   /**
    * Get an answer that contains the feedback and fraction that should be
    * awarded for this resonse.
    * @param number $value the numerical value of a response.
    * @return question_answer the matching answer.
    */
   public function get_matching_answer($value) {
       foreach ($this->answers as $aid => $answer) {
           if ($answer->within_tolerance($value)) {
               $answer->id = $aid;
               return $answer;
           }
       }
       return null;
   }

modified accordingly.

If the 2,1 new engine initial code uses an equivalent to the Moodle 2,0 apply_unit() function, this improvement could wait after 2,1 release.

The actual code does not detect number format

The historical objective in the apply_unit() was to convert various number formats so that they comply to the PHP norm for a number.

The most current thousand separators( i.e space and ,) are filtered out.

In 2,0 the , as unit separator is replaced by .

In all cases PHP numerical format is valid i.e. 123456.78 or 1.2345678e5

A clever student will never use thousand separators and will learn once for all if he can use , or . as unit separator.

Shortanswer is a better way to test for number formats...

Converting number in a string to a double or float variable

The answer is being stored in a TEXT database field or in a string in import-export files.

However in numerical questiontype code it is used a numeric PHP parameter (float or double). From http://ca2.php.net/manual/en/language.types.string.php#language.types.string.conversion

String conversion to numbers

When a string is evaluated in a numeric context, the resulting value and type are determined as follows.

If the string *does not contain* any of the characters '.', 'e', or 'E' and the numeric value fits into integer type limits (as defined by PHP_INT_MAX), the string will be evaluated as an integer. In all other cases it will be evaluated as a float.

The value is given by the initial portion of the string. If the string starts with valid numeric data, this will be the value used. Otherwise, the value will be 0 (zero). Valid numeric data is an optional sign, followed by one or more digits (optionally containing a decimal point), followed by an optional exponent. The exponent is an 'e' or 'E' followed by one or more digits.

For more information on this conversion, see the Unix manual page for strtod(3).


Unix manual page for strtod

http://compute.cnr.berkeley.edu/cgi-bin/man-cgi?strtod+3 DESCRIPTION

    The strtod(), strtof(), and strtold() functions convert  the
    initial  portion of the string pointed to by nptr to double,
    float, and long double representation,  respectively.  First
    they decompose the input string into three parts:
    1.  An initial,  possibly  empty,  sequence  of  white-space
        characters (as specified by isspace(3C))
    2.  A subject sequence interpreted as a floating-point  con-
        stant or representing infinity or NaN
    3.  A final string of one or more  unrecognized  characters,
        including the terminating null byte of the input string.
    Then they attempt to  convert  the  subject  sequence  to  a
    floating-point number, and return the result.
    The expected form of the subject  sequence  is  an  optional
    plus or minus sign, then one of the following:
      o  A non-empty sequence of digits optionally containing  a
         radix character, then an optional exponent part
      o  A 0x or 0X, then a non-empty  sequence  of  hexadecimal
         digits optionally containing a radix character, then an
         optional binary exponent part
      o  One of INF or INFINITY, ignoring case

...

    The radix character  is  defined  in  the  program's  locale
    (category  LC_NUMERIC).  In the POSIX locale, or in a locale
    where the radix character is not defined, the radix  charac-
    ter defaults to a period ('.').

So if the locale LC_NUMERIC i.e. Array (

   [decimal_point] => .
   [thousands_sep] => 
   [int_curr_symbol] => 
   [currency_symbol] => 
   [mon_decimal_point] => 
   [mon_thousands_sep] => 
   [positive_sign] => 
   [negative_sign] => 
   [int_frac_digits] => 127
   [frac_digits] => 127
   [p_cs_precedes] => 127
   [p_sep_by_space] => 127
   [n_cs_precedes] => 127
   [n_sep_by_space] => 127
   [p_sign_posn] => 127
   [n_sign_posn] => 127
   [grouping] => Array
       (
       )
   [mon_grouping] => Array
       (
       )
)

define decimal_point not as . as here but as , then 1,234.56 will be recognized as 1,234 i.e. smaller than 2 although the number written is greater than 1000.

PHP 5.36 zend_strtod()

However in PHP 5.3.6 strtod() is replaced by zend_strtod()

ZEND_API double zend_strtod (CONST char *s00, char **se)
{

int bb2, bb5, bbe, bd2, bd5, bbbits, bs2, c, dsign, e, e1, esign, i, j, k, nd, nd0, nf, nz, nz0, sign; CONST char *s, *s0, *s1; volatile double aadj, aadj1, adj; volatile _double rv, rv0; Long L; ULong y, z; Bigint *bb, *bb1, *bd, *bd0, *bs, *delta, *tmp; double result;

CONST char decimal_point = '.';

  ...

z = 10*z + c - '0'; nd0 = nd; if (c == decimal_point) { c = *++s; if (!nd) { for(; c == '0'; c = *++s) nz++; if (c > '0' && c <= '9') { s0 = s; nf += nz; nz = 0; goto have_dig; }


So in PHP 5.3.6 the zend_strtod() ALWAYS use . as decimal_point

Other functions allow the user to define the decimal_point or use the locale defined decimal_point.


Implementing a number format analysis

to do ...


Pierre Pichet 22:13, 2 May 2011 (WST)