[system] / trunk / webwork / system / courseScripts / PGanswermacros.pl Repository:
ViewVC logotype

View of /trunk/webwork/system/courseScripts/PGanswermacros.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 274 - (download) (as text) (annotate)
Mon May 13 18:40:56 2002 UTC (11 years, 1 month ago) by gage
File size: 144120 byte(s)
Modified a problem involving the option "tolerance" in num_cmp.

    1 #!/usr/local/bin/webwork-perl
    2 
    3 # This file is PGanswermacros.pl
    4 # This includes the subroutines for the ANS macros, that
    5 # is, macros allowing a more flexible answer checking
    6 ####################################################################
    7 # Copyright @ 1995-2000 University of Rochester
    8 # All Rights Reserved
    9 ####################################################################
   10 #$Id$
   11 
   12 =head1 NAME
   13 
   14   PGanswermacros.pl -- located in the courseScripts directory
   15 
   16 =head1 SYNPOSIS
   17 
   18   Number Answer Evaluators:
   19     num_cmp() --  uses an input hash to determine parameters
   20 
   21     std_num_cmp(), std_num_cmp_list(), std_num_cmp_abs, std_num_cmp_abs_list()
   22     frac_num_cmp(), frac_num_cmp_list(), frac_num_cmp_abs, frac_num_cmp_abs_list()
   23     arith_num_cmp(), arith_num_cmp_list(), arith_num_cmp_abs, arith_num_cmp_abs_list()
   24     strict_num_cmp(), strict_num_cmp_list(), strict_num_cmp_abs, strict_num_cmp_abs_list()
   25     numerical_compare_with_units()  --  requires units as part of the answer
   26     std_num_str_cmp() --  also accepts a set of strings as possible answers
   27 
   28   Function Answer Evaluators:
   29     fun_cmp() --  uses an input hash to determine parameters
   30 
   31     function_cmp(), function_cmp_abs()
   32     function_cmp_up_to_constant(), function_cmp_up_to_constant_abs()
   33     multivar_function_cmp()
   34 
   35   String Answer Evaluators:
   36     str_cmp() --  uses an input hash to determine parameters
   37 
   38     std_str_cmp(), std_str_cmp_list(), std_cs_str_cmp(), std_cs_str_cmp_list()
   39     strict_str_cmp(), strict_str_cmp_list()
   40     ordered_str_cmp(), ordered_str_cmp_list(), ordered_cs_str_cmp(), ordered_cs_str_cmp_list()
   41     unordered_str_cmp(), unordered_str_cmp_list(), unordered_cs_str_cmp(), unordered_cs_str_cmp_list()
   42 
   43   Miscellaneous Answer Evaluators:
   44     checkbox_cmp()
   45     radio_cmp()
   46 
   47 =cut
   48 
   49 =head1 DESCRIPTION
   50 
   51 This file adds subroutines which create "answer evaluators" for checking
   52 answers. Each answer evaluator accepts a single input from a student answer,
   53 checks it and creates an output hash %ans_hash with seven or eight entries
   54 (the preview_latex_string is optional). The output hash is now being created
   55 with the AnswerHash package "class", which is located at the end of this file.
   56 This class is currently just a wrapper for the hash, but this might change in
   57 the future as new capabilities are added.
   58 
   59           score     =>  $correctQ,
   60           correct_ans   =>  $originalCorrEqn,
   61           student_ans   =>  $modified_student_ans
   62           original_student_ans  =>  $original_student_answer,
   63           ans_message   =>  $PGanswerMessage,
   64           type      =>  'typeString',
   65           preview_text_string =>  $preview_text_string,
   66           preview_latex_string  =>  $preview_latex_string
   67 
   68 
   69   $ans_hash{score}      --  a number between 0 and 1 indicating
   70                     whether the answer is correct. Fractions
   71                     allow the implementation of partial
   72                     credit for incorrect answers.
   73   $ans_hash{correct_ans}      --  The correct answer, as supplied by the
   74                     instructor and then formatted. This can
   75                     be viewed by the student after the answer date.
   76   $ans_hash{student_ans}      --  This is the student answer, after reformatting;
   77                     for example the answer might be forced
   78                     to capital letters for comparison with
   79                     the instructors answer. For a numerical
   80                     answer, it gives the evaluated answer.
   81                     This is displayed in the section reporting
   82                     the results of checking the student answers.
   83   $ans_hash{original_student_ans}   --  This is the original student answer. This is displayed
   84                     on the preview page and may be used for sticky answers.
   85   $ans_hash{ans_message}      --  Any error message, or hint provided by the answer evaluator.
   86                     This is also displayed in the section reporting
   87                     the results of checking the student answers.
   88   $ans_hash{type}       --  A string indicating the type of answer evaluator. This
   89                     helps in preprocessing the student answer for errors.
   90                     Some examples:
   91                       'number_with_units'
   92                       'function'
   93                       'frac_number'
   94                       'arith_number'
   95   $ans_hash{preview_text_string}    --  This typically shows how the student answer was parsed. It is
   96                     displayed on the preview page. For a student answer of 2sin(3x)
   97                     this would be 2*sin(3*x). For string answers it is typically the
   98                     same as $ans_hash{student_ans}.
   99   $ans_hash{preview_latex_string}   --  THIS IS OPTIONAL. This is latex version of the student answer
  100                     which is used to show a typeset view on the answer on the preview
  101                     page. For a student answer of 2/3, this would be \frac{2}{3}.
  102 
  103 Technical note: the routines in this file are not actually answer evaluators. Instead, they create
  104 answer evaluators. An answer evaluator is an anonymous subroutine, referenced by a named scalar. The
  105 routines in this file build the subroutine and return a reference to it. Later, when the student
  106 actually enters an answer, the problem processor feeds that answer to the referenced subroutine, which
  107 evaluates it and returns a score (usually 0 or 1). For most users, this distinction is unimportant, but
  108 if you plan on writing your own answer evaluators, you should understand this point.
  109 
  110 =cut
  111 
  112 BEGIN {
  113   be_strict(); # an alias for use strict.  This means that all global variable must contain main:: as a prefix.
  114 }
  115 
  116 
  117 my ($BR           ,   # convenient localizations.
  118   $PAR          ,
  119   $numRelPercentTolDefault    ,
  120   $numZeroLevelDefault      ,
  121   $numZeroLevelTolDefault     ,
  122   $numAbsTolDefault     ,
  123   $numFormatDefault     ,
  124   $functRelPercentTolDefault    ,
  125   $functZeroLevelDefault      ,
  126   $functZeroLevelTolDefault   ,
  127   $functAbsTolDefault     ,
  128   $functNumOfPoints     ,
  129   $functVarDefault      ,
  130   $functLLimitDefault     ,
  131   $functULimitDefault     ,
  132   $functMaxConstantOfIntegration    ,
  133   $CA
  134 );
  135 
  136 
  137 
  138 
  139 sub _PGanswermacros_init {
  140 
  141      $BR  = $main::BR;    # convenient localizations.
  142      $PAR = $main::PAR;
  143 
  144     # import defaults
  145     # these are now imported from the %envir variable
  146      $numRelPercentTolDefault     = $main::numRelPercentTolDefault;
  147      $numZeroLevelDefault       = $main::numZeroLevelDefault;
  148      $numZeroLevelTolDefault      = $main::numZeroLevelTolDefault;
  149      $numAbsTolDefault        = $main::numAbsTolDefault;
  150      $numFormatDefault        = $main::numFormatDefault;
  151      $functRelPercentTolDefault     = $main::functRelPercentTolDefault;
  152      $functZeroLevelDefault       = $main::functZeroLevelDefault;
  153      $functZeroLevelTolDefault      = $main::functZeroLevelTolDefault;
  154      $functAbsTolDefault        = $main::functAbsTolDefault;
  155      $functNumOfPoints        = $main::functNumOfPoints;
  156      $functVarDefault       = $main::functVarDefault;
  157      $functLLimitDefault        = $main::functLLimitDefault;
  158      $functULimitDefault        = $main::functULimitDefault;
  159      $functMaxConstantOfIntegration     = $main::functMaxConstantOfIntegration;
  160 
  161 
  162 
  163 }
  164 
  165 ##########################################################################
  166 ##########################################################################
  167 ## Number answer evaluators
  168 
  169 =head2 Number Answer Evaluators
  170 
  171 Number answer evaluators take in a numerical answer, compare it to the correct answer,
  172 and return a score. In addition, they can choose to accept or reject an answer based on
  173 its format, closeness to the correct answer, and other criteria. There are two types
  174 of numerical answer evaluators: num_cmp(), which takes a hash of named options as parameters,
  175 and the "mode"_num_cmp() variety, which use different functions to access different sets of
  176 options. In addition, there is the special case of std_num_str_cmp(), which can evaluate
  177 both numbers and strings.
  178 
  179 Numerical Comparison Options
  180 
  181   correctAnswer   --  This is the correct answer that the student answer will
  182             be compared to. However, this does not mean that the
  183             student answer must match this exactly. How close the
  184             student answer must be is determined by the other
  185             options, especially tolerance and format.
  186 
  187   tolerance   --  These options determine how close the student answer
  188             must be to the correct answer to qualify. There are two
  189             types of tolerance: relative and absolute. Relative
  190             tolerances are given in percentages. A relative
  191             tolerance of 1 indicates that the student answer must
  192             be within 1% of the correct answer to qualify as correct.
  193             In other words, a student answer is correct when
  194               abs(studentAnswer - correctAnswer) <= abs(.01*relpercentTol*correctAnswer)
  195             Using absolute tolerance, the student answer must be a
  196             fixed distance from the correct answer to qualify.
  197             For example, an absolute tolerance of 5 means that any
  198             number which is +-5 of the correct answer qualifies as correct.
  199               Final (rarely used) tolerance options are zeroLevel
  200             and zeroLevelTol, used in conjunction with relative
  201             tolerance. if correctAnswer has absolute value less than
  202             or equal to zeroLevel, then the student answer must be,
  203             in absolute terms, within zeroLevelTol of correctAnswer, i.e.,
  204               abs(studentAnswer - correctAnswer) <= zeroLevelTol.
  205             In other words, if the correct answer is very near zero,
  206             an absolute tolerance will be used. One must do this to
  207             handle floating point answers very near zero, because of
  208             the inaccuracy of floating point arithmetic. However, the
  209             default values are almost always adequate.
  210 
  211   mode      --  This determines the allowable methods for entering an
  212             answer. Answers which do not meet this requirement will
  213             be graded as incorrect, regardless of their numerical
  214             value. The recognized modes are:
  215               'std' (default) --  allows any expression which evaluates
  216                         to a number, including those using
  217                         elementary functions like sin() and
  218                         exp(), as well as the operations of
  219                         arithmetic (+, -, *, /, ^)
  220               'strict'  --  only decimal numbers are allowed
  221               'frac'    --  whole numbers and fractions are allowed
  222               'arith'   --  arithmetic expressions are allowed, but
  223                         no functions
  224             Note that all modes allow the use of "pi" and "e" as
  225             constants, and also the use of "E" to represent scientific
  226             notation.
  227 
  228   format      --  The format to use when displaying the correct and
  229             submitted answers. This has no effect on how answers are
  230             evaluated; it is only for cosmetic purposes. The
  231             formatting syntax is the same as Perl uses for the sprintf()
  232             function. Format strings are of the form '%m.nx' or '%m.nx#',
  233             where m and n are described below, and x is a formatter.
  234               Esentially, m is the minimum length of the field
  235             (make this negative to left-justify). Note that the decimal
  236             point counts as a character when determining the field width.
  237             If m begins with a zero, the number will be padded with zeros
  238             instead of spaces to fit the field.
  239               The precision specifier (n) works differently, depending
  240             on which formatter you are using. For d, i, o, u, x and X
  241             formatters (non-floating point formatters), n is the minimum
  242             number of digits to display. For e and f, it is the number of
  243             digits that appear after the decimal point (extra digits will
  244             be rounded; insufficient digits will be padded with spaces--see
  245             '#' below). For g, it is the number of significant digits to
  246             display.
  247               The full list of formatters can be found in the manpage
  248             for printf(3), or by typing "perldoc -f sprintf" at a
  249             terminal prompt. The following is a brief summary of the
  250             most frequent formatters:
  251               d --  decimal number
  252               ld  --  long decimal number
  253               u --  unsigned decimal number
  254               lu  --  long unsigned decimal number
  255               x --  hexadecimal number
  256               o --  octal number
  257               e --  floating point number in scientific notation
  258               f --  floating point number
  259               g --  either e or f, whichever takes less space
  260             Technically, g will use e if the exponent is less than -4 or
  261             greater than or equal to the precision. Trailing zeros are
  262             removed in this mode.
  263               If the format string ends in '#', trailing zeros will be
  264             removed in the decimal part. Note that this is not a standard
  265             syntax; it is handled internally by WeBWorK and not by Perl
  266             (although this should not be a concern to end users).
  267             The default format is '%0.5f#', which displays as a floating
  268             point number with 5 digits of precision and no trailing zeros.
  269             Other useful format strings might be '%0.2f' for displaying
  270             dollar amounts, or '%010d' to display an integer with leading
  271             zeros. Setting format to an empty string ( '' ) means no
  272             formatting will be used; this will show 'arbitrary' precision
  273             floating points.
  274 
  275 Default Values (As of 7/24/2000) (Option -- Variable Name -- Value)
  276 
  277   Format          --  $numFormatDefault   --  "%0.5f#"
  278   Relative Tolerance    --  $numRelPercentTolDefault  --  .1
  279   Absolute Tolerance    --  $numAbsTolDefault   --  .001
  280   Zero Level        --  $numZeroLevelDefault    --  1E-14
  281   Zero Level Tolerance  --  $numZeroLevelTolDefault   --  1E-12
  282 
  283 =cut
  284 
  285 
  286 =head3 num_cmp()
  287 
  288 Compares a number or a list of numbers, using a named hash of options to set
  289 parameters. This can make for more readable code than using the "mode"_num_cmp()
  290 style, but some people find one or the other easier to remember.
  291 
  292 ANS( num_cmp( answer or answer_array_ref, options_hash ) );
  293 
  294   1. the correct answer, or a reference to an array of correct answers
  295   2. a hash with the following keys (all optional):
  296     mode      --  'std' (default) (allows any expression evaluating to
  297                 a number)
  298               'strict' (only numbers are allowed)
  299               'frac' (fractions are allowed)
  300               'arith' (arithmetic expressions allowed)
  301     format      --  '%0.5f#' (default); defines formatting for the
  302                 correct answer
  303     tol       --  an absolute tolerance, or
  304     relTol      --  a relative tolerance
  305     units     --  the units to use for the answer(s)
  306     strings     --  a reference to an array of strings which are valid
  307                 answers (works like std_num_str_cmp() )
  308     zeroLevel   --  if the correct answer is this close to zero,
  309                  then zeroLevelTol applies
  310     zeroLevelTol  --  absolute tolerance to allow when answer is close
  311                  to zero
  312 
  313     debug     --  if set to 1, provides verbose listing of
  314                 hash entries throughout fliters.
  315 
  316   Returns an answer evaluator, or (if given a reference to an array of
  317   answers), a list of answer evaluators. Note that a reference to an array of
  318   answers results is just a shortcut for writing a separate <code>num_cmp()</code> for each
  319   answer.
  320 
  321 EXAMPLES:
  322 
  323   num_cmp( 5 )          --  correct answer is 5, using defaults
  324                   for all options
  325   num_cmp( [5,6,7] )        --  correct answers are 5, 6, and 7,
  326                   using defaults for all options
  327   num_cmp( 5, mode => 'strict' )  --  correct answer is 5, mode is strict
  328   num_cmp( [5,6], relTol => 5 ) --  correct answers are 5 and 6,
  329                     both with 5% relative tolerance
  330   num_cmp( 6, strings => ["Inf", "Minf", "NaN"] )
  331                   --  correct answer is 6, "Inf", "Minf",
  332                    and "NaN" recognized as valid, but
  333                    incorrect answers.
  334 
  335 =cut
  336 
  337 sub num_cmp {
  338   my $correctAnswer = shift @_;
  339   $CA = $correctAnswer;
  340   my @opt = @_;
  341   my %out_options;
  342 
  343 #########################################################################
  344 # Retain this first check for backword compatibility.  Allows input of the form
  345 # num_cmp($ans, 1, '%0.5f') but warns against it
  346 #########################################################################
  347   my %known_options = (
  348           'mode'      =>  'std',
  349           'format'    =>  $numFormatDefault,
  350           'tol'     =>  $numAbsTolDefault,
  351           'relTol'    =>  $numRelPercentTolDefault,
  352           'units'     =>  undef,
  353           'strings'   =>  undef,
  354           'zeroLevel'   =>  $numZeroLevelDefault,
  355           'zeroLevelTol'  =>  $numZeroLevelTolDefault,
  356           'tolType'       =>  'relative',
  357           'tolerance'     =>  1,
  358           'reltol'    =>  undef,      #alternate spelling
  359           'unit'      =>  undef,      #alternate spelling
  360           'debug'     =>  0
  361         );
  362 
  363   my @output_list;
  364   my( $relPercentTol, $format, $zeroLevel, $zeroLevelTol) = @opt;
  365 
  366   unless( ref($correctAnswer) eq 'ARRAY' || scalar( @opt ) == 0 ||
  367         ( defined($opt[0]) and exists $known_options{$opt[0]} ) ) {
  368     # unless the first parameter is a list of arrays
  369     # or the second parameter is a known option or
  370     # no options were used,
  371     # use the old num_cmp which does not use options, but has inputs
  372     # $relPercentTol,$format,$zeroLevel,$zeroLevelTol
  373     warn "This method of using num_cmp() is deprecated. Please rewrite this" .
  374           " problem using the options style of parameter passing (or" .
  375           " check that your first option is spelled correctly).";
  376 
  377     %out_options = (  'relTol'    => $relPercentTol,
  378           'format'    => $format,
  379           'zeroLevel'   => $zeroLevel,
  380           'zeroLevelTol'  => $zeroLevelTol,
  381           'mode'      => 'std'
  382     );
  383   }
  384 
  385 #########################################################################
  386 # Now handle the options assuming they are entered in the form
  387 # num_cmp($ans, relTol=>1, format=>'%0.5f')
  388 #########################################################################
  389   %out_options = @opt;
  390   assign_option_aliases( \%out_options,
  391         'reltol'    =>      'relTol',
  392         'unit'      =>      'units',
  393         'abstol'  =>    'tol',
  394         );
  395 
  396   set_default_options( \%out_options,
  397            'tolType'    =>  (defined($out_options{tol}) ) ? 'absolute' : 'relative',  # the existence of "tol" means that we use absolute tolerance mode
  398            'tolerance'    =>  (defined($out_options{tol}) ) ? $numAbsTolDefault : $numRelPercentTolDefault,
  399            'mode'       =>  'std',
  400            'format'   =>  $numFormatDefault,
  401            'tol'        =>  undef,
  402            'relTol'   =>  undef,
  403            'units'    =>  undef,
  404            'strings'    =>  undef,
  405            'zeroLevel'  =>  $numZeroLevelDefault,
  406            'zeroLevelTol' =>  $numZeroLevelTolDefault,
  407            'debug'    =>  0,
  408   );
  409 
  410   # can't use both units and strings
  411   if( defined( $out_options{'units'} ) && defined( $out_options{'strings'} ) ) {
  412     warn "Can't use both 'units' and 'strings' in the same problem " .
  413     "(check your parameters to num_cmp() )";
  414   }
  415 
  416   # Handle legacy options
  417     if ($out_options{tolType} eq 'absolute')   {
  418     $out_options{'tolerance'}=$out_options{'tol'} if defined($out_options{'tol'});
  419     delete($out_options{'relTol'}) if exists( $out_options{'relTol'} );
  420   } else {
  421     $out_options{'tolerance'}=$out_options{'relTol'} if defined($out_options{'relTol'});
  422     # delete($out_options{'tol'}) if exists( $out_options{'tol'} );
  423   }
  424   # end legacy options
  425 
  426   # thread over lists
  427   my @ans_list = ();
  428 
  429   if ( ref($correctAnswer) eq 'ARRAY' ) {
  430     @ans_list = @{$correctAnswer};
  431   }
  432   else { push( @ans_list, $correctAnswer );
  433   }
  434 
  435   # produce answer evaluators
  436   foreach my $ans (@ans_list) {
  437     if( defined( $out_options{'units'} ) ) {
  438       $ans = "$ans $out_options{'units'}";
  439 
  440       push( @output_list, NUM_CMP(  'correctAnswer'       =>  $ans,
  441               'tolerance'   =>  $out_options{'tolerance'},
  442               'tolType'   =>  $out_options{'tolType'},
  443               'format'    =>  $out_options{'format'},
  444               'mode'      =>  $out_options{'mode'},
  445               'zeroLevel'   =>  $out_options{'zeroLevel'},
  446               'zeroLevelTol'  =>  $out_options{'zeroLevelTol'},
  447               'debug'     =>  $out_options{'debug'},
  448               'units'     =>  $out_options{'units'},
  449             )
  450       );
  451     } elsif( defined( $out_options{'strings'} ) ) {
  452 
  453 
  454       push( @output_list, NUM_CMP(  'correctAnswer' =>  $ans,
  455               'tolerance' =>  $out_options{tolerance},
  456               'tolType' =>  $out_options{tolType},
  457               'format'  =>  $out_options{'format'},
  458               'mode'    =>  $out_options{'mode'},
  459               'zeroLevel' =>  $out_options{'zeroLevel'},
  460               'zeroLevelTol'  =>  $out_options{'zeroLevelTol'},
  461               'debug'   =>  $out_options{'debug'},
  462               'strings' =>  $out_options{'strings'},
  463          )
  464          );
  465     } else {
  466       push(@output_list,
  467         NUM_CMP(  'correctAnswer'       =>  $ans,
  468           'tolerance'   =>  $out_options{tolerance},
  469           'tolType'   =>  $out_options{tolType},
  470           'format'    =>  $out_options{'format'},
  471           'mode'      =>  $out_options{'mode'},
  472           'zeroLevel'   =>  $out_options{'zeroLevel'},
  473           'zeroLevelTol'        =>  $out_options{'zeroLevelTol'},
  474           'debug'     =>  $out_options{'debug'},
  475         ),
  476       );
  477       }
  478   }
  479 
  480   return (wantarray) ? @output_list : $output_list[0];
  481 }
  482 
  483 #legacy code for compatability purposes
  484 sub num_rel_cmp {   # compare numbers
  485     std_num_cmp( @_ );
  486 }
  487 
  488 
  489 =head3 "mode"_num_cmp() functions
  490 
  491 There are 16 functions total, 4 for each mode (std, frac, strict, arith). Each mode has
  492 one "normal" function, one which accepts a list of answers, one which uses absolute
  493 rather than relative tolerance, and one which uses absolute tolerance and accepts a list.
  494 The "std" family is documented below; all others work precisely the same.
  495 
  496  std_num_cmp($correctAnswer) OR
  497  std_num_cmp($correctAnswer, $relPercentTol) OR
  498  std_num_cmp($correctAnswer, $relPercentTol, $format) OR
  499  std_num_cmp($correctAnswer, $relPercentTol, $format, $zeroLevel) OR
  500  std_num_cmp($correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol)
  501 
  502   $correctAnswer  --  the correct answer
  503   $relPercentTol  --  the tolerance, as a percentage (optional)
  504   $format   --  the format of the displayed answer (optional)
  505   $zeroLevel  --  if the correct answer is this close to zero, then zeroLevelTol applies (optional)
  506   $zeroLevelTol --  absolute tolerance to allow when correct answer is close to zero (optional)
  507 
  508   std_num_cmp() uses standard mode (arithmetic operations and elementary
  509   functions allowed) and relative tolerance. Options are specified by
  510   one or more parameters. Note that if you wish to set an option which
  511   is later in the parameter list, you must set all previous options.
  512 
  513  std_num_cmp_abs($correctAnswer) OR
  514  std_num_cmp_abs($correctAnswer, $absTol) OR
  515  std_num_cmp_abs($correctAnswer, $absTol, $format)
  516 
  517   $correctAnswer    --  the correct answer
  518   $absTol     --  an absolute tolerance (optional)
  519   $format     --  the format of the displayed answer (optional)
  520 
  521   std_num_cmp_abs() uses standard mode and absolute tolerance. Options
  522   are set as with std_num_cmp(). Note that $zeroLevel and $zeroLevelTol
  523   do not apply with absolute tolerance.
  524 
  525  std_num_cmp_list($relPercentTol, $format, @answerList)
  526 
  527   $relPercentTol    --  the tolerance, as a percentage
  528   $format     --  the format of the displayed answer(s)
  529   @answerList   --  a list of one or more correct answers
  530 
  531   std_num_cmp_list() uses standard mode and relative tolerance. There
  532   is no way to set $zeroLevel or $zeroLevelTol. Note that no
  533   parameters are optional. All answers in the list will be
  534   evaluated with the same set of parameters.
  535 
  536  std_num_cmp_abs_list($absTol, $format, @answerList)
  537 
  538   $absTol   --  an absolute tolerance
  539   $format   --  the format of the displayed answer(s)
  540   @answerList --  a list of one or more correct answers
  541 
  542   std_num_cmp_abs_list() uses standard mode and absolute tolerance.
  543   Note that no parameters are optional. All answers in the list will be
  544   evaluated with the same set of parameters.
  545 
  546  arith_num_cmp(), arith_num_cmp_list(), arith_num_cmp_abs(), arith_num_cmp_abs_list()
  547  strict_num_cmp(), strict_num_cmp_list(), strict_num_cmp_abs(), strict_num_cmp_abs_list()
  548  frac_num_cmp(), frac_num_cmp_list(), frac_num_cmp_abs(), frac_num_cmp_abs_list()
  549 
  550 Examples:
  551 
  552   ANS( strict_num_cmp( 3.14159 ) )  --  The student answer must be a number
  553     in decimal or scientific notation which is within .1 percent of 3.14159.
  554     This assumes $numRelPercentTolDefault has been set to .1.
  555   ANS( strict_num_cmp( $answer, .01 ) ) --  The student answer must be a
  556     number within .01 percent of $answer (e.g. 3.14159 if $answer is 3.14159
  557     or $answer is "pi" or $answer is 4*atan(1)).
  558   ANS( frac_num_cmp( $answer) ) or ANS( frac_num_cmp( $answer,.01 ))  --
  559     The student answer can be a number or fraction, e.g. 2/3.
  560   ANS( arith_num_cmp( $answer) ) or ANS( arith_num_cmp( $answer,.01 ))  --
  561     The student answer can be an arithmetic expression, e.g. (2+3)/7-2^.5 .
  562   ANS( std_num_cmp( $answer) ) or ANS( std_num_cmp( $answer,.01 ))  --
  563     The student answer can contain elementary functions, e.g. sin(.3+pi/2)
  564 
  565 =cut
  566 
  567 sub std_num_cmp {           # compare numbers allowing use of elementary functions
  568     my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_;
  569 
  570   my %options = ( 'relTol'    =>  $relPercentTol,
  571         'format'    =>  $format,
  572         'zeroLevel'   =>  $zeroLevel,
  573         'zeroLevelTol'  =>  $zeroLevelTol
  574     );
  575 
  576     set_default_options( \%options,
  577        'tolType'  =>      'relative',
  578        'tolerance'    =>      $numRelPercentTolDefault,
  579        'mode'   =>  'std',
  580        'format' =>  $numFormatDefault,
  581        'relTol' =>  $numRelPercentTolDefault,
  582        'zeroLevel'    =>      $numZeroLevelDefault,
  583        'zeroLevelTol' =>      $numZeroLevelTolDefault,
  584        'debug'        =>      0,
  585     );
  586 
  587     num_cmp([$correctAnswer], %options);
  588 }
  589 
  590 ##  Similar to std_num_cmp but accepts a list of numbers in the form
  591 ##  std_num_cmp_list(relpercentTol,format,ans1,ans2,ans3,...)
  592 ##  format is of the form "%10.3g" or "", i.e., a format suitable for sprintf(). Use "" for default
  593 ##  You must enter a format and tolerance
  594 
  595 sub std_num_cmp_list {
  596   my ( $relPercentTol, $format, @answerList) = @_;
  597 
  598   my %options = ( 'relTol'  =>      $relPercentTol,
  599       'format'        =>      $format,
  600   );
  601 
  602   set_default_options( \%options,
  603            'tolType'      =>      'relative',
  604            'tolerance'    =>      $numRelPercentTolDefault,
  605            'mode'         =>      'std',
  606            'format'       =>      $numFormatDefault,
  607            'relTol'       =>      $numRelPercentTolDefault,
  608            'zeroLevel'    =>      $numZeroLevelDefault,
  609            'zeroLevelTol' =>      $numZeroLevelTolDefault,
  610            'debug'        =>      0,
  611   );
  612 
  613   num_cmp(\@answerList, %options);
  614 
  615 }
  616 
  617 sub std_num_cmp_abs {     # compare numbers allowing use of elementary functions with absolute tolerance
  618   my ( $correctAnswer, $absTol, $format) = @_;
  619   my %options = ( 'tolerance'  => $absTol,
  620             'format'     => $format
  621   );
  622 
  623   set_default_options (\%options,
  624            'tolType'      =>      'absolute',
  625            'tolerance'    =>      $absTol,
  626            'mode'         =>      'std',
  627            'format'       =>      $numFormatDefault,
  628            'zeroLevel'    =>      0,
  629            'zeroLevelTol' =>      0,
  630            'debug'        =>      0,
  631   );
  632 
  633   num_cmp([$correctAnswer], %options);
  634 }
  635 
  636 ##  See std_num_cmp_list for usage
  637 
  638 sub std_num_cmp_abs_list {
  639   my ( $absTol, $format, @answerList ) = @_;
  640 
  641         my %options = ( 'tolerance'         =>      $absTol,
  642                         'format'            =>      $format,
  643   );
  644 
  645         set_default_options( \%options,
  646                              'tolType'      =>      'absolute',
  647                              'tolerance'    =>      $absTol,
  648                              'mode'         =>      'std',
  649                              'format'       =>      $numFormatDefault,
  650                              'zeroLevel'    =>      0,
  651                              'zeroLevelTol' =>      0,
  652                              'debug'        =>      0,
  653         );
  654 
  655         num_cmp(\@answerList, %options);
  656 }
  657 
  658 sub frac_num_cmp {            # only allow fractions and numbers as submitted answer
  659 
  660   my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_;
  661 
  662   my %options = ( 'relTol'   =>   $relPercentTol,
  663           'format'     =>   $format,
  664           'zeroLevel'  =>   $zeroLevel,
  665           'zeroLevelTol'   =>   $zeroLevelTol
  666   );
  667 
  668   set_default_options( \%options,
  669          'tolType'     =>   'relative',
  670          'tolerance'   =>   $relPercentTol,
  671          'mode'      =>   'frac',
  672          'format'    =>   $numFormatDefault,
  673          'zeroLevel'   =>   $numZeroLevelDefault,
  674          'zeroLevelTol'  =>   $numZeroLevelTolDefault,
  675          'relTol'    =>   $numRelPercentTolDefault,
  676          'debug'     =>   0,
  677    );
  678 
  679   num_cmp([$correctAnswer], %options);
  680 }
  681 
  682 ##  See std_num_cmp_list for usage
  683 sub frac_num_cmp_list {
  684   my ( $relPercentTol, $format, @answerList ) = @_;
  685 
  686   my %options = (      'relTol'  =>   $relPercentTol,
  687                'format'    =>   $format
  688   );
  689 
  690   set_default_options( \%options,
  691        'tolType'     =>   'relative',
  692        'tolerance'   =>   $relPercentTol,
  693        'mode'      =>   'frac',
  694        'format'    =>   $numFormatDefault,
  695        'zeroLevel'   =>   $numZeroLevelDefault,
  696        'zeroLevelTol'  =>   $numZeroLevelTolDefault,
  697        'relTol'    =>   $numRelPercentTolDefault,
  698        'debug'     =>   0,
  699   );
  700 
  701   num_cmp(\@answerList, %options);
  702 }
  703 
  704 sub frac_num_cmp_abs {      # only allow fraction expressions as submitted answer with absolute tolerance
  705     my ( $correctAnswer, $absTol, $format ) = @_;
  706 
  707     my %options = (             'tolerance'    =>     $absTol,
  708               'format'       =>     $format
  709     );
  710 
  711   set_default_options (\%options,
  712       'tolType'    =>   'absolute',
  713       'tolerance'    =>   $absTol,
  714       'mode'       =>   'frac',
  715       'format'     =>   $numFormatDefault,
  716       'zeroLevel'    =>   0,
  717       'zeroLevelTol' =>   0,
  718       'debug'      =>   0,
  719   );
  720 
  721     num_cmp([$correctAnswer], %options);
  722 }
  723 
  724 ##  See std_num_cmp_list for usage
  725 
  726 sub frac_num_cmp_abs_list {
  727     my ( $absTol, $format, @answerList ) = @_;
  728 
  729     my %options = (             'tolerance'    =>     $absTol,
  730               'format'       =>     $format
  731     );
  732 
  733     set_default_options (\%options,
  734        'tolType'      =>     'absolute',
  735        'tolerance'    =>     $absTol,
  736        'mode'         =>     'frac',
  737        'format'       =>     $numFormatDefault,
  738        'zeroLevel'    =>     0,
  739        'zeroLevelTol' =>     0,
  740        'debug'        =>     0,
  741     );
  742 
  743     num_cmp(\@answerList, %options);
  744 }
  745 
  746 
  747 sub arith_num_cmp {           # only allow arithmetic expressions as submitted answer
  748 
  749     my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_;
  750 
  751     my %options = (     'relTol'      =>     $relPercentTol,
  752       'format'         =>     $format,
  753       'zeroLevel'      =>     $zeroLevel,
  754       'zeroLevelTol'   =>     $zeroLevelTol
  755     );
  756 
  757     set_default_options( \%options,
  758                         'tolType'       =>     'relative',
  759                         'tolerance'     =>     $relPercentTol,
  760                         'mode'          =>     'arith',
  761                         'format'        =>     $numFormatDefault,
  762                         'zeroLevel'     =>     $numZeroLevelDefault,
  763                         'zeroLevelTol'  =>     $numZeroLevelTolDefault,
  764                         'relTol'        =>     $numRelPercentTolDefault,
  765                         'debug'         =>     0,
  766     );
  767 
  768     num_cmp([$correctAnswer], %options);
  769 }
  770 
  771 ##  See std_num_cmp_list for usage
  772 sub arith_num_cmp_list {
  773     my ( $relPercentTol, $format, @answerList ) = @_;
  774 
  775     my %options = (     'relTol'     =>     $relPercentTol,
  776                         'format'        =>     $format,
  777     );
  778 
  779     set_default_options( \%options,
  780                          'tolType'       =>     'relative',
  781                          'tolerance'     =>     $relPercentTol,
  782                          'mode'          =>     'arith',
  783                          'format'        =>     $numFormatDefault,
  784                          'zeroLevel'     =>     $numZeroLevelDefault,
  785                          'zeroLevelTol'  =>     $numZeroLevelTolDefault,
  786                          'relTol'        =>     $numRelPercentTolDefault,
  787                          'debug'         =>     0,
  788     );
  789 
  790     num_cmp(\@answerList, %options);
  791 }
  792 
  793 sub arith_num_cmp_abs {     # only allow arithmetic expressions as submitted answer with absolute tolerance
  794     my ( $correctAnswer, $absTol, $format ) = @_;
  795 
  796     my %options = (      'tolerance'    =>     $absTol,
  797                          'format'       =>     $format
  798     );
  799 
  800     set_default_options (\%options,
  801                          'tolType'      =>     'absolute',
  802                          'tolerance'    =>     $absTol,
  803                          'mode'         =>     'arith',
  804                          'format'       =>     $numFormatDefault,
  805                          'zeroLevel'    =>     0,
  806                          'zeroLevelTol' =>     0,
  807                          'debug'        =>     0,
  808     );
  809 
  810     num_cmp([$correctAnswer], %options);
  811 }
  812 
  813 ##  See std_num_cmp_list for usage
  814 sub arith_num_cmp_abs_list {
  815     my ( $absTol, $format, @answerList ) = @_;
  816 
  817     my %options = (      'tolerance'    =>     $absTol,
  818                          'format'       =>     $format
  819     );
  820 
  821     set_default_options (\%options,
  822                          'tolType'      =>     'absolute',
  823                          'tolerance'    =>     $absTol,
  824                          'mode'         =>     'arith',
  825                          'format'       =>     $numFormatDefault,
  826                          'zeroLevel'    =>     0,
  827                          'zeroLevelTol' =>     0,
  828                          'debug'        =>     0,
  829     );
  830 
  831     num_cmp(\@answerList, %options);
  832 }
  833 
  834 sub strict_num_cmp {          # only allow numbers as submitted answer
  835     my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_;
  836 
  837     my %options = (      'relTol'     =>     $relPercentTol,
  838                          'format'        =>     $format,
  839                          'zeroLevel'     =>     $zeroLevel,
  840                          'zeroLevelTol'  =>     $zeroLevelTol
  841     );
  842 
  843     set_default_options( \%options,
  844                          'tolType'       =>     'relative',
  845                          'tolerance'     =>     $relPercentTol,
  846                          'mode'          =>     'strict',
  847                          'format'        =>     $numFormatDefault,
  848                          'zeroLevel'     =>     $numZeroLevelDefault,
  849                          'zeroLevelTol'  =>     $numZeroLevelTolDefault,
  850                          'relTol'        =>     $numRelPercentTolDefault,
  851                          'debug'         =>     0,
  852     );
  853     num_cmp([$correctAnswer], %options);
  854 
  855 }
  856 
  857 ##  See std_num_cmp_list for usage
  858 sub strict_num_cmp_list {       # compare numbers
  859     my ( $relPercentTol, $format, @answerList ) = @_;
  860 
  861     my %options = (    'relTol'     =>     $relPercentTol,
  862        'format'        =>     $format,
  863     );
  864 
  865     set_default_options( \%options,
  866                          'tolType'       =>     'relative',
  867                          'tolerance'     =>     $relPercentTol,
  868                          'mode'          =>     'strict',
  869                          'format'        =>     $numFormatDefault,
  870                          'zeroLevel'     =>     $numZeroLevelDefault,
  871                          'zeroLevelTol'  =>     $numZeroLevelTolDefault,
  872                          'relTol'        =>     $numRelPercentTolDefault,
  873                          'debug'         =>     0,
  874     );
  875 
  876     num_cmp(\@answerList, %options);
  877 }
  878 
  879 
  880 sub strict_num_cmp_abs {        # only allow numbers as submitted answer with absolute tolerance
  881     my ( $correctAnswer, $absTol, $format ) = @_;
  882 
  883     my %options = (       'tolerance'    =>     $absTol,
  884                     'format'       =>     $format
  885     );
  886 
  887     set_default_options (\%options,
  888                          'tolType'      =>     'absolute',
  889                          'tolerance'    =>     $absTol,
  890                          'mode'         =>     'strict',
  891                          'format'       =>     $numFormatDefault,
  892                          'zeroLevel'    =>     0,
  893                          'zeroLevelTol' =>     0,
  894                          'debug'        =>     0,
  895     );
  896     num_cmp([$correctAnswer], %options);
  897 
  898 }
  899 
  900 ##  See std_num_cmp_list for usage
  901 sub strict_num_cmp_abs_list {     # compare numbers
  902     my ( $absTol, $format, @answerList ) = @_;
  903 
  904     my %options = (      'tolerance'    =>     $absTol,
  905                          'format'       =>     $format
  906     );
  907 
  908     set_default_options (\%options,
  909                          'tolType'      =>     'absolute',
  910                          'tolerance'    =>     $absTol,
  911                          'mode'         =>     'strict',
  912                          'format'       =>     $numFormatDefault,
  913                          'zeroLevel'    =>     0,
  914                          'zeroLevelTol' =>     0,
  915                          'debug'        =>     0,
  916     );
  917 
  918     num_cmp(\@answerList, %options);
  919 }
  920 
  921 ## sub numerical_compare_with_units
  922 ## Compares a number with units
  923 ## Deprecated; use num_cmp()
  924 ##
  925 ## IN:  a string which includes the numerical answer and the units
  926 ##    a hash with the following keys (all optional):
  927 ##      mode    --  'std', 'frac', 'arith', or 'strict'
  928 ##      format    --  the format to use when displaying the answer
  929 ##      tol   --  an absolute tolerance, or
  930 ##      relTol    --  a relative tolerance
  931 ##      zeroLevel --  if the correct answer is this close to zero, then zeroLevelTol applies
  932 ##      zeroLevelTol  --  absolute tolerance to allow when correct answer is close to zero
  933 
  934 # This mode is depricated.  send input through num_cmp -- it can handle units.
  935 
  936 sub numerical_compare_with_units {
  937   my $correct_answer = shift;  # the answer is a string which includes both the numerical answer and the units.
  938   my %options = @_;    # all of the other inputs are (key value) pairs
  939 
  940   # Prepare the correct answer
  941   $correct_answer = str_filters( $correct_answer, 'trim_whitespace' );
  942 
  943   # it surprises me that the match below works since the first .* is greedy.
  944   my ($correct_num_answer, $correct_units) = $correct_answer =~ /^(.*)\s+([^\s]*)$/;
  945   $options{units} = $correct_units;
  946 
  947   num_cmp($correct_num_answer, %options);
  948 }
  949 
  950 
  951 =head3 std_num_str_cmp()
  952 
  953 NOTE: This function is maintained for compatibility. num_cmp() with the
  954     'strings' parameter is slightly preferred.
  955 
  956 std_num_str_cmp() is used when the correct answer could be either a number or a
  957 string. For example, if you wanted the student to evaluate a function at number
  958 of points, but write "Inf" or "Minf" if the function is unbounded. This routine
  959 will provide error messages that do not give a hint as to whether the correct
  960 answer is a string or a number. For numerical comparisons, std_num_cmp() is
  961 used internally; for string comparisons, std_str_cmp() is used.
  962 
  963  std_num_str_cmp( $correctAnswer ) OR
  964  std_num_str_cmp( $correctAnswer, $ra_legalStrings ) OR
  965  std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol ) OR
  966  std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol, $format ) OR
  967  std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol, $format, $zeroLevel ) OR
  968  std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol, $format,
  969           $zeroLevel, $zeroLevelTol )
  970 
  971   $correctAnswer    --  the correct answer
  972   $ra_legalStrings  --  a reference to an array of legal strings, e.g. ["str1", "str2"]
  973   $relPercentTol    --  the error tolerance as a percentage
  974   $format     --  the display format
  975   $zeroLevel    --  if the correct answer is this close to zero, then zeroLevelTol applies
  976   $zeroLevelTol   --  absolute tolerance to allow when correct answer is close to zero
  977 
  978 Example:
  979   ANS( std_num_str_cmp( $ans, ["Inf", "Minf", "NaN"] ) );
  980 
  981 =cut
  982 
  983 sub std_num_str_cmp {
  984   my ( $correctAnswer, $ra_legalStrings, $relpercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_;
  985   # warn ('This method is depreciated.  Use num_cmp instead.');
  986   return num_cmp ($correctAnswer, strings=>$ra_legalStrings, relTol=>$relpercentTol, format=>$format,
  987     zeroLevel=>$zeroLevel, zeroLevelTol=>$zeroLevelTol);
  988 }
  989 
  990 sub NUM_CMP {   # low level numeric compare
  991   my %num_params = @_;
  992 
  993   my @keys = qw ( correctAnswer tolerance tolType format mode zeroLevel zeroLevelTol debug );
  994   foreach my $key (@keys) {
  995       warn "$key must be defined in options when calling NUM_CMP" unless defined ($num_params{$key});
  996   }
  997 
  998   my $correctAnswer = $num_params{'correctAnswer'};
  999   my $format    = $num_params{'format'};
 1000   my $mode    = $num_params{'mode'};
 1001 
 1002   if( $num_params{tolType} eq 'relative' ) {
 1003     $num_params{'tolerance'} = .01*$num_params{'tolerance'};
 1004   }
 1005 
 1006   my $formattedCorrectAnswer;
 1007   my $correct_units;
 1008   my $correct_num_answer;
 1009   my %correct_units;
 1010   my $corrAnswerIsString = 0;
 1011 
 1012 
 1013   if (defined($num_params{units}) && $num_params{units}) {
 1014     $correctAnswer  = str_filters( $correctAnswer, 'trim_whitespace' );
 1015             # units are in form stuff space units where units contains no spaces.
 1016 
 1017     ($correct_num_answer, $correct_units) = $correctAnswer =~ /^(.*)\s+([^\s]*)$/;
 1018     %correct_units = Units::evaluate_units($correct_units);
 1019     if ( defined( $correct_units{'ERROR'} ) ) {
 1020        warn ("ERROR: The answer \"$correctAnswer\" in the problem definition cannot be parsed:\n" .
 1021         "$correct_units{'ERROR'}\n");
 1022     }
 1023     # $formattedCorrectAnswer = spf($correct_num_answer,$num_params{'format'}) . " $correct_units";
 1024     $formattedCorrectAnswer = prfmt($correct_num_answer,$num_params{'format'}) . " $correct_units";
 1025 
 1026   } elsif (defined($num_params{strings}) && $num_params{strings}) {
 1027     my $legalString = '';
 1028     my @legalStrings = @{$num_params{strings}};
 1029     $correct_num_answer = $correctAnswer;
 1030     $formattedCorrectAnswer = $correctAnswer;
 1031     foreach $legalString (@legalStrings) {
 1032       if ( uc($correctAnswer) eq uc($legalString) ) {
 1033         $corrAnswerIsString = 1;
 1034 
 1035         last;
 1036       }
 1037     }     ## at this point $corrAnswerIsString = 0 iff correct answer is numeric
 1038   } else {
 1039     $correct_num_answer = $correctAnswer;
 1040     $formattedCorrectAnswer = prfmt( $correctAnswer, $num_params{'format'} );
 1041   }
 1042 
 1043   $correct_num_answer = math_constants($correct_num_answer);
 1044 
 1045   my $PGanswerMessage = '';
 1046 
 1047   my ($inVal,$correctVal,$PG_eval_errors,$PG_full_error_report);
 1048 
 1049   if (defined($correct_num_answer) && $correct_num_answer =~ /\S/ && $corrAnswerIsString == 0 ) {
 1050       ($correctVal, $PG_eval_errors,$PG_full_error_report) = PG_answer_eval($correct_num_answer);
 1051   } else { # case of a string answer
 1052     $PG_eval_errors = ' ';
 1053     $correctVal = $correctAnswer;
 1054   }
 1055 
 1056   if ( ($PG_eval_errors && $corrAnswerIsString == 0) or ((not is_a_number($correctVal)) && $corrAnswerIsString == 0)) {
 1057         ##error message from eval or above
 1058     warn "Error in 'correct' answer: $PG_eval_errors<br>
 1059           The answer $correctAnswer evaluates to $correctVal,
 1060           which cannot be interpreted as a number.  ";
 1061 
 1062   }
 1063   #########################################################################
 1064 
 1065   #construct the answer evaluator
 1066       my $answer_evaluator = new AnswerEvaluator;
 1067       $answer_evaluator->{debug} = $num_params{debug};
 1068       $answer_evaluator->ans_hash(
 1069                 correct_ans       =>  $correctVal,
 1070                 type          =>  "${mode}_number",
 1071                 tolerance       =>  $num_params{tolerance},
 1072               tolType         =>  $num_params{tolType},
 1073               units         =>  $correct_units,
 1074                 original_correct_ans  =>  $formattedCorrectAnswer,
 1075                 rh_correct_units    =>      \%correct_units,
 1076                 answerIsString      =>  $corrAnswerIsString,
 1077       );
 1078       my ($in, $formattedSubmittedAnswer);
 1079   $answer_evaluator->install_pre_filter(sub {my $rh_ans = shift;
 1080     $rh_ans->{original_student_ans} = $rh_ans->{student_ans}; $rh_ans;}
 1081   );
 1082   if (defined($num_params{units}) && $num_params{units}) {
 1083       $answer_evaluator->install_pre_filter(\&check_units);
 1084   }
 1085   if (defined($num_params{strings}) && $num_params{strings}) {
 1086       $answer_evaluator->install_pre_filter(\&check_strings, %num_params);
 1087   }
 1088 
 1089   $answer_evaluator->install_pre_filter(\&check_syntax);
 1090 
 1091   $answer_evaluator->install_pre_filter(\&math_constants);
 1092 
 1093   if ($mode eq 'std') {
 1094         # do nothing
 1095   } elsif ($mode eq 'strict') {
 1096     $answer_evaluator->install_pre_filter(\&is_a_number);
 1097   } elsif ($mode eq 'arith') {
 1098       $answer_evaluator->install_pre_filter(\&is_an_arithmetic_expression);
 1099     } elsif ($mode eq 'frac') {
 1100       $answer_evaluator->install_pre_filter(\&is_a_fraction);
 1101 
 1102     } else {
 1103       $PGanswerMessage = 'Tell your professor that there is an error in his or her answer mechanism. No mode was specified.';
 1104       $formattedSubmittedAnswer = $in;
 1105     }
 1106 
 1107   if ($corrAnswerIsString == 0 ){   # avoiding running compare_numbers when correct answer is a string.
 1108     $answer_evaluator->install_evaluator(\&compare_numbers, %num_params);
 1109    }
 1110 
 1111 
 1112 ###############################################################################
 1113 # We'll leave these next lines out for now, so that the evaluated versions of the student's and professor's
 1114 # can be displayed in the answer message.  This may still cause a few anomolies when strings are used
 1115 #
 1116 ###############################################################################
 1117 
 1118   $answer_evaluator->install_post_filter(\&fix_answers_for_display);
 1119 
 1120       $answer_evaluator->install_post_filter(sub {my $rh_ans = shift;
 1121           return $rh_ans unless $rh_ans->catch_error('EVAL');
 1122           $rh_ans->{student_ans} = $rh_ans->{original_student_ans}. ' '. $rh_ans->{error_message};
 1123           $rh_ans->clear_error('EVAL'); } );
 1124       $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; $rh_ans->clear_error('SYNTAX'); } );
 1125       $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; $rh_ans->clear_error('UNITS'); } );
 1126       $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; $rh_ans->clear_error('NUMBER'); } );
 1127   $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; $rh_ans->clear_error('STRING'); } );
 1128       $answer_evaluator;
 1129 }
 1130 
 1131 
 1132 
 1133 ##########################################################################
 1134 ##########################################################################
 1135 ## Function answer evaluators
 1136 
 1137 =head2 Function Answer Evaluators
 1138 
 1139 Function answer evaluators take in a function, compare it numerically to a
 1140 correct function, and return a score. They can require an exactly equivalent
 1141 function, or one that is equal up to a constant. They can accept or reject an
 1142 answer based on specified tolerances for numerical deviation.
 1143 
 1144 Function Comparison Options
 1145 
 1146   correctEqn  --  The correct equation, specified as a string. It may include
 1147           all basic arithmetic operations, as well as elementary
 1148           functions. Variable usage is described below.
 1149 
 1150   Variables --  The independent variable(s). When comparing the correct
 1151           equation to the student equation, each variable will be
 1152           replaced by a certain number of numerical values. If
 1153           the student equation agrees numerically with the correct
 1154           equation, they are considered equal. Note that all
 1155           comparison is numeric; it is possible (although highly
 1156           unlikely and never a practical concern) for two unequal
 1157           functions to yield the same numerical results.
 1158 
 1159   Limits    --  The limits of evaluation for the independent variables.
 1160           Each variable is evaluated only in the half-open interval
 1161           [lower_limit, upper_limit). This is useful if the function
 1162           has a singularity or is not defined in a certain range.
 1163           For example, the function "sqrt(-1-x)" could be evaluated
 1164           in [-2,-1).
 1165 
 1166   Tolerance --  Tolerance in function comparisons works exactly as in
 1167           numerical comparisons; see the numerical comparison
 1168           documentation for a complete description. Note that the
 1169           tolerance does applies to the function as a whole, not
 1170           each point individually.
 1171 
 1172   Number of --  Specifies how many points to evaluate each variable at. This
 1173   Points      is typically 3, but can be set higher if it is felt that
 1174           there is a strong possibility of "false positives."
 1175 
 1176   Maximum   --  Sets the maximum size of the constant of integration. For
 1177   Constant of   technical reasons concerning floating point arithmetic, if
 1178   Integration   the additive constant, i.e., the constant of integration, is
 1179           greater (in absolute value) than maxConstantOfIntegration
 1180           AND is greater than maxConstantOfIntegration times the
 1181           correct value, WeBWorK will give an error message saying
 1182           that it can not handle such a large constant of integration.
 1183           This is to prevent e.g. cos(x) + 1E20 or even 1E20 as being
 1184           accepted as a correct antiderivatives of sin(x) since
 1185           floating point arithmetic cannot tell the difference
 1186           between cos(x) + 1E20, 1E20, and -cos(x) + 1E20.
 1187 
 1188 Technical note: if you examine the code for the function routines, you will see
 1189 that most subroutines are simply doing some basic error-checking and then
 1190 passing the parameters on to the low-level FUNCTION_CMP(). Because this routine
 1191 is set up to handle multivariable functions, with single-variable functions as
 1192 a special case, it is possible to pass multivariable parameters to single-
 1193 variable functions. This usage is strongly discouraged as unnecessarily
 1194 confusing. Avoid it.
 1195 
 1196 Default Values (As of 7/24/2000) (Option -- Variable Name -- Value)
 1197 
 1198   Variable      --  $functVarDefault      --  'x'
 1199   Relative Tolerance    --  $functRelPercentTolDefault    --  .1
 1200   Absolute Tolerance    --  $functAbsTolDefault     --  .001
 1201   Lower Limit     --  $functLLimitDefault     --  .0000001
 1202   Upper Limit     --  $functULimitDefault     --  1
 1203   Number of Points    --  $functNumOfPoints     --  3
 1204   Zero Level      --  $functZeroLevelDefault      --  1E-14
 1205   Zero Level Tolerance    --  $functZeroLevelTolDefault   --  1E-12
 1206   Maximum Constant    --  $functMaxConstantOfIntegration    --  1E8
 1207     of Integration
 1208 
 1209 =cut
 1210 
 1211 
 1212 
 1213 =head3 fun_cmp()
 1214 
 1215 Compares a function or a list of functions, using a named hash of options to set
 1216 parameters. This can make for more readable code than using the function_cmp()
 1217 style, but some people find one or the other easier to remember.
 1218 
 1219 ANS( fun_cmp( answer or answer_array_ref, options_hash ) );
 1220 
 1221   1. a string containing the correct function, or a reference to an
 1222     array of correct functions
 1223   2. a hash containing the following items (all optional):
 1224     var           --  either the number of variables or a reference to an
 1225                       array of variable names (see below)
 1226     limits            --  reference to an array of arrays of limits (see below), or:
 1227     mode            --  'std' (default) (function must match exactly), or:
 1228                     'antider' (function must match up to a constant)
 1229     relTol            --  (default) a relative tolerance (as a percentage), or:
 1230     tol           --  an absolute tolerance for error
 1231     numPoints         --  the number of points to evaluate the function at
 1232     maxConstantOfIntegration      --  maximum size of the constant of integration
 1233     zeroLevel         --  if the correct answer is this close to zero, then
 1234                       zeroLevelTol applies
 1235     zeroLevelTol          --  absolute tolerance to allow when answer is close to zero
 1236     params                an array of "free" parameters which can be used to adapt
 1237                     the correct answer to the submitted answer. (e.g. ['c'] for
 1238                     a constant of integration in the answer x^3/3 + c.
 1239     debug           --  when set to 1 this provides extra information while checking the
 1240                         the answer.
 1241 
 1242   Returns an answer evaluator, or (if given a reference to an array
 1243   of answers), a list of answer evaluators
 1244 
 1245 ANSWER:
 1246 
 1247   The answer must be in the form of a string. The answer can contain
 1248   functions, pi, e, and arithmetic operations. However, the correct answer
 1249   string follows a slightly stricter syntax than student answers; specifically,
 1250   there is no implicit multiplication. So the correct answer must be "3*x" rather
 1251   than "3 x". Students can still enter "3 x".
 1252 
 1253 VARIABLES:
 1254 
 1255   The var parameter can contain either a number or a reference to an array of
 1256   variable names. If it contains a number, the variables are named automatically
 1257   as follows: 1 variable  --  x
 1258       2 variables --  x, y
 1259       3 variables --  x, y, z
 1260       4 or more --  x_1, x_2, x_3, etc.
 1261   If the var parameter contains a reference to an array of variable names, then
 1262   the number of variables is determined by the number of items in the array. A
 1263   reference to an array is created with brackets, e.g. "var => ['r', 's', 't']".
 1264   If only one variable is being used, you can write either "var => ['t']" for
 1265   consistency or "var => 't'" as a shortcut. The default is one variable, x.
 1266 
 1267 LIMITS:
 1268 
 1269   Limits are specified with the limits parameter. You may NOT use llimit/ulimit.
 1270   If you specify limits for one variable, you must specify them for all variables.
 1271   The limit parameter must be a reference to an array of arrays of the form
 1272   [lower_limit. upper_limit], each array corresponding to the lower and upper
 1273   endpoints of the (half-open) domain of one variable. For example,
 1274   "vars => 2, limits => [[0,2], [-3,8]]" would cause x to be evaluated in [0,2) and
 1275   y to be evaluated in [-3,8). If only one variable is being used, you can write
 1276   either "limits => [[0,3]]" for consistency or "limits => [0,3]" as a shortcut.
 1277 
 1278 EXAMPLES:
 1279 
 1280   fun_cmp( "3*x" )  --  standard compare, variable is x
 1281   fun_cmp( ["3*x", "4*x+3", "3*x**2"] ) --  standard compare, defaults used for all three functions
 1282   fun_cmp( "3*t", var => 't' )  --  standard compare, variable is t
 1283   fun_cmp( "5*x*y*z", var => 3 )  --  x, y and z are the variables
 1284   fun_cmp( "5*x", mode => 'antider' ) --  student answer must match up to constant (i.e., 5x+C)
 1285   fun_cmp( ["3*x*y", "4*x*y"], limits => [[0,2], [5,7]] ) --  x evaluated in [0,2)
 1286                                 y evaluated in [5,7)
 1287 
 1288 =cut
 1289 
 1290 sub fun_cmp {
 1291   my $correctAnswer = shift @_;
 1292   my %opt = @_;
 1293 
 1294     assign_option_aliases( \%opt,
 1295         'vars'    =>  'var',    # set the standard option 'var' to the one specified as vars
 1296           'domain'  =>  'limits', # set the standard option 'limits' to the one specified as domain
 1297           'reltol'    =>  'relTol',
 1298           'param'   =>  'params',
 1299     );
 1300 
 1301     set_default_options(  \%opt,
 1302         'var'         =>  $functVarDefault,
 1303             'params'        =>  [],
 1304         'limits'        =>  [[$functLLimitDefault, $functULimitDefault]],
 1305         'mode'          =>  'std',
 1306         'tolType'       =>    (defined($opt{tol}) ) ? 'absolute' : 'relative',
 1307         'tol'         =>  .01, # default mode should be relative, to obtain this tol must not be defined
 1308             'relTol'        =>  $functRelPercentTolDefault,
 1309         'numPoints'       =>  $functNumOfPoints,
 1310         'maxConstantOfIntegration'  =>  $functMaxConstantOfIntegration,
 1311         'zeroLevel'       =>  $functZeroLevelDefault,
 1312         'zeroLevelTol'      =>  $functZeroLevelTolDefault,
 1313             'debug'         =>  0,
 1314      );
 1315 
 1316     # allow var => 'x' as an abbreviation for var => ['x']
 1317   my %out_options = %opt;
 1318   unless ( ref($out_options{var}) eq 'ARRAY' ) {
 1319     $out_options{var} = [$out_options{var}];
 1320   }
 1321   # allow params => 'c' as an abbreviation for params => ['c']
 1322   unless ( ref($out_options{params}) eq 'ARRAY' ) {
 1323     $out_options{params} = [$out_options{params}];
 1324   }
 1325   my ($tolType, $tol);
 1326     if ($out_options{tolType} eq 'absolute') {
 1327     $tolType = 'absolute';
 1328     $tol = $out_options{'tol'};
 1329     delete($out_options{'relTol'}) if exists( $out_options{'relTol'} );
 1330   } else {
 1331     $tolType = 'relative';
 1332     $tol = $out_options{'relTol'};
 1333     delete($out_options{'tol'}) if exists( $out_options{'tol'} );
 1334   }
 1335 
 1336   my @output_list = ();
 1337   # thread over lists
 1338   my @ans_list = ();
 1339 
 1340   if ( ref($correctAnswer) eq 'ARRAY' ) {
 1341     @ans_list = @{$correctAnswer};
 1342   }
 1343   else {
 1344     push( @ans_list, $correctAnswer );
 1345   }
 1346 
 1347   # produce answer evaluators
 1348   foreach my $ans (@ans_list) {
 1349     push(@output_list,
 1350       FUNCTION_CMP(
 1351           'correctEqn'    =>  $ans,
 1352           'var'       =>  $out_options{'var'},
 1353           'limits'      =>  $out_options{'limits'},
 1354           'tolerance'     =>  $tol,
 1355           'tolType'     =>  $tolType,
 1356           'numPoints'     =>  $out_options{'numPoints'},
 1357           'mode'        =>  $out_options{'mode'},
 1358           'maxConstantOfIntegration'  =>  $out_options{'maxConstantOfIntegration'},
 1359           'zeroLevel'     =>  $out_options{'zeroLevel'},
 1360           'zeroLevelTol'    =>  $out_options{'zeroLevelTol'},
 1361           'params'      =>  $out_options{'params'},
 1362           'debug'       =>  $out_options{'debug'},
 1363       ),
 1364     );
 1365   }
 1366 
 1367   return (wantarray) ? @output_list : $output_list[0];
 1368 }
 1369 
 1370 =head3 Single-variable Function Comparisons
 1371 
 1372 There are four single-variable function answer evaluators: "normal," absolute
 1373 tolerance, antiderivative, and antiderivative with absolute tolerance. All
 1374 parameters (other than the correct equation) are optional.
 1375 
 1376  function_cmp( $correctEqn ) OR
 1377  function_cmp( $correctEqn, $var ) OR
 1378  function_cmp( $correctEqn, $var, $llimit, $ulimit ) OR
 1379  function_cmp( $correctEqn, $var, $llimit, $ulimit, $relPercentTol ) OR
 1380  function_cmp( $correctEqn, $var, $llimit, $ulimit,
 1381         $relPercentTol, $numPoints ) OR
 1382  function_cmp( $correctEqn, $var, $llimit, $ulimit,
 1383         $relPercentTol, $numPoints, $zeroLevel ) OR
 1384  function_cmp( $correctEqn, $var, $llimit, $ulimit, $relPercentTol, $numPoints,
 1385         $zeroLevel,$zeroLevelTol )
 1386 
 1387   $correctEqn   --  the correct equation, as a string
 1388   $var      --  the string representing the variable (optional)
 1389   $llimit     --  the lower limit of the interval to evaluate the
 1390               variable in (optional)
 1391   $ulimit     --  the upper limit of the interval to evaluate the
 1392               variable in (optional)
 1393   $relPercentTol  --  the error tolerance as a percentage (optional)
 1394   $numPoints    --  the number of points at which to evaluate the
 1395               variable (optional)
 1396   $zeroLevel    --  if the correct answer is this close to zero, then
 1397               zeroLevelTol applies (optional)
 1398   $zeroLevelTol --  absolute tolerance to allow when answer is close to zero
 1399 
 1400   function_cmp() uses standard comparison and relative tolerance. It takes a
 1401   string representing a single-variable function and compares the student
 1402   answer to that function numerically.
 1403 
 1404  function_cmp_up_to_constant( $correctEqn ) OR
 1405  function_cmp_up_to_constant( $correctEqn, $var ) OR
 1406  function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit ) OR
 1407  function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit,
 1408                 $relpercentTol ) OR
 1409  function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit,
 1410                 $relpercentTol, $numOfPoints ) OR
 1411  function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit,
 1412                 $relpercentTol, $numOfPoints,
 1413                 $maxConstantOfIntegration ) OR
 1414  function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit,
 1415                 $relpercentTol, $numOfPoints,
 1416                 $maxConstantOfIntegration, $zeroLevel)  OR
 1417  function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit,
 1418                 $relpercentTol, $numOfPoints,
 1419                 $maxConstantOfIntegration,
 1420                 $zeroLevel, $zeroLevelTol )
 1421 
 1422   $maxConstantOfIntegration --  the maximum size of the constant of
 1423                   integration
 1424 
 1425   function_cmp_up_to_constant() uses antiderivative compare and relative
 1426   tolerance. All options work exactly like function_cmp(), except of course
 1427   $maxConstantOfIntegration. It will accept as correct any function which
 1428   differs from $correctEqn by at most a constant; that is, if
 1429     $studentEqn = $correctEqn + C
 1430   the answer is correct.
 1431 
 1432  function_cmp_abs( $correctFunction ) OR
 1433  function_cmp_abs( $correctFunction, $var ) OR
 1434  function_cmp_abs( $correctFunction, $var, $llimit, $ulimit ) OR
 1435  function_cmp_abs( $correctFunction, $var, $llimit, $ulimit, $absTol ) OR
 1436  function_cmp_abs( $correctFunction, $var, $llimit, $ulimit, $absTol,
 1437           $numOfPoints )
 1438 
 1439   $absTol --  the tolerance as an absolute value
 1440 
 1441   function_cmp_abs() uses standard compare and absolute tolerance. All
 1442   other options work exactly as for function_cmp().
 1443 
 1444  function_cmp_up_to_constant_abs( $correctFunction ) OR
 1445  function_cmp_up_to_constant_abs( $correctFunction, $var ) OR
 1446  function_cmp_up_to_constant_abs( $correctFunction, $var, $llimit, $ulimit ) OR
 1447  function_cmp_up_to_constant_abs( $correctFunction, $var, $llimit, $ulimit,
 1448                   $absTol ) OR
 1449  function_cmp_up_to_constant_abs( $correctFunction, $var, $llimit, $ulimit,
 1450                   $absTol, $numOfPoints ) OR
 1451  function_cmp_up_to_constant_abs( $correctFunction, $var, $llimit, $ulimit,
 1452                   $absTol, $numOfPoints,
 1453                   $maxConstantOfIntegration )
 1454 
 1455   function_cmp_up_to_constant_abs() uses antiderivative compare
 1456   and absolute tolerance. All other options work exactly as with
 1457   function_cmp_up_to_constant().
 1458 
 1459 Examples:
 1460 
 1461   ANS( function_cmp( "cos(x)" ) ) --  Accepts cos(x), sin(x+pi/2),
 1462     sin(x)^2 + cos(x) + cos(x)^2 -1, etc. This assumes
 1463     $functVarDefault has been set to "x".
 1464   ANS( function_cmp( $answer, "t" ) ) --  Assuming $answer is "cos(t)",
 1465     accepts cos(t), etc.
 1466   ANS( function_cmp_up_to_constant( "cos(x)" ) )  --  Accepts any
 1467     antiderivative of sin(x), e.g. cos(x) + 5.
 1468   ANS( function_cmp_up_to_constant( "cos(z)", "z" ) ) --  Accepts any
 1469     antiderivative of sin(z), e.g. sin(z+pi/2) + 5.
 1470 
 1471 =cut
 1472 
 1473 sub adaptive_function_cmp {
 1474   my $correctEqn = shift;
 1475   my %options = @_;
 1476   set_default_options(  \%options,
 1477       'vars'      =>  [qw( x y )],
 1478                   'params'    =>  [],
 1479                   'limits'    =>  [ [0,1], [0,1]],
 1480                   'reltol'    =>  $main::functRelPercentTolDefault,
 1481                   'numPoints'   =>  $main::functNumOfPoints,
 1482                   'zeroLevel'   =>  $main::functZeroLevelDefault,
 1483                   'zeroLevelTol'    =>  $main::functZeroLevelTolDefault,
 1484                   'debug'     =>  0,
 1485   );
 1486 
 1487     my $var_ref = $options{'vars'};
 1488     my $ra_params = $options{ 'params'};
 1489     my $limit_ref = $options{'limits'};
 1490     my $relPercentTol= $options{'reltol'};
 1491     my $numPoints = $options{'numPoints'};
 1492     my $zeroLevel = $options{'zeroLevel'};
 1493     my $zeroLevelTol = $options{'zeroLevelTol'};
 1494 
 1495   FUNCTION_CMP( 'correctEqn'          =>  $correctEqn,
 1496       'var'           =>  $var_ref,
 1497       'limits'          =>  $limit_ref,
 1498       'tolerance'         =>  $relPercentTol,
 1499       'tolType'         =>  'relative',
 1500       'numPoints'         =>  $numPoints,
 1501       'mode'            =>  'std',
 1502       'maxConstantOfIntegration'      =>  10**100,
 1503       'zeroLevel'         =>  $zeroLevel,
 1504       'zeroLevelTol'          =>  $zeroLevelTol,
 1505       'scale_norm'                      =>    1,
 1506       'params'                          =>    $ra_params,
 1507       'debug'               =>  $options{debug} ,
 1508   );
 1509 }
 1510 
 1511 sub function_cmp {
 1512   my ($correctEqn,$var,$llimit,$ulimit,$relPercentTol,$numPoints,$zeroLevel,$zeroLevelTol) = @_;
 1513 
 1514   if ( (scalar(@_) == 3) or (scalar(@_) > 8) or (scalar(@_) == 0) ) {
 1515     function_invalid_params( $correctEqn );
 1516   }
 1517   else {
 1518     FUNCTION_CMP( 'correctEqn'          =>  $correctEqn,
 1519         'var'           =>  $var,
 1520         'limits'          =>  [$llimit, $ulimit],
 1521         'tolerance'         =>  $relPercentTol,
 1522         'tolType'         =>  'relative',
 1523         'numPoints'         =>  $numPoints,
 1524         'mode'            =>  'std',
 1525         'maxConstantOfIntegration'      =>  0,
 1526         'zeroLevel'         =>  $zeroLevel,
 1527         'zeroLevelTol'          =>  $zeroLevelTol
 1528           );
 1529   }
 1530 }
 1531 
 1532 sub function_cmp_up_to_constant { ## for antiderivative problems
 1533   my ($correctEqn,$var,$llimit,$ulimit,$relPercentTol,$numPoints,$maxConstantOfIntegration,$zeroLevel,$zeroLevelTol) = @_;
 1534 
 1535   if ( (scalar(@_) == 3) or (scalar(@_) > 9) or (scalar(@_) == 0) ) {
 1536     function_invalid_params( $correctEqn );
 1537   }
 1538   else {
 1539     FUNCTION_CMP( 'correctEqn'          =>  $correctEqn,
 1540         'var'           =>  $var,
 1541         'limits'          =>  [$llimit, $ulimit],
 1542         'tolerance'         =>  $relPercentTol,
 1543         'tolType'         =>  'relative',
 1544         'numPoints'         =>  $numPoints,
 1545         'mode'            =>  'antider',
 1546         'maxConstantOfIntegration'      =>  $maxConstantOfIntegration,
 1547         'zeroLevel'         =>  $zeroLevel,
 1548         'zeroLevelTol'          =>  $zeroLevelTol
 1549           );
 1550   }
 1551 }
 1552 
 1553 sub function_cmp_abs {      ## similar to function_cmp but uses absolute tolerance
 1554   my ($correctEqn,$var,$llimit,$ulimit,$absTol,$numPoints) = @_;
 1555 
 1556   if ( (scalar(@_) == 3) or (scalar(@_) > 6) or (scalar(@_) == 0) ) {
 1557     function_invalid_params( $correctEqn );
 1558   }
 1559   else {
 1560     FUNCTION_CMP( 'correctEqn'      =>  $correctEqn,
 1561         'var'       =>  $var,
 1562         'limits'      =>  [$llimit, $ulimit],
 1563         'tolerance'     =>  $absTol,
 1564         'tolType'     =>  'absolute',
 1565         'numPoints'     =>  $numPoints,
 1566         'mode'        =>  'std',
 1567         'maxConstantOfIntegration'  =>  0,
 1568         'zeroLevel'     =>  0,
 1569         'zeroLevelTol'      =>  0
 1570     );
 1571   }
 1572 }
 1573 
 1574 
 1575 sub function_cmp_up_to_constant_abs  {  ## for antiderivative problems
 1576                     ## similar to function_cmp_up_to_constant
 1577                     ## but uses absolute tolerance
 1578   my ($correctEqn,$var,$llimit,$ulimit,$absTol,$numPoints,$maxConstantOfIntegration) = @_;
 1579 
 1580   if ( (scalar(@_) == 3) or (scalar(@_) > 7) or (scalar(@_) == 0) ) {
 1581     function_invalid_params( $correctEqn );
 1582   }
 1583 
 1584   else {
 1585     FUNCTION_CMP( 'correctEqn'          =>  $correctEqn,
 1586         'var'           =>  $var,
 1587         'limits'          =>  [$llimit, $ulimit],
 1588         'tolerance'         =>  $absTol,
 1589         'tolType'         =>  'absolute',
 1590         'numPoints'         =>  $numPoints,
 1591         'mode'            =>  'antider',
 1592         'maxConstantOfIntegration'      =>  $maxConstantOfIntegration,
 1593         'zeroLevel'         =>  0,
 1594         'zeroLevelTol'          =>  0
 1595     );
 1596   }
 1597 }
 1598 
 1599 ## The following answer evaluator for comparing multivarable functions was
 1600 ## contributed by Professor William K. Ziemer
 1601 ## (Note: most of the multivariable functionality provided by Professor Ziemer
 1602 ## has now been integrated into fun_cmp and FUNCTION_CMP)
 1603 ############################
 1604 # W.K. Ziemer, Sep. 1999
 1605 # Math Dept. CSULB
 1606 # email: wziemer@csulb.edu
 1607 ############################
 1608 
 1609 =head3 multivar_function_cmp
 1610 
 1611 NOTE: this function is maintained for compatibility. fun_cmp() is
 1612     slightly preferred.
 1613 
 1614 usage:
 1615 
 1616   multivar_function_cmp( $answer, $var_reference, options)
 1617     $answer       --  string, represents function of several variables
 1618     $var_reference    --  number (of variables), or list reference (e.g. ["var1","var2"] )
 1619   options:
 1620     $limit_reference  --  reference to list of lists (e.g. [[1,2],[3,4]])
 1621     $relPercentTol    --  relative percent tolerance in answer
 1622     $numPoints      --  number of points to sample in for each variable
 1623     $zeroLevel      --  if the correct answer is this close to zero, then zeroLevelTol applies
 1624     $zeroLevelTol   --  absolute tolerance to allow when answer is close to zero
 1625 
 1626 =cut
 1627 
 1628 sub multivar_function_cmp {
 1629   my ($correctEqn,$var_ref,$limit_ref,$relPercentTol,$numPoints,$zeroLevel,$zeroLevelTol) = @_;
 1630 
 1631   if ( (scalar(@_) > 7) or (scalar(@_) < 2) ) {
 1632     function_invalid_params( $correctEqn );
 1633   }
 1634 
 1635   FUNCTION_CMP( 'correctEqn'      =>  $correctEqn,
 1636       'var'       =>  $var_ref,
 1637       'limits'      =>  $limit_ref,
 1638       'tolerance'     =>  $relPercentTol,
 1639       'tolType'     =>  'relative',
 1640       'numPoints'     =>  $numPoints,
 1641       'mode'        =>  'std',
 1642       'maxConstantOfIntegration'  =>  0,
 1643       'zeroLevel'     =>  $zeroLevel,
 1644       'zeroLevelTol'      =>  $zeroLevelTol
 1645   );
 1646 }
 1647 
 1648 ## LOW-LEVEL ROUTINE -- NOT NORMALLY FOR END USERS -- USE WITH CAUTION
 1649 ## NOTE: PG_answer_eval is used instead of PG_restricted_eval in order to insure that the answer
 1650 ## evaluated within the context of the package the problem was originally defined in.
 1651 ## Includes multivariable modifications contributed by Professor William K. Ziemer
 1652 ##
 1653 ## IN:  a hash consisting of the following keys (error checking to be added later?)
 1654 ##      correctEqn      --  the correct equation as a string
 1655 ##      var       --  the variable name as a string,
 1656 ##                or a reference to an array of variables
 1657 ##      limits        --  reference to an array of arrays of type [lower,upper]
 1658 ##      tolerance     --  the allowable margin of error
 1659 ##      tolType       --  'relative' or 'absolute'
 1660 ##      numPoints     --  the number of points to evaluate the function at
 1661 ##      mode        --  'std' or 'antider'
 1662 ##      maxConstantOfIntegration  --  maximum size of the constant of integration
 1663 ##      zeroLevel     --  if the correct answer is this close to zero,
 1664 ##                        then zeroLevelTol applies
 1665 ##      zeroLevelTol      --  absolute tolerance to allow when answer is close to zero
 1666 
 1667 
 1668 sub FUNCTION_CMP {
 1669   my %func_params = @_;
 1670 
 1671   my $correctEqn          = $func_params{'correctEqn'};
 1672   my $var           = $func_params{'var'};
 1673   my $ra_limits         = $func_params{'limits'};
 1674   my $tol           = $func_params{'tolerance'};
 1675   my $tolType         = $func_params{'tolType'};
 1676   my $numPoints         = $func_params{'numPoints'};
 1677   my $mode          = $func_params{'mode'};
 1678   my $maxConstantOfIntegration      = $func_params{'maxConstantOfIntegration'};
 1679   my $zeroLevel         = $func_params{'zeroLevel'};
 1680   my $zeroLevelTol        = $func_params{'zeroLevelTol'};
 1681 
 1682 
 1683     # Check that everything is defined:
 1684     $func_params{debug} = 0 unless defined($func_params{debug});
 1685     $mode = 'std' unless defined($mode);
 1686     my @VARS = get_var_array( $var );
 1687   my @limits = get_limits_array( $ra_limits );
 1688   my @PARAMS = ();
 1689   @PARAMS = @{$func_params{'params'}} if defined($func_params{'params'});
 1690 
 1691   if ($mode eq 'antider' ) {
 1692     # doctor the equation to allow addition of a constant
 1693     my $CONSTANT_PARAM = 'Q';  # unfortunately parameters must be single letters.
 1694                    # There is the possibility of conflict here.
 1695                    #  'Q' seemed less dangerous than  'C'.
 1696     $correctEqn = "( $correctEqn ) + $CONSTANT_PARAM";
 1697     push(@PARAMS, $CONSTANT_PARAM);
 1698   }
 1699     my $dim_of_param_space = @PARAMS;      # dimension of equivalence space
 1700 
 1701   if( $tolType eq 'relative' ) {
 1702     $tol = $functRelPercentTolDefault     unless defined $tol;
 1703     $tol *= .01;
 1704   }
 1705   else {
 1706     $tol = $functAbsTolDefault        unless defined $tol;
 1707   }
 1708 
 1709   #loop ensures that number of limits matches number of variables
 1710   for( my $i = 0; $i < scalar(@VARS); $i++ ) {
 1711     $limits[$i][0] = $functLLimitDefault      unless defined $limits[$i][0];
 1712     $limits[$i][1] = $functULimitDefault      unless defined $limits[$i][1];
 1713   }
 1714   $numPoints = $functNumOfPoints          unless defined $numPoints;
 1715   $maxConstantOfIntegration = $functMaxConstantOfIntegration  unless defined $maxConstantOfIntegration;
 1716   $zeroLevel = $functZeroLevelDefault       unless defined $zeroLevel;
 1717   $zeroLevelTol = $functZeroLevelTolDefault     unless defined $zeroLevelTol;
 1718 
 1719   $func_params{'var'}       = $var;
 1720       $func_params{'limits'}        = \@limits;
 1721       $func_params{'tolerance'}     = $tol;
 1722       $func_params{'tolType'}       = $tolType;
 1723       $func_params{'numPoints'}     = $numPoints;
 1724       $func_params{'mode'}        = $mode;
 1725       $func_params{'maxConstantOfIntegration'}  = $maxConstantOfIntegration;
 1726       $func_params{'zeroLevel'}     = $zeroLevel;
 1727       $func_params{'zeroLevelTol'}        =   $zeroLevelTol;
 1728 
 1729 ########################################################
 1730 #   End of cleanup of calling parameters
 1731 ########################################################
 1732   my $i;            #for use with loops
 1733   my $PGanswerMessage = "";
 1734   my $originalCorrEqn = $correctEqn;
 1735 
 1736 #prepare the correct answer and check it's syntax
 1737       my $rh_correct_ans = new AnswerHash;
 1738   $rh_correct_ans->input($correctEqn);
 1739   $rh_correct_ans = check_syntax($rh_correct_ans);
 1740   warn  $rh_correct_ans->{error_message} if $rh_correct_ans->{error_flag};
 1741   $rh_correct_ans->clear_error();
 1742   $rh_correct_ans = function_from_string2($rh_correct_ans, ra_vars => [ @VARS, @PARAMS ],
 1743                                                            store_in =>'rf_correct_ans',
 1744                                                            debug =>  $func_params{debug});
 1745   my $correct_eqn_sub = $rh_correct_ans->{rf_correct_ans};
 1746   warn $rh_correct_ans->{error_message} if $rh_correct_ans->{error_flag};
 1747 
 1748 #create the evaluation points
 1749   my $random_for_answers = new PGrandom($main::PG_original_problemSeed);
 1750       my $NUMBER_OF_STEPS_IN_RANDOM = 1000;    # determines the granularity of the random_for_answers number generator
 1751   my (@evaluation_points);
 1752   for( my $count = 0; $count < @PARAMS+1+$numPoints; $count++ ) {
 1753       my (@vars,$iteration_limit);
 1754     for( my $i = 0; $i < @VARS; $i++ ) {
 1755       my $iteration_limit = 10;
 1756       while (  0 < --$iteration_limit ) {  # make sure that the endpoints of the interval are not included
 1757           $vars[$i] = $random_for_answers->random($limits[$i][0], $limits[$i][1], abs($limits[$i][1] - $limits[$i][0])/$NUMBER_OF_STEPS_IN_RANDOM );
 1758           last if $vars[$i]!=$limits[$i][0] and $vars[$i]!=$limits[$i][1];
 1759         }
 1760         warn "Unable to properly choose  evaluation points for this function in the interval ( $limits[$i][0] , $limits[$i][1] )"
 1761           if $iteration_limit == 0;
 1762     };
 1763 
 1764     push(@evaluation_points,\@vars);
 1765   }
 1766   my $evaluation_points = Matrix->new_from_array_ref(\@evaluation_points);
 1767 
 1768   #my $COEFFS = determine_param_coeffs($correct_eqn_sub,$evaluation_points[0],$numOfParameters);
 1769       #warn "coeff", join(" | ", @{$COEFFS});
 1770 
 1771 #construct the answer evaluator
 1772     my $answer_evaluator = new AnswerEvaluator;
 1773     $answer_evaluator->{debug} = $func_params{debug};
 1774     $answer_evaluator->ans_hash(  correct_ans     =>  $originalCorrEqn,
 1775           rf_correct_ans    =>  $rh_correct_ans->{rf_correct_ans},
 1776           evaluation_points =>  \@evaluation_points,
 1777           ra_param_vars     =>  \@PARAMS,
 1778           ra_vars     =>  \@VARS,
 1779           type      =>  'function',
 1780     );
 1781 
 1782     $answer_evaluator->install_pre_filter(\&check_syntax);
 1783     $answer_evaluator->install_pre_filter(\&function_from_string2, ra_vars => \@VARS,debug=>$func_params{debug},); # @VARS has been guaranteed to be an array, $var might be a single string.
 1784     $answer_evaluator->install_pre_filter(\&best_approx_parameters, %func_params, param_vars => \@PARAMS);
 1785     $answer_evaluator->install_evaluator(\&calculate_difference_vector, %func_params);
 1786     $answer_evaluator->install_evaluator(\&is_zero_array, tolerance => $tol );
 1787     $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; $rh_ans->clear_error('SYNTAX'); $rh_ans;} );
 1788     $answer_evaluator->install_post_filter(
 1789             sub {my $rh_ans = shift;
 1790             if ($rh_ans->catch_error('EVAL') ) {
 1791               $rh_ans->{ans_message} = $rh_ans->{error_message};
 1792               $rh_ans->clear_error('EVAL');
 1793             }
 1794             $rh_ans;
 1795           }
 1796   );
 1797     $answer_evaluator;
 1798 }
 1799 
 1800 
 1801 ## LOW-LEVEL ROUTINE -- NOT NORMALLY FOR END USERS -- USE WITH CAUTION
 1802 ##
 1803 ## IN:  a hash containing the following items (error-checking to be added later?):
 1804 ##      correctAnswer --  the correct answer
 1805 ##      tolerance   --  the allowable margin of error
 1806 ##      tolType     --  'relative' or 'absolute'
 1807 ##      format      --  the display format of the answer
 1808 ##      mode      --  one of 'std', 'strict', 'arith', or 'frac';
 1809 ##                  determines allowable formats for the input
 1810 ##      zeroLevel   --  if the correct answer is this close to zero, then zeroLevelTol applies
 1811 ##      zeroLevelTol  --  absolute tolerance to allow when answer is close to zero
 1812 
 1813 
 1814 ##########################################################################
 1815 ##########################################################################
 1816 ## String answer evaluators
 1817 
 1818 =head2 String Answer Evaluators
 1819 
 1820 String answer evaluators compare a student string to the correct string.
 1821 Different filters can be applied to allow various degrees of variation.
 1822 Both the student and correct answers are subject to the same filters, to
 1823 ensure that there are no unexpected matches or rejections.
 1824 
 1825 String Filters
 1826 
 1827   remove_whitespace --  Removes all whitespace from the string.
 1828             It applies the following substitution
 1829             to the string:
 1830               $filteredAnswer =~ s/\s+//g;
 1831 
 1832   compress_whitespace --  Removes leading and trailing whitespace, and
 1833             replaces all other blocks of whitespace by a
 1834             single space. Applies the following substitutions:
 1835               $filteredAnswer =~ s/^\s*//;
 1836               $filteredAnswer =~ s/\s*$//;
 1837               $filteredAnswer =~ s/\s+/ /g;
 1838 
 1839   trim_whitespace   --  Removes leading and trailing whitespace.
 1840             Applies the following substitutions:
 1841               $filteredAnswer =~ s/^\s*//;
 1842               $filteredAnswer =~ s/\s*$//;
 1843 
 1844   ignore_case     --  Ignores the case of the string. More accurately,
 1845             it converts the string to uppercase (by convention).
 1846             Applies the following function:
 1847               $filteredAnswer = uc $filteredAnswer;
 1848 
 1849   ignore_order    --  Ignores the order of the letters in the string.
 1850             This is used for problems of the form "Choose all
 1851             that apply." Specifically, it removes all
 1852             whitespace and lexically sorts the letters in
 1853             ascending alphabetical order. Applies the following
 1854             functions:
 1855               $filteredAnswer = join( "", lex_sort(
 1856                 split( /\s*/, $filteredAnswer ) ) );
 1857 
 1858 =cut
 1859 
 1860 ################################
 1861 ## STRING ANSWER FILTERS
 1862 
 1863 ## IN:  --the string to be filtered
 1864 ##    --a list of the filters to use
 1865 ##
 1866 ## OUT: --the modified string
 1867 ##
 1868 ## Use this subroutine instead of the
 1869 ## individual filters below it
 1870 
 1871 sub str_filters {
 1872   my $stringToFilter = shift @_;
 1873   my @filters_to_use = @_;
 1874   my %known_filters = ( 'remove_whitespace'   =>  undef,
 1875         'compress_whitespace'   =>  undef,
 1876         'trim_whitespace'   =>  undef,
 1877         'ignore_case'     =>  undef,
 1878         'ignore_order'      =>  undef
 1879   );
 1880 
 1881   #test for unknown filters
 1882   my $filter;
 1883   foreach $filter (@filters_to_use) {
 1884     die "Unknown string filter $filter (try checking the parameters to str_cmp() )"
 1885                 unless exists $known_filters{$filter};
 1886   }
 1887 
 1888   if( grep( /remove_whitespace/i, @filters_to_use ) ) {
 1889     $stringToFilter = remove_whitespace( $stringToFilter );
 1890   }
 1891   if( grep( /compress_whitespace/i, @filters_to_use ) ) {
 1892     $stringToFilter = compress_whitespace( $stringToFilter );
 1893   }
 1894   if( grep( /trim_whitespace/i, @filters_to_use ) ) {
 1895     $stringToFilter = trim_whitespace( $stringToFilter );
 1896   }
 1897   if( grep( /ignore_case/i, @filters_to_use ) ) {
 1898     $stringToFilter = ignore_case( $stringToFilter );
 1899   }
 1900   if( grep( /ignore_order/i, @filters_to_use ) ) {
 1901     $stringToFilter = ignore_order( $stringToFilter );
 1902   }
 1903 
 1904   return $stringToFilter;
 1905 }
 1906 
 1907 sub remove_whitespace {
 1908   my $filteredAnswer = shift;
 1909 
 1910   $filteredAnswer =~ s/\s+//g;    # remove all whitespace
 1911 
 1912   return $filteredAnswer;
 1913 }
 1914 
 1915 sub compress_whitespace {
 1916   my $filteredAnswer = shift;
 1917 
 1918   $filteredAnswer =~ s/^\s*//;    # remove initial whitespace
 1919   $filteredAnswer =~ s/\s*$//;    # remove trailing whitespace
 1920   $filteredAnswer =~ s/\s+/ /g;   # replace spaces by single space
 1921 
 1922   return $filteredAnswer;
 1923 }
 1924 
 1925 sub trim_whitespace {
 1926   my $filteredAnswer = shift;
 1927 
 1928   $filteredAnswer =~ s/^\s*//;    # remove initial whitespace
 1929   $filteredAnswer =~ s/\s*$//;    # remove trailing whitespace
 1930 
 1931   return $filteredAnswer;
 1932 }
 1933 
 1934 sub ignore_case {
 1935   my $filteredAnswer = shift;
 1936 
 1937   $filteredAnswer = uc $filteredAnswer;
 1938 
 1939   return $filteredAnswer;
 1940 }
 1941 
 1942 sub ignore_order {
 1943   my $filteredAnswer = shift;
 1944 
 1945   $filteredAnswer = join( "", lex_sort( split( /\s*/, $filteredAnswer ) ) );
 1946 
 1947   return $filteredAnswer;
 1948 }
 1949 ################################
 1950 ## END STRING ANSWER FILTERS
 1951 
 1952 
 1953 =head3 str_cmp()
 1954 
 1955 Compares a string or a list of strings, using a named hash of options to set
 1956 parameters. This can make for more readable code than using the "mode"_str_cmp()
 1957 style, but some people find one or the other easier to remember.
 1958 
 1959 ANS( str_cmp( answer or answer_array_ref, options_hash ) );
 1960 
 1961   1. the correct answer or a reference to an array of answers
 1962   2. either a list of filters, or:
 1963      a hash consisting of
 1964     filters - a reference to an array of filters
 1965 
 1966   Returns an answer evaluator, or (if given a reference to an array of answers),
 1967   a list of answer evaluators
 1968 
 1969 FILTERS:
 1970 
 1971   remove_whitespace --  removes all whitespace
 1972   compress_whitespace --  removes whitespace from the beginning and end of the string,
 1973               and treats one or more whitespace characters in a row as a
 1974               single space (true by default)
 1975   trim_whitespace   --  removes whitespace from the beginning and end of the string
 1976   ignore_case   --  ignores the case of the letters (true by default)
 1977   ignore_order    --  ignores the order in which letters are entered
 1978 
 1979 EXAMPLES:
 1980 
 1981   str_cmp( "Hello" )  --  matches "Hello", "  hello" (same as std_str_cmp() )
 1982   str_cmp( ["Hello", "Goodbye"] ) --  same as std_str_cmp_list()
 1983   str_cmp( " hello ", trim_whitespace ) --  matches "hello", " hello  "
 1984   str_cmp( "ABC", filters => 'ignore_order' ) --  matches "ACB", "A B C", but not "abc"
 1985   str_cmp( "D E F", remove_whitespace, ignore_case )  --  matches "def" and "d e f" but not "fed"
 1986 
 1987 
 1988 =cut
 1989 
 1990 sub str_cmp {
 1991   my $correctAnswer = shift @_;
 1992   $correctAnswer = '' unless defined($correctAnswer);
 1993   my @options = @_;
 1994   my $ra_filters;
 1995 
 1996   # error-checking for filters occurs in the filters() subroutine
 1997   if( not defined( $options[0] ) ) {    # used with no filters as alias for std_str_cmp()
 1998     @options = ( 'compress_whitespace', 'ignore_case' );
 1999   }
 2000 
 2001   if( $options[0] eq 'filters' ) {    # using filters => [f1, f2, ...] notation
 2002     $ra_filters = $options[1];
 2003   }
 2004   else {            # using a list of filters
 2005     $ra_filters = \@options;
 2006   }
 2007 
 2008   # thread over lists
 2009   my @ans_list = ();
 2010 
 2011   if ( ref($correctAnswer) eq 'ARRAY' ) {
 2012     @ans_list = @{$correctAnswer};
 2013   }
 2014   else {
 2015     push( @ans_list, $correctAnswer );
 2016   }
 2017 
 2018   # final_answer;
 2019   my @output_list = ();
 2020 
 2021   foreach my $ans (@ans_list) {
 2022     push(@output_list, STR_CMP( 'correctAnswer' =>  $ans,
 2023             'filters' =>  $ra_filters,
 2024             'type'    =>  'str_cmp'
 2025          )
 2026     );
 2027   }
 2028 
 2029   return (wantarray) ? @output_list : $output_list[0] ;
 2030 }
 2031 
 2032 =head3 "mode"_str_cmp functions
 2033 
 2034 The functions of the the form "mode"_str_cmp() use different functions to
 2035 specify which filters to apply. They take no options except the correct
 2036 string. There are also versions which accept a list of strings.
 2037 
 2038  std_str_cmp( $correctString )
 2039  std_str_cmp_list( @correctStringList )
 2040   Filters: compress_whitespace, ignore_case
 2041 
 2042  std_cs_str_cmp( $correctString )
 2043  std_cs_str_cmp_list( @correctStringList )
 2044   Filters: compress_whitespace
 2045 
 2046  strict_str_cmp( $correctString )
 2047  strict_str_cmp_list( @correctStringList )
 2048   Filters: trim_whitespace
 2049 
 2050  unordered_str_cmp( $correctString )
 2051  unordered_str_cmp_list( @correctStringList )
 2052   Filters: ignore_order, ignore_case
 2053 
 2054  unordered_cs_str_cmp( $correctString )
 2055  unordered_cs_str_cmp_list( @correctStringList )
 2056   Filters: ignore_order
 2057 
 2058  ordered_str_cmp( $correctString )
 2059  ordered_str_cmp_list( @correctStringList )
 2060   Filters: remove_whitespace, ignore_case
 2061 
 2062  ordered_cs_str_cmp( $correctString )
 2063  ordered_cs_str_cmp_list( @correctStringList )
 2064   Filters: remove_whitespace
 2065 
 2066 Examples
 2067 
 2068   ANS( std_str_cmp( "W. Mozart" ) ) --  Accepts "W. Mozart", "W. MOZarT",
 2069     and so forth. Case insensitive. All internal spaces treated
 2070     as single spaces.
 2071   ANS( std_cs_str_cmp( "Mozart" ) ) --  Rejects "mozart". Same as
 2072     std_str_cmp() but case sensitive.
 2073   ANS( strict_str_cmp( "W. Mozart" ) )  --  Accepts only the exact string.
 2074   ANS( unordered_str_cmp( "ABC" ) ) --  Accepts "a c B", "CBA" and so forth.
 2075     Unordered, case insensitive, spaces ignored.
 2076   ANS( unordered_cs_str_cmp( "ABC" ) )  --  Rejects "abc". Same as
 2077     unordered_str_cmp() but case sensitive.
 2078   ANS( ordered_str_cmp( "ABC" ) ) --  Accepts "a b C", "A B C" and so forth.
 2079     Ordered, case insensitive, spaces ignored.
 2080   ANS( ordered_cs_str_cmp( "ABC" ) )  --  Rejects "abc", accepts "A BC" and
 2081     so forth. Same as ordered_str_cmp() but case sensitive.
 2082 
 2083 =cut
 2084 
 2085 sub std_str_cmp {         # compare strings
 2086   my $correctAnswer = shift @_;
 2087   my @filters = ( 'compress_whitespace', 'ignore_case' );
 2088   my $type = 'std_str_cmp';
 2089   STR_CMP(  'correctAnswer' =>  $correctAnswer,
 2090       'filters' =>  \@filters,
 2091       'type'    =>  $type
 2092   );
 2093 }
 2094 
 2095 sub std_str_cmp_list {        # alias for std_str_cmp
 2096   my @answerList = @_;
 2097   my @output;
 2098   while (@answerList) {
 2099     push( @output, std_str_cmp(shift @answerList) );
 2100   }
 2101   @output;
 2102 }
 2103 
 2104 sub std_cs_str_cmp {        # compare strings case sensitive
 2105   my $correctAnswer = shift @_;
 2106   my @filters = ( 'compress_whitespace' );
 2107   my $type = 'std_cs_str_cmp';
 2108   STR_CMP(  'correctAnswer' =>  $correctAnswer,
 2109       'filters' =>  \@filters,
 2110       'type'    =>  $type
 2111   );
 2112 }
 2113 
 2114 sub std_cs_str_cmp_list {     # alias for std_cs_str_cmp
 2115   my @answerList = @_;
 2116   my @output;
 2117   while (@answerList) {
 2118     push( @output, std_cs_str_cmp(shift @answerList) );
 2119   }
 2120   @output;
 2121 }
 2122 
 2123 sub strict_str_cmp {        # strict string compare
 2124   my $correctAnswer = shift @_;
 2125   my @filters = ( 'trim_whitespace' );
 2126   my $type = 'strict_str_cmp';
 2127   STR_CMP(  'correctAnswer' =>  $correctAnswer,
 2128       'filters' =>  \@filters,
 2129       'type'    =>  $type
 2130   );
 2131 }
 2132 
 2133 sub strict_str_cmp_list {     # alias for strict_str_cmp
 2134   my @answerList = @_;
 2135   my @output;
 2136   while (@answerList) {
 2137     push( @output, strict_str_cmp(shift @answerList) );
 2138   }
 2139   @output;
 2140 }
 2141 
 2142 sub unordered_str_cmp {       # unordered, case insensitive, spaces ignored
 2143   my $correctAnswer = shift @_;
 2144   my @filters = ( 'ignore_order', 'ignore_case' );
 2145   my $type = 'unordered_str_cmp';
 2146   STR_CMP(  'correctAnswer'   =>  $correctAnswer,
 2147       'filters'   =>  \@filters,
 2148       'type'      =>  $type
 2149   );
 2150 }
 2151 
 2152 sub unordered_str_cmp_list {    # alias for unordered_str_cmp
 2153   my @answerList = @_;
 2154   my @output;
 2155   while (@answerList) {
 2156     push( @output, unordered_str_cmp(shift @answerList) );
 2157   }
 2158   @output;
 2159 }
 2160 
 2161 sub unordered_cs_str_cmp {      # unordered, case sensitive, spaces ignored
 2162   my $correctAnswer = shift @_;
 2163   my @filters = ( 'ignore_order' );
 2164   my $type = 'unordered_cs_str_cmp';
 2165   STR_CMP(  'correctAnswer'   =>  $correctAnswer,
 2166       'filters'   =>  \@filters,
 2167       'type'      =>  $type
 2168   );
 2169 }
 2170 
 2171 sub unordered_cs_str_cmp_list {   # alias for unordered_cs_str_cmp
 2172   my @answerList = @_;
 2173   my @output;
 2174   while (@answerList) {
 2175     push( @output, unordered_cs_str_cmp(shift @answerList) );
 2176   }
 2177   @output;
 2178 }
 2179 
 2180 sub ordered_str_cmp {       # ordered, case insensitive, spaces ignored
 2181   my $correctAnswer = shift @_;
 2182   my @filters = ( 'remove_whitespace', 'ignore_case' );
 2183   my $type = 'ordered_str_cmp';
 2184   STR_CMP(  'correctAnswer' =>  $correctAnswer,
 2185       'filters' =>  \@filters,
 2186       'type'    =>  $type
 2187   );
 2188 }
 2189 
 2190 sub ordered_str_cmp_list {      # alias for ordered_str_cmp
 2191   my @answerList = @_;
 2192   my @output;
 2193   while (@answerList) {
 2194     push( @output, ordered_str_cmp(shift @answerList) );
 2195   }
 2196   @output;
 2197 }
 2198 
 2199 sub ordered_cs_str_cmp {      # ordered,  case sensitive, spaces ignored
 2200   my $correctAnswer = shift @_;
 2201   my @filters = ( 'remove_whitespace' );
 2202   my $type = 'ordered_cs_str_cmp';
 2203   STR_CMP(  'correctAnswer' =>  $correctAnswer,
 2204       'filters' =>  \@filters,
 2205       'type'    =>  $type
 2206   );
 2207 }
 2208 
 2209 sub ordered_cs_str_cmp_list {   # alias for ordered_cs_str_cmp
 2210   my @answerList = @_;
 2211   my @output;
 2212   while (@answerList) {
 2213     push( @output, ordered_cs_str_cmp(shift @answerList) );
 2214   }
 2215   @output;
 2216 }
 2217 
 2218 
 2219 ## LOW-LEVEL ROUTINE -- NOT NORMALLY FOR END USERS -- USE WITH CAUTION
 2220 ##
 2221 ## IN:  a hashtable with the following entries (error-checking to be added later?):
 2222 ##      correctAnswer --  the correct answer, before filtering
 2223 ##      filters     --  reference to an array containing the filters to be applied
 2224 ##      type      --  a string containing the type of answer evaluator in use
 2225 ## OUT: a reference to an answer evaluator subroutine
 2226 
 2227 sub STR_CMP {
 2228   my %str_params = @_;
 2229   $str_params{'correctAnswer'} = str_filters( $str_params{'correctAnswer'}, @{$str_params{'filters'}} );
 2230   my $answer_evaluator = sub {
 2231     my $in = shift @_;
 2232     $in = '' unless defined $in;
 2233     my $original_student_ans = $in;
 2234     $in = str_filters( $in, @{$str_params{'filters'}} );
 2235     my $correctQ = ( $in eq $str_params{'correctAnswer'} ) ? 1: 0;
 2236     my $ans_hash = new AnswerHash(    'score'       =>  $correctQ,
 2237               'correct_ans'     =>  $str_params{'correctAnswer'},
 2238               'student_ans'     =>  $in,
 2239               'ans_message'     =>  '',
 2240               'type'        =>  $str_params{'type'},
 2241               'preview_text_string'   =>  $in,
 2242               'preview_latex_string'    =>  $in,
 2243               'original_student_ans'    =>  $original_student_ans
 2244     );
 2245     return $ans_hash;
 2246   };
 2247   return $answer_evaluator;
 2248 }
 2249 
 2250 ##########################################################################
 2251 ##########################################################################
 2252 ## Miscellaneous answer evaluators
 2253 
 2254 =head2 Miscellaneous Answer Evaluators (Checkboxes and Radio Buttons)
 2255 
 2256 These evaluators do not fit any of the other categories.
 2257 
 2258 checkbox_cmp( $correctAnswer )
 2259 
 2260   $correctAnswer  --  a string containing the names of the correct boxes,
 2261             e.g. "ACD". Note that this means that individual
 2262             checkbox names can only be one character. Internally,
 2263             this is largely the same as unordered_cs_str_cmp().
 2264 
 2265 radio_cmp( $correctAnswer )
 2266 
 2267   $correctAnswer  --  a string containing the name of the correct radio
 2268             button, e.g. "Choice1". This is case sensitive and
 2269             whitespace sensitive, so the correct answer must match
 2270             the name of the radio button exactly.
 2271 
 2272 =cut
 2273 
 2274 # added 6/14/2000 by David Etlinger
 2275 # because of the conversion of the answer
 2276 # string to an array, I thought it better not
 2277 # to force STR_CMP() to work with this
 2278 sub checkbox_cmp {
 2279   my  $correctAnswer = shift @_;
 2280   $correctAnswer = str_filters( $correctAnswer, 'ignore_order' );
 2281 
 2282   my  $answer_evaluator = sub {
 2283     my $in = shift @_;
 2284     $in = '' unless defined $in;      #in case no boxes checked
 2285 
 2286     my @temp = split( "\0", $in );      #convert "\0"-delimited string to array...
 2287     $in = join( "", @temp );        #and then to a single no-delimiter string
 2288 
 2289     my $original_student_ans = $in;     #well, almost original
 2290     $in = str_filters( $in, 'ignore_order' );
 2291 
 2292     my $correctQ = ($in eq $correctAnswer) ? 1: 0;
 2293 
 2294     my $ans_hash = new AnswerHash(
 2295               'score'     =>  $correctQ,
 2296               'correct_ans'   =>  $correctAnswer,
 2297               'student_ans'   =>  $in,
 2298               'ans_message'   =>  "",
 2299               'type'      =>  "checkbox_cmp",
 2300               'preview_text_string' =>  $in,
 2301               'original_student_ans'  =>  $original_student_ans
 2302     );
 2303     return $ans_hash;
 2304 
 2305   };
 2306   return $answer_evaluator;
 2307 }
 2308 
 2309 #added 6/28/2000 by David Etlinger
 2310 #exactly the same as strict_str_cmp,
 2311 #but more intuitive to the user
 2312 sub radio_cmp {
 2313   strict_str_cmp( @_ );
 2314 }
 2315 
 2316 ##########################################################################
 2317 ##########################################################################
 2318 ## Text and e-mail routines
 2319 
 2320 sub store_ans_at {
 2321   my $answerStringRef = shift;
 2322   my %options = @_;
 2323   my $ans_eval= '';
 2324   if ( ref($answerStringRef) eq 'SCALAR' ) {
 2325     $ans_eval= sub {
 2326       my $text = shift;
 2327       $text = '' unless defined($text);
 2328       $$answerStringRef = $$answerStringRef  . $text;
 2329       my $ans_hash = new AnswerHash(
 2330                'score'      =>  1,
 2331                'correct_ans'      =>  '',
 2332                'student_ans'      =>  $text,
 2333                'ans_message'      =>  '',
 2334                'type'       =>  'store_ans_at',
 2335                'original_student_ans'   =>  $text,
 2336                'preview_text_string'    =>  ''
 2337       );
 2338 
 2339     return $ans_hash;
 2340     };
 2341   }
 2342   else {
 2343     die "Syntax error: \n The argument to store_ans_at() must be a pointer to a scalar.\n(e.g.  store_ans_at(~~\$MSG) )\n\n";
 2344   }
 2345 
 2346   return $ans_eval;
 2347 }
 2348 
 2349 #### subroutines used in producing a questionnaire
 2350 #### these are at least good models for other answers of this type
 2351 
 2352 my $QUESTIONNAIRE_ANSWERS=''; #  stores the answers until it is time to send them
 2353        #  this must be initialized before the answer evaluators are run
 2354        #  but that happens long after all of the text in the problem is
 2355        #  evaluated.
 2356 # this is a utility script for cleaning up the answer output for display in
 2357 #the answers.
 2358 
 2359 sub DUMMY_ANSWER {
 2360   my $num = shift;
 2361   qq{<INPUT TYPE="HIDDEN" NAME="answer$num" VALUE="">}
 2362 }
 2363 
 2364 sub escapeHTML {
 2365   my $string = shift;
 2366   $string =~ s/\n/$BR/ge;
 2367   $string;
 2368 }
 2369 
 2370 # these next three subroutines show how to modify the "store_ans_at()" answer
 2371 # evaluator to add extra information before storing the info
 2372 # They provide a good model for how to tweak answer evaluators in special cases.
 2373 
 2374 sub anstext {
 2375   my $num = shift;
 2376   my $ans_eval_template = store_ans_at(\$QUESTIONNAIRE_ANSWERS);
 2377   my $ans_eval = sub {
 2378          my $text = shift;
 2379          $text = '' unless defined($text);
 2380          my $new_text = "\n$main::psvnNumber-Problem-$main::probNum-Question-$num:\n $text "; # modify entered text
 2381          my $out = &$ans_eval_template($new_text);       # standard evaluator
 2382          #warn "$QUESTIONNAIRE_ANSWERS";
 2383          $out->{student_ans} = escapeHTML($text);  #  restore original entered text
 2384          $out->{correct_ans} = "Question  $num answered";
 2385          $out->{original_student_ans} = escapeHTML($text);
 2386          $out;
 2387     };
 2388    $ans_eval;
 2389 }
 2390 
 2391 sub ansradio {
 2392   my $num = shift;
 2393   my $ans_eval_template = store_ans_at(\$QUESTIONNAIRE_ANSWERS);
 2394   my $ans_eval = sub {
 2395          my $text = shift;
 2396          $text = '' unless defined($text);
 2397          my $new_text = "\n$main::psvnNumber-Problem-$main::probNum-RADIO-$num:\n $text ";       # modify entered text
 2398          my $out = $ans_eval_template->($new_text);       # standard evaluator
 2399          $out->{student_ans} =escapeHTML($text);  # restore original entered text
 2400          $out->{original_student_ans} = escapeHTML($text);
 2401          $out;
 2402    };
 2403 
 2404    $ans_eval;
 2405 }
 2406 
 2407 sub anstext_non_anonymous {
 2408   ## this emails identifying information
 2409   my $num = shift;
 2410   my $ans_eval_template = store_ans_at(\$QUESTIONNAIRE_ANSWERS);
 2411   my $ans_eval = sub {
 2412          my $text = shift;
 2413          $text = '' unless defined($text);
 2414          my $new_text = "\n$main::psvnNumber-Problem-$main::probNum-Question-$num:\n$main::studentLogin $main::studentID $main::studentName\n$text "; # modify entered text
 2415          my $out = &$ans_eval_template($new_text);       # standard evaluator
 2416          #warn "$QUESTIONNAIRE_ANSWERS";
 2417          $out->{student_ans} = escapeHTML($text);  #  restore original entered text
 2418          $out->{correct_ans} = "Question  $num answered";
 2419          $out->{original_student_ans} = escapeHTML($text);
 2420          $out;
 2421     };
 2422    $ans_eval;
 2423 }
 2424 
 2425 
 2426 #  This is another example of how to modify an  answer evaluator to obtain
 2427 #  the desired behavior in a special case.  Here the object is to have
 2428 #  have the last answer trigger the send_mail_to subroutine which mails
 2429 #  all of the answers to the designated address.
 2430 #  (This address must be listed in PG_environment{'ALLOW_MAIL_TO'} or an error occurs.)
 2431 
 2432 sub mail_answers_to {  #accepts the last answer and mails off the result
 2433   my $user_address = shift;
 2434   my $ans_eval = sub {
 2435 
 2436     # then mail out all of the answers, including this last one.
 2437 
 2438     send_mail_to( $user_address,
 2439           'subject'   =>  "$main::courseName WeBWorK questionnaire",
 2440           'body'      =>  $QUESTIONNAIRE_ANSWERS,
 2441           'ALLOW_MAIL_TO'   =>  $main::ALLOW_MAIL_TO
 2442     );
 2443 
 2444     my $ans_hash = new AnswerHash(  'score'   =>  1,
 2445             'correct_ans' =>  '',
 2446             'student_ans' =>  'Answer recorded',
 2447             'ans_message' =>  '',
 2448             'type'    =>  'send_mail_to',
 2449     );
 2450 
 2451     return $ans_hash;
 2452   };
 2453 
 2454   return $ans_eval;
 2455 }
 2456 sub mail_answers_to2 {  #accepts the last answer and mails off the result
 2457   my $user_address = shift;
 2458   my $subject = shift;
 2459   $subject = "$main::courseName WeBWorK questionnaire" unless defined $subject;
 2460 
 2461   send_mail_to($user_address,
 2462       'subject'     => $subject,
 2463       'body'        => $QUESTIONNAIRE_ANSWERS,
 2464       'ALLOW_MAIL_TO'   => $main::ALLOW_MAIL_TO
 2465   );
 2466 }
 2467 
 2468 ##########################################################################
 2469 ##########################################################################
 2470 
 2471 
 2472 ###########################################################################
 2473 ### THE FOLLOWING ARE LOCAL SUBROUTINES THAT ARE MEANT TO BE CALLED ONLY FROM THIS SCRIPT.
 2474 
 2475 ## Internal routine that converts variables into the standard array format
 2476 ##
 2477 ## IN:  one of the following:
 2478 ##      an undefined value (i.e., no variable was specified)
 2479 ##      a reference to an array of variable names -- [var1, var2]
 2480 ##      a number (the number of variables desired) -- 3
 2481 ##      one or more variable names -- (var1, var2)
 2482 ## OUT: an array of variable names
 2483 
 2484 sub get_var_array {
 2485   my $in = shift @_;
 2486   my @out;
 2487 
 2488   if( not defined($in) ) {      #if nothing defined, build default array and return
 2489     @out = ( $functVarDefault );
 2490     return @out;
 2491   }
 2492   elsif( ref( $in ) eq 'ARRAY' ) {  #if given an array ref, dereference and return
 2493     return @{$in};
 2494   }
 2495   elsif( $in =~ /^\d+/ ) {      #if given a number, set up the array and return
 2496     if( $in == 1 ) {
 2497       $out[0] = 'x';
 2498     }
 2499     elsif( $in == 2 ) {
 2500       $out[0] = 'x';
 2501       $out[1] = 'y';
 2502     }
 2503     elsif( $in == 3 ) {
 2504       $out[0] = 'x';
 2505       $out[1] = 'y';
 2506       $out[2] = 'z';
 2507     }
 2508     else {  #default to the x_1, x_2, ... convention
 2509       my ($i, $tag);
 2510       for( $i=0; $i < $in; $i++ ) {
 2511               ## akp the above seems to be off by one 1/4/00
 2512         $tag = $i + 1;                            ## akp 1/4/00
 2513         $out[$i] = "${functVarDefault}_" . $tag;              ## akp 1/4/00
 2514       }
 2515     }
 2516     return @out;
 2517   }
 2518   else {            #if given one or more names, return as an array
 2519     unshift( @_, $in );
 2520     return @_;
 2521   }
 2522 }
 2523 
 2524 ## Internal routine that converts limits into the standard array of arrays format
 2525 ##  Some of the cases are probably unneccessary, but better safe than sorry
 2526 ##
 2527 ## IN:  one of the following:
 2528 ##      an undefined value (i.e., no limits were specified)
 2529 ##      a reference to an array of arrays of limits -- [[llim,ulim], [llim,ulim]]
 2530 ##      a reference to an array of limits -- [llim, ulim]
 2531 ##      an array of array references -- ([llim,ulim], [llim,ulim])
 2532 ##      an array of limits -- (llim,ulim)
 2533 ## OUT: an array of array references -- ([llim,ulim], [llim,ulim]) or ([llim,ulim])
 2534 
 2535 sub get_limits_array {
 2536   my $in = shift @_;
 2537   my @out;
 2538 
 2539   if( not defined($in) ) {        #if nothing defined, build default array and return
 2540     @out = ( [$functLLimitDefault, $functULimitDefault] );
 2541     return @out;
 2542   }
 2543   elsif( ref($in) eq 'ARRAY' ) {        #$in is either ref to array, or ref to array of refs
 2544     my @deref = @{$in};
 2545 
 2546     if( ref( $in->[0] ) eq 'ARRAY' ) {    #$in is a ref to an array of array refs
 2547       return @deref;
 2548     }
 2549     else {            #$in was just a ref to an array of numbers
 2550       @out = ( $in );
 2551       return @out;
 2552     }
 2553   }
 2554   else {              #$in was an array of references or numbers
 2555     unshift( @_, $in );
 2556 
 2557     if( ref($_[0]) eq 'ARRAY' ) {     #$in was an array of references, so just return it
 2558       return @_;
 2559     }
 2560     else {            #$in was an array of numbers
 2561       @out = ( \@_ );
 2562       return @out;
 2563     }
 2564   }
 2565 }
 2566 
 2567 #sub check_option_list {
 2568 # my $size = scalar(@_);
 2569 # if( ( $size % 2 ) != 0 ) {
 2570 #   warn "ERROR in answer evaluator generator:\n" .
 2571 #     "Usage: <CODE>str_cmp([\$ans1,  \$ans2],%options)</CODE>
 2572 #     or <CODE> num_cmp([\$num1, \$num2], %options)</CODE><BR>
 2573 #     A list of inputs must be inclosed in square brackets <CODE>[\$ans1, \$ans2]</CODE>";
 2574 # }
 2575 #}
 2576 
 2577 # simple subroutine to display an error message when
 2578 # function compares are called with invalid parameters
 2579 sub function_invalid_params {
 2580   my $correctEqn = shift @_;
 2581   my $error_response = sub {
 2582     my $PGanswerMessage = "Tell your professor that there is an error with the parameters " .
 2583             "to the function answer evaluator";
 2584     return ( 0, $correctEqn, "", $PGanswerMessage );
 2585   };
 2586   return $error_response;
 2587 }
 2588 
 2589 sub clean_up_error_msg {
 2590   my $msg = $_[0];
 2591   $msg =~ s/^\[[^\]]*\][^:]*://;
 2592   $msg =~ s/Unquoted string//g;
 2593   $msg =~ s/may\s+clash.*/does not make sense here/;
 2594   $msg =~ s/\sat.*line [\d]*//g;
 2595   $msg = 'error: '. $msg;
 2596 
 2597   return $msg;
 2598 }
 2599 
 2600 #formats the student and correct answer as specified
 2601 #format must be of a form suitable for sprintf (e.g. '%0.5g'),
 2602 #with the exception that a '#' at the end of the string
 2603 #will cause trailing zeros in the decimal part to be removed
 2604 sub prfmt {
 2605   my($number,$format) = @_;  # attention, the order of format and number are reversed
 2606   my $out;
 2607   if ($format) {
 2608     warn "Incorrect format used: $format. <BR> Format should look something like %4.5g<BR>"
 2609                 unless $format =~ /^\s*%\d*\.?\d*\w#?\s*$/;
 2610 
 2611     if( $format =~ s/#\s*$// ) {  # remove trailing zeros in the decimal
 2612       $out = sprintf( $format, $number );
 2613       $out =~ s/(\.\d*?)0+$/$1/;
 2614       $out =~ s/\.$//;      # in case all decimal digits were zero, remove the decimal
 2615       $out =~ s/e/E/g;        # only use capital E's for exponents. Little e is for 2.71828...
 2616     } elsif (is_a_number($number) ){
 2617       $out = sprintf( $format, $number );
 2618       $out =~ s/e/E/g;        # only use capital E's for exponents. Little e is for 2.71828...
 2619     } else { # number is probably a string representing an arithmetic expression
 2620       $out = $number;
 2621     }
 2622 
 2623   } else {
 2624     if (is_a_number($number)) {# only use capital E's for exponents. Little e is for 2.71828...
 2625       $out = $number;
 2626       $out =~ s/e/E/g;
 2627     } else { # number is probably a string representing an arithmetic expression
 2628       $out = $number;
 2629     }
 2630   }
 2631   return $out;
 2632 }
 2633 #########################################################################
 2634 # Filters for answer evaluators
 2635 #########################################################################
 2636 
 2637 =head2 Filters
 2638 
 2639 =pod
 2640 
 2641 A filter is a short subroutine with the following structure.  It accepts an
 2642 AnswerHash, followed by a hash of options.  It returns an AnswerHash
 2643 
 2644   $ans_hash = filter($ans_hash, %options);
 2645 
 2646 See the AnswerHash.pm file for a list of entries which can be expected to be found
 2647 in an AnswerHash, such as 'student_ans', 'score' and so forth.  Other entries
 2648 may be present for specialized answer evaluators.
 2649 
 2650 The hope is that a well designed set of filters can easily be combined to form
 2651 a new answer_evaluator and that this method will produce answer evaluators which are
 2652 are more robust than the method of copying existing answer evaluators and modifying them.
 2653 
 2654 Here is an outline of how a filter is constructed:
 2655 
 2656   sub filter{
 2657     my $rh_ans = shift;
 2658     my %options = @_;
 2659     assign_option_aliases(\%options,
 2660         'alias1'  => 'option5'
 2661         'alias2'  => 'option7'
 2662     );
 2663     set_default_options(\%options,
 2664         '_filter_name'  =>  'filter',
 2665         'option5'   =>  .0001,
 2666         'option7'   =>  'ascii',
 2667         'allow_unknown_options  =>  0,
 2668     }
 2669     .... body code of filter .......
 2670       if ($error) {
 2671         $rh_ans->throw_error("FILTER_ERROR", "Something went wrong");
 2672         # see AnswerHash.pm for details on using the throw_error method.
 2673 
 2674     $rh_ans;  #reference to an AnswerHash object is returned.
 2675   }
 2676 
 2677 =cut
 2678 
 2679 =head4 compare_numbers
 2680 
 2681 
 2682 =cut
 2683 
 2684 
 2685 sub compare_numbers {
 2686   my ($rh_ans, %options) = @_;
 2687   my ($inVal,$PG_eval_errors,$PG_full_error_report) = PG_answer_eval($rh_ans->{student_ans});
 2688   if ($PG_eval_errors) {
 2689     $rh_ans->throw_error('EVAL','There is a syntax error in your answer');
 2690     $rh_ans->{ans_message} = clean_up_error_msg($PG_eval_errors);
 2691     # return $rh_ans;
 2692   } else {
 2693     $rh_ans->{student_ans} = prfmt($inVal,$options{format});
 2694   }
 2695 
 2696   my $permitted_error;
 2697 
 2698   if ($rh_ans->{tolType} eq 'absolute') {
 2699     $permitted_error = $rh_ans->{tolerance};
 2700   }
 2701   elsif ( abs($rh_ans->{correct_ans}) <= $options{zeroLevel}) {
 2702       $permitted_error = $options{zeroLevelTol};  ## want $tol to be non zero
 2703   }
 2704   else {
 2705     $permitted_error = abs($rh_ans->{tolerance}*$rh_ans->{correct_ans});
 2706   }
 2707 
 2708   my $is_a_number = is_a_number($inVal);
 2709   $rh_ans->{score} = 1 if ( ($is_a_number) and
 2710       (abs( $inVal - $rh_ans->{correct_ans} ) <= $permitted_error) );
 2711   if (not $is_a_number) {
 2712     $rh_ans->{error_message} = "$rh_ans->{error_message}". 'Your answer does not evaluate to a number ';
 2713   }
 2714 
 2715   $rh_ans;
 2716 }
 2717 
 2718 =head4 std_num_filter
 2719 
 2720   std_num_filter($rh_ans, %options)
 2721   returns $rh_ans
 2722 
 2723 Replaces some constants using math_constants, then evaluates a perl expression.
 2724 
 2725 
 2726 =cut
 2727 
 2728 sub std_num_filter {
 2729   my $rh_ans = shift;
 2730   my %options = @_;
 2731   my $in = $rh_ans->input();
 2732   $in = math_constants($in);
 2733   $rh_ans->{type} = 'std_number';
 2734   my ($inVal,$PG_eval_errors,$PG_full_error_report);
 2735   if ($in =~ /\S/) {
 2736     ($inVal,$PG_eval_errors,$PG_full_error_report) = PG_answer_eval($in);
 2737   } else {
 2738     $PG_eval_errors = '';
 2739   }
 2740 
 2741   if ($PG_eval_errors) {        ##error message from eval or above
 2742     $rh_ans->{ans_message} = 'There is a syntax error in your answer';
 2743     $rh_ans->{student_ans} = clean_up_error_msg($PG_eval_errors);
 2744   } else {
 2745     $rh_ans->{student_ans} = $inVal;
 2746   }
 2747   $rh_ans;
 2748 }
 2749 
 2750 =head std_num_array_filter
 2751 
 2752   std_num_array_filter($rh_ans, %options)
 2753   returns $rh_ans
 2754 
 2755 Assumes the {student_ans} field is a numerical  array, and applies BOTH check_syntax and std_num_filter
 2756 to each element of the array.  Does it's best to generate sensible error messages for syntax errors.
 2757 A typical error message displayed in {studnet_ans} might be ( 56, error message, -4).
 2758 
 2759 =cut
 2760 
 2761 sub std_num_array_filter {
 2762   my $rh_ans= shift;
 2763   my %options = @_;
 2764   set_default_options(  \%options,
 2765         '_filter_name'  =>  'std_num_array_filter',
 2766     );
 2767   my @in = @{$rh_ans->{student_ans}};
 2768   my $temp_hash = new AnswerHash;
 2769   my @out=();
 2770   my $PGanswerMessage = '';
 2771   foreach my $item (@in)   {  # evaluate each number in the vector
 2772     $temp_hash->input($item);
 2773     $temp_hash = check_syntax($temp_hash);
 2774     if (defined($temp_hash->{error_flag}) and $temp_hash->{error_flag} eq 'SYNTAX') {
 2775       $PGanswerMessage .= $temp_hash->{ans_message};
 2776       $temp_hash->{ans_message} = undef;
 2777     } else {
 2778       #continue processing
 2779       $temp_hash = std_num_filter($temp_hash);
 2780       if (defined($temp_hash->{ans_message}) and $temp_hash->{ans_message} ) {
 2781         $PGanswerMessage .= $temp_hash->{ans_message};
 2782         $temp_hash->{ans_message} = undef;
 2783       }
 2784     }
 2785     push(@out, $temp_hash->input());
 2786 
 2787   }
 2788   if ($PGanswerMessage) {
 2789     $rh_ans->input( "( " . join(", ", @out ) . " )" );
 2790         $rh_ans->throw_error('SYNTAX', 'There is a syntax error in your answer.');
 2791   } else {
 2792     $rh_ans->input( [@out] );
 2793   }
 2794   $rh_ans;
 2795 }
 2796 
 2797 =head4 function_from_string2
 2798 
 2799 
 2800 
 2801 =cut
 2802 
 2803 sub function_from_string2 {
 2804     my $rh_ans = shift;
 2805     my %options = @_;
 2806   my $eqn = $rh_ans->{student_ans};
 2807   assign_option_aliases(\%options,
 2808         'vars'      => 'ra_vars',
 2809         'var'           => 'ra_vars',
 2810   );
 2811   set_default_options(  \%options,
 2812               'store_in'    =>      'rf_student_ans',
 2813           'ra_vars'   =>  [qw( x y )],
 2814           'debug'     =>  0,
 2815           '_filter_name'  =>  'function_from_string2',
 2816     );
 2817     $rh_ans->{_filter_name} = $options{_filter_name};
 2818     my @VARS = @{ $options{ 'ra_vars'}};
 2819     #warn "VARS = ", join("<>", @VARS) if defined($options{debug}) and $options{debug} ==1;
 2820     my $originalEqn = $eqn;
 2821     $eqn = &math_constants($eqn);
 2822     for( my $i = 0; $i < @VARS; $i++ ) {
 2823         #  This next line is a hack required for 5.6.0 -- it doesn't appear to be needed in 5.6.1
 2824         my ($temp,$er1,$er2) = PG_restricted_eval('"'. $VARS[$i] . '"');
 2825   #$eqn =~ s/\b$VARS[$i]\b/\$VARS[$i]/g;
 2826         $eqn  =~ s/\b$temp\b/\$VARS[$i]/g;
 2827 
 2828   }
 2829   #warn "equation evaluated = $eqn",$rh_ans->pretty_print(), "<br>\noptions<br>\n",
 2830   #     pretty_print(\%options)
 2831   #     if defined($options{debug}) and $options{debug} ==1;
 2832     my ($function_sub,$PG_eval_errors, $PG_full_errors) = PG_answer_eval( q!
 2833       sub {
 2834         my @VARS = @_;
 2835         my $input_str = '';
 2836         for( my $i=0; $i<@VARS; $i++ ) {
 2837           $input_str .= "\$VARS[$i] = $VARS[$i]; ";
 2838         }
 2839         my $PGanswerMessage;
 2840         $input_str .= '! . $eqn . q!';  # need the single quotes to keep the contents of $eqn from being
 2841                                         # evaluated when it is assigned to $input_str;
 2842         my ($out, $PG_eval_errors, $PG_full_errors) = PG_answer_eval($input_str); #Finally evaluated
 2843 
 2844         if ( defined($PG_eval_errors) and $PG_eval_errors =~ /\S/ ) {
 2845             $PGanswerMessage  = clean_up_error_msg($PG_eval_errors);
 2846 # This message seemed too verbose, but it does give extra information, we'll see if it is needed.
 2847 #                    "<br> There was an error in evaluating your function <br>
 2848 #           !. $originalEqn . q! <br>
 2849 #           at ( " . join(', ', @VARS) . " ) <br>
 2850 #            $PG_eval_errors
 2851 #           ";   # this message appears in the answer section which is not process by Latex2HTML so it must
 2852 #                # be in HTML.  That is why $BR is NOT used.
 2853 
 2854       }
 2855       (wantarray) ? ($out, $PGanswerMessage): $out;   # PGanswerMessage may be undefined.
 2856       };
 2857   !);
 2858 
 2859   if (defined($PG_eval_errors) and $PG_eval_errors =~/\S/ ) {
 2860         $PG_eval_errors = clean_up_error_msg($PG_eval_errors);
 2861 
 2862     my $PGanswerMessage = "There was an error in converting the expression
 2863       $main::BR $originalEqn $main::BR into a function.
 2864       $main::BR $PG_eval_errors.";
 2865     $rh_ans->{rf_student_ans} = $function_sub;
 2866     $rh_ans->{ans_message} = $PGanswerMessage;
 2867     $rh_ans->{error_message} = $PGanswerMessage;
 2868     $rh_ans->{error_flag} = 1;
 2869      # we couldn't compile the equation, we'll return an error message.
 2870   } else {
 2871 #     if (defined($options{store_in} )) {
 2872 #       $rh_ans ->{$options{store_in}} = $function_sub;
 2873 #     } else {
 2874 #         $rh_ans->{rf_student_ans} = $function_sub;
 2875 #       }
 2876       $rh_ans ->{$options{store_in}} = $function_sub;
 2877   }
 2878 
 2879     $rh_ans;
 2880 }
 2881 
 2882 =head4 is_zero_array
 2883 
 2884 
 2885 =cut
 2886 
 2887 
 2888 sub is_zero_array {
 2889     my $rh_ans = shift;
 2890     my %options = @_;
 2891     set_default_options(  \%options,
 2892         '_filter_name'  =>  'is_zero_array',
 2893         'tolerance' =>  0.000001,
 2894     );
 2895     my $array = $rh_ans -> {ra_differences};
 2896   my $num = @$array;
 2897   my $i;
 2898   my $max = 0; my $mm;
 2899   for ($i=0; $i< $num; $i++) {
 2900     $mm = $array->[$i] ;
 2901     if  (not is_a_number($mm) ) {
 2902       $max = $mm;  # break out if one of the elements is not a number
 2903       last;
 2904     }
 2905     $max = abs($mm) if abs($mm) > $max;
 2906   }
 2907   if (not is_a_number($max)) {
 2908     $rh_ans->{score} = 0;
 2909       my $error = "WeBWorK was unable evaluate your function. Please check that your
 2910                 expression doesn't take roots of negative numbers, or divide by zero.";
 2911     $rh_ans->throw_error('EVAL',$error);
 2912   } else {
 2913       $rh_ans->{score} = ($max < $options{tolerance} ) ? 1: 0;       # 1 if the array is close to 0;
 2914   }
 2915   $rh_ans;
 2916 }
 2917 
 2918 =head4 best_approx_parameters
 2919 
 2920   best_approx_parameters($rh_ans,%options);   #requires the following fields in $rh_ans
 2921                         {rf_student_ans}      # reference to the test answer
 2922                         {rf_correct_ans}      # reference to the comparison answer
 2923                         {evaluation_points},  # an array of row vectors indicating the points
 2924                                       # to evaluate when comparing the functions
 2925 
 2926                          %options       # debug => 1   gives more error answers
 2927                                     # param_vars => ['']  additional parameters used to adapt to function
 2928                          )
 2929 
 2930 
 2931 The parameters for the comparison function which best approximates the test_function are stored
 2932 in the field {ra_parameters}.
 2933 
 2934 
 2935 The last $dim_of_parms_space variables are assumed to be parameters, and it is also
 2936 assumed that the function \&comparison_fun
 2937 depends linearly on these variables.  This function finds the  values for these parameters which minimizes the
 2938 Euclidean distance (L2 distance) between the test function and the comparison function and the test points specified
 2939 by the array reference  \@rows_of_test_points.  This is assumed to be an array of arrays, with the inner arrays
 2940 determining a test point.
 2941 
 2942 The comparison function should have $dim_of_params_space more input variables than the test function.
 2943 
 2944 
 2945 
 2946 
 2947 
 2948 =cut
 2949 
 2950 # Used internally:
 2951 #
 2952 #   &$determine_param_coeff( $rf_comparison_function # a reference to the correct answer function
 2953 #                    $ra_variables                   # an array of the active input variables to the functions
 2954 #                    $dim_of_params_space            # indicates the number of parameters upon which the
 2955 #                                                    # the comparison function depends linearly.  These are assumed to
 2956 #                                                    # be the last group of inputs to the comparison function.
 2957 #
 2958 #                    %options                        # $options{debug} gives more error messages
 2959 #
 2960 #                                                    # A typical function might look like
 2961 #                                                    # f(x,y,z,a,b) = x^2+a*cos(xz) + b*sin(x) with a parameter
 2962 #                                                    # space of dimension 2 and a variable space of dimension 3.
 2963 #                   )
 2964 #         # returns a list of coefficients
 2965 
 2966 sub best_approx_parameters {
 2967     my $rh_ans = shift;
 2968     my %options = @_;
 2969     set_default_options(\%options,
 2970         '_filter_name'      =>  'best_approx_paramters',
 2971         'allow_unknown_options' =>  1,
 2972     );
 2973     my $errors = undef;
 2974     # This subroutine for the determining the coefficents of the parameters at a given point
 2975     # is pretty specialized, so it is included here as a sub-subroutine.
 2976     my $determine_param_coeffs  = sub {
 2977     my ($rf_fun, $ra_variables, $dim_of_params_space, %options) =@_;
 2978     my @zero_params=();
 2979     for(my $i=1;$i<=$dim_of_params_space;$i++){push(@zero_params,0); }
 2980     my @vars = @$ra_variables;
 2981     my @coeff = ();
 2982     my @inputs = (@vars,@zero_params);
 2983     my ($f0, $f1, $err);
 2984     ($f0, $err) = &{$rf_fun}(@inputs);
 2985     if (defined($err) ) {
 2986       $errors .= "$err ";
 2987     } else {
 2988       for (my $i=@vars;$i<@inputs;$i++) {
 2989         $inputs[$i]=1;  # set one parameter to 1;
 2990         my($f1,$err) = &$rf_fun(@inputs);
 2991         if (defined($err) ) {
 2992           $errors .= " $err ";
 2993         } else {
 2994           push(@coeff, $f1-$f0);
 2995         }
 2996         $inputs[$i]=0;  # set it back
 2997       }
 2998     }
 2999     (\@coeff, $errors);
 3000   };
 3001     my $rf_fun = $rh_ans->{rf_student_ans};
 3002     my $rf_correct_fun = $rh_ans->{rf_correct_ans};
 3003     my $ra_vars_matrix = $rh_ans->{evaluation_points};
 3004     my $dim_of_param_space = @{$options{param_vars}};
 3005     # Short cut.  Bail if there are no param_vars
 3006     unless ($dim_of_param_space >0) {
 3007     $rh_ans ->{ra_parameters} = [];
 3008     return $rh_ans;
 3009     }
 3010     # inputs are row arrays in this case.
 3011     my @zero_params=();
 3012 
 3013     for(my $i=1;$i<=$dim_of_param_space;$i++){push(@zero_params,0); }
 3014     my @rows_of_vars = @$ra_vars_matrix;
 3015     warn "input rows ", pretty_print(\@rows_of_vars) if defined($options{debug}) and $options{debug};
 3016     my $rows = @rows_of_vars;
 3017     my $matrix =new Matrix($rows,$dim_of_param_space);
 3018     my $rhs_vec = new Matrix($rows, 1);
 3019     my $row_num = 1;
 3020     my ($ra_coeff,$val2, $val1, $err1,$err2,@inputs,@vars);
 3021     my $number_of_data_points = $dim_of_param_space +2;
 3022     while (@rows_of_vars and $row_num <= $number_of_data_points) {
 3023      # get one set of data points from the test function;
 3024       @vars = @{ shift(@rows_of_vars) };
 3025       ($val2, $err1) = &{$rf_fun}(@vars);
 3026       $errors .= " $err1 "  if defined($err1);
 3027       @inputs = (@vars,@zero_params);
 3028       ($val1, $err2) = &{$rf_correct_fun}(@inputs);
 3029       $errors .= " $err2 " if defined($err2);
 3030 
 3031       unless (defined($err1) or defined($err2) ) {
 3032           $rhs_vec->assign($row_num,1, $val2-$val1 );
 3033 
 3034     # warn "rhs data  val1=$val1, val2=$val2, val2 - val1 = ", $val2 - $val1 if $options{debug};
 3035     # warn "vars ", join(" | ", @vars) if $options{debug};
 3036 
 3037       ($ra_coeff, $err1) = &{$determine_param_coeffs}($rf_correct_fun,\@vars,$dim_of_param_space,%options);
 3038       if (defined($err1) ) {
 3039         $errors .= " $err1 ";
 3040       } else {
 3041         my @coeff = @$ra_coeff;
 3042         my $col_num=1;
 3043           while(@coeff) {
 3044             $matrix->assign($row_num,$col_num, shift(@coeff) );
 3045             $col_num++;
 3046           }
 3047         }
 3048       }
 3049       $row_num++;
 3050       last if $errors;  # break if there are any errors.
 3051                       # This cuts down on the size of error messages.
 3052                       # However it impossible to check for equivalence at 95% of points
 3053             # which might be useful for functions that are not defined at some points.
 3054   }
 3055     warn "<br> best_approx_parameters: matrix1 <br>  ", " $matrix " if $options{debug};
 3056     warn "<br> best_approx_parameters: vector <br>  ", " $rhs_vec " if $options{debug};
 3057 
 3058    # we have   Matrix * parameter = data_vec + perpendicular vector
 3059    # where the matrix has column vectors defining the span of the parameter space
 3060    # multiply both sides by Matrix_transpose and solve for the parameters
 3061    # This is exactly what the method proj_coeff method does.
 3062    my @array;
 3063    if (defined($errors) ) {
 3064     @array = ();   #     new Matrix($dim_of_param_space,1);
 3065    } else {
 3066     @array = $matrix->proj_coeff($rhs_vec)->list();
 3067    }
 3068   # check size (hack)
 3069   my $max = 0;
 3070   foreach my $val (@array ) {
 3071     $max = abs($val) if  $max < abs($val);
 3072     if (not is_a_number($val) ) {
 3073       $max = "NaN: $val";
 3074       last;
 3075     }
 3076   }
 3077   if ($max =~/NaN/) {
 3078     $errors .= "WeBWorK was unable evaluate your function. Please check that your
 3079                 expression doesn't take roots of negative numbers, or divide by zero.";
 3080   } elsif ($max > $options{maxConstantOfIntegration} ) {
 3081     $errors .= "At least one of the adapting parameters
 3082              (perhaps the constant of integration) is too large: $max,
 3083              ( the maximum allowed is $options{maxConstantOfIntegration} )";
 3084   }
 3085 
 3086     $rh_ans->{ra_parameters} = \@array;
 3087     $rh_ans->throw_error('EVAL', $errors) if defined($errors);
 3088     $rh_ans;
 3089 }
 3090 
 3091 =head4 calculate_difference_vector
 3092 
 3093   calculate_difference_vector( $ans_hash, %options);
 3094 
 3095                 {rf_student_ans},     # a reference to the test function
 3096                                {rf_correct_ans},      # a reference to the correct answer function
 3097                                {evaluation_points},   # an array of row vectors indicating the points
 3098                                           # to evaluate when comparing the functions
 3099                                {ra_parameters}        # these are the (optional) additional inputs to
 3100                                                       # the comparison function which adapt it properly
 3101                                                       # to the problem at hand.
 3102 
 3103                                %options               # mode => 'rel'  specifies that each element in the
 3104                                                       # difference matrix is divided by the correct answer.
 3105                                                       # unless the correct answer is nearly 0.
 3106                               )
 3107 
 3108 =cut
 3109 
 3110 sub calculate_difference_vector {
 3111   my $rh_ans = shift;
 3112   my %options = @_;
 3113   # initialize
 3114   my $rf_fun = $rh_ans -> {rf_student_ans};
 3115   my $rf_correct_fun = $rh_ans -> {rf_correct_ans};
 3116   my $ra_parameters = $rh_ans ->{ra_parameters};
 3117   my @evaluation_points = @{$rh_ans->{evaluation_points} };
 3118   my @parameters = ();
 3119   @parameters = @$ra_parameters if defined($ra_parameters) and ref($ra_parameters) eq 'ARRAY';
 3120   my $errors = undef;
 3121   my @zero_params=();
 3122   for(my $i=1;$i<=@{$ra_parameters};$i++){push(@zero_params,0); }
 3123   my @differences = ();
 3124   my @student_values;
 3125   my @adjusted_student_values;
 3126   my @instructorVals;
 3127   my ($diff,$instructorVal);
 3128   # calculate the vector of differences between the test function and the comparison function.
 3129   while (@evaluation_points) {
 3130     my ($err1, $err2,$err3);
 3131     my @vars = @{ shift(@evaluation_points) };
 3132     my @inputs = (@vars, @parameters);
 3133     my ($inVal,  $correctVal);
 3134     ($inVal, $err1) = &{$rf_fun}(@vars);
 3135     $errors .= " $err1 "  if defined($err1);
 3136     $errors .= " Error detected evaluating student input at (".join(' , ',@vars) ." ) " if  defined($options{debug}) and $options{debug}=1 and defined($err1);
 3137     ($correctVal, $err2) =&{$rf_correct_fun}(@inputs);
 3138     $errors .= " There is an error in WeBWorK's answer to this problem, please alert your instructor.<br> $err2 " if defined($err2);
 3139     $errors .= " Error detected evaluating correct adapted answer  at (".join(' , ',@inputs) ." ) " if defined($options{debug}) and $options{debug}=1 and defined($err2);
 3140     ($instructorVal,$err3)= &$rf_correct_fun(@vars, @zero_params);
 3141     $errors .= " There is an error in WeBWorK's answer to this problem, please alert your instructor.<br> $err3 " if defined($err3);
 3142     $errors .= " Error detected evaluating instructor answer  at (".join(' , ',@vars, @zero_params) ." ) " if defined($options{debug}) and $options{debug}=1 and defined($err3);
 3143     unless (defined($err1) or defined($err2) or defined($err3) ) {
 3144       $diff = ( $inVal - ($correctVal -$instructorVal ) ) - $instructorVal;  #prevents entering too high a number?
 3145       #warn "taking the difference of ", $inVal, " and ", $correctVal, " is ", $diff;
 3146       if (defined($options{tolType}) and $options{tolType} eq 'relative' ) {  #relative tolerance
 3147         #warn "diff = $diff";
 3148         #$diff = ( $inVal - ($correctVal-$instructorVal ) )/abs($instructorVal) -1    if abs($instructorVal) > $options{zeroLevel};
 3149         $diff = ( $inVal - ($correctVal-$instructorVal ) )/$instructorVal -1    if abs($instructorVal) > $options{zeroLevel};
 3150         #$diff = ( $inVal - ($correctVal-$instructorVal- $instructorVal ) )/abs($instructorVal)    if abs($instructorVal) > $options{zeroLevel};
 3151         #warn "diff = $diff,   ", abs( &$rf_correct_fun(@inputs) ) , "-- $correctVal";
 3152       }
 3153     }
 3154     last if $errors;  # break if there are any errors.
 3155                   # This cuts down on the size of error messages.
 3156                   # However it impossible to check for equivalence at 95% of points
 3157                   # which might be useful for functions that are not defined at some points.
 3158         push(@student_values,$inVal);
 3159         push(@adjusted_student_values,(  $inVal - ($correctVal -$instructorVal) ) );
 3160     push(@differences, $diff);
 3161     push(@instructorVals,$instructorVal);
 3162   }
 3163   $rh_ans ->{ra_differences} = \@differences;
 3164   $rh_ans ->{ra_student_values} = \@student_values;
 3165   $rh_ans ->{ra_adjusted_student_values} = \@adjusted_student_values;
 3166   $rh_ans->{ra_instructor_values}=\@instructorVals;
 3167   $rh_ans->throw_error('EVAL', $errors) if defined($errors);
 3168   $rh_ans;
 3169 }
 3170 
 3171 =head4 fix_answer_for_display
 3172 
 3173 =cut
 3174 
 3175 sub fix_answers_for_display {
 3176   my ($rh_ans, %options) = @_;
 3177   if ( $rh_ans->{answerIsString} ==1) {
 3178     $rh_ans = evaluatesToNumber ($rh_ans, %options);
 3179   }
 3180   if (defined ($rh_ans->{student_units})) {
 3181     $rh_ans->{student_ans} = $rh_ans->{student_ans}. ' '. $rh_ans->{student_units};
 3182   }
 3183   $rh_ans->{correct_ans} = $rh_ans->{original_correct_ans};
 3184 
 3185   $rh_ans;
 3186 }
 3187 
 3188 =head4 evaluatesToNumber
 3189 
 3190 =cut
 3191 
 3192 sub evaluatesToNumber {
 3193   my ($rh_ans, %options) = @_;
 3194   if (is_a_numeric_expression($rh_ans->{student_ans})) {
 3195     my ($inVal,$PG_eval_errors,$PG_full_error_report) = PG_answer_eval($rh_ans->{student_ans});
 3196     if ($PG_eval_errors) { # this if statement should never be run
 3197       # change nothing
 3198     } else {
 3199       # change this
 3200       $rh_ans->{student_ans} = prfmt($inVal,$options{format});
 3201     }
 3202   }
 3203   $rh_ans;
 3204 }
 3205 
 3206 =head4 is_numeric_expression
 3207 
 3208 =cut
 3209 
 3210 sub is_a_numeric_expression {
 3211   my $testString = shift;
 3212   my $is_a_numeric_expression = 0;
 3213   my ($inVal,$PG_eval_errors,$PG_full_error_report) = PG_answer_eval($testString);
 3214   if ($PG_eval_errors) {
 3215     $is_a_numeric_expression = 0;
 3216   } else {
 3217     $is_a_numeric_expression = 1;
 3218   }
 3219   $is_a_numeric_expression;
 3220 }
 3221 
 3222 =head4 is_a_number
 3223 
 3224 =cut
 3225 
 3226 sub is_a_number {
 3227   my ($num,%options) =  @_;
 3228   my $process_ans_hash = ( ref( $num ) eq 'AnswerHash' ) ? 1 : 0 ;
 3229   my ($rh_ans);
 3230   if ($process_ans_hash) {
 3231     $rh_ans = $num;
 3232     $num = $rh_ans->{student_ans};
 3233   }
 3234 
 3235   my $is_a_number = 0;
 3236   return $is_a_number unless defined($num);
 3237   $num =~ s/^\s*//; ## remove initial spaces
 3238   $num =~ s/\s*$//; ## remove trailing spaces
 3239 
 3240   ## the following is copied from the online perl manual
 3241   if ($num =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/){
 3242     $is_a_number = 1;
 3243   }
 3244 
 3245   if ($process_ans_hash)   {
 3246         if ($is_a_number == 1 ) {
 3247           $rh_ans->{student_ans}=$num;
 3248           return $rh_ans;
 3249         } else {
 3250           $rh_ans->{student_ans} = "Incorrect number format:  You must enter a number, e.g. -6, 5.3, or 6.12E-3";
 3251           $rh_ans->throw_error('NUMBER', 'You must enter a number, e.g. -6, 5.3, or 6.12E-3');
 3252           return $rh_ans;
 3253         }
 3254   } else {
 3255     return $is_a_number;
 3256   }
 3257 }
 3258 
 3259 =head4 is_a_fraction
 3260 
 3261 =cut
 3262 
 3263 sub is_a_fraction {
 3264   my ($num,%options) =  @_;
 3265   my $process_ans_hash = ( ref( $num ) eq 'AnswerHash' ) ? 1 : 0 ;
 3266   my ($rh_ans);
 3267   if ($process_ans_hash) {
 3268     $rh_ans = $num;
 3269     $num = $rh_ans->{student_ans};
 3270   }
 3271 
 3272   my $is_a_fraction = 0;
 3273   return $is_a_fraction unless defined($num);
 3274   $num =~ s/^\s*//; ## remove initial spaces
 3275   $num =~ s/\s*$//; ## remove trailing spaces
 3276 
 3277   if ($num =~ /^\s*\-?\s*[\/\d\.Ee\s]*$/) {
 3278     $is_a_fraction = 1;
 3279   }
 3280 
 3281     if ($process_ans_hash)   {
 3282       if ($is_a_fraction == 1 ) {
 3283         $rh_ans->{student_ans}=$num;
 3284         return $rh_ans;
 3285       } else {
 3286         $rh_ans->{student_ans} = "Not a number of fraction: You must enter a number or fraction, e.g. -6 or 7/13";
 3287         $rh_ans->throw_error('NUMBER', 'You must enter a number, e.g. -6, 5.3, or 6.12E-3');
 3288         return $rh_ans;
 3289       }
 3290 
 3291       } else {
 3292     return $is_a_fraction;
 3293   }
 3294 }
 3295 
 3296 =head4 is_an_arithemetic_expression
 3297 
 3298 =cut
 3299 
 3300 sub is_an_arithmetic_expression {
 3301   my ($num,%options) =  @_;
 3302   my $process_ans_hash = ( ref( $num ) eq 'AnswerHash' ) ? 1 : 0 ;
 3303   my ($rh_ans);
 3304   if ($process_ans_hash) {
 3305     $rh_ans = $num;
 3306     $num = $rh_ans->{student_ans};
 3307   }
 3308 
 3309   my $is_an_arithmetic_expression = 0;
 3310   return $is_an_arithmetic_expression unless defined($num);
 3311   $num =~ s/^\s*//; ## remove initial spaces
 3312   $num =~ s/\s*$//; ## remove trailing spaces
 3313 
 3314   if ($num =~ /^[+\-*\/\^\(\)\[\]\{\}\s\d\.Ee]*$/) {
 3315     $is_an_arithmetic_expression =  1;
 3316   }
 3317 
 3318     if ($process_ans_hash)   {
 3319       if ($is_an_arithmetic_expression == 1 ) {
 3320         $rh_ans->{student_ans}=$num;
 3321         return $rh_ans;
 3322       } else {
 3323 
 3324     $rh_ans->{student_ans} = "Not an arithmetic expression: You must enter an arithmetic expression, e.g. -6 or (2.3*4+5/3)^2";
 3325         $rh_ans->throw_error('NUMBER', 'You must enter an arithmetic expression, e.g. -6 or (2.3*4+5/3)^2');
 3326         return $rh_ans;
 3327       }
 3328 
 3329       } else {
 3330     return $is_an_arithmetic_expression;
 3331   }
 3332 }
 3333 
 3334 #
 3335 
 3336 =head4 math_constants
 3337 
 3338 replaces pi, e, and ^ with their Perl equivalents
 3339 
 3340 =cut
 3341 
 3342 sub math_constants {
 3343   my($in,%options) = @_;
 3344   my $rh_ans;
 3345   my $process_ans_hash = ( ref( $in ) eq 'AnswerHash' ) ? 1 : 0 ;
 3346   if ($process_ans_hash) {
 3347     $rh_ans = $in;
 3348     $in = $rh_ans->{student_ans};
 3349   }
 3350   # The code fragment above allows this filter to be used when the input is simply a string
 3351   # as well as when the input is an AnswerHash, and options.
 3352   $in =~s/\bpi\b/(4*atan2(1,1))/ge;
 3353   $in =~s/\be\b/(exp(1))/ge;
 3354   $in =~s/\^/**/g;
 3355 
 3356   if ($process_ans_hash)   {
 3357       $rh_ans->{student_ans}=$in;
 3358       return $rh_ans;
 3359     } else {
 3360     return $in;
 3361   }
 3362 }
 3363 
 3364 
 3365 
 3366 =head4 is_array
 3367 
 3368   is_array($rh_ans)
 3369     returns: $rh_ans.   Throws error "NOTARRAY" if this is not an array
 3370 
 3371 =cut
 3372 
 3373 sub is_array  {
 3374   my $rh_ans = shift;
 3375     # return if the result is an array
 3376   return($rh_ans) if  ref($rh_ans->{student_ans}) eq 'ARRAY' ;
 3377   $rh_ans->throw_error("NOTARRAY","The answer is not an array");
 3378   $rh_ans;
 3379 }
 3380 
 3381 =head4 check_syntax
 3382 
 3383   check_syntax( $rh_ans, %options)
 3384     returns an answer hash.
 3385 
 3386 latex2html preview code are installed in the answer hash.
 3387 The input has been transformed, changing 7pi to 7*pi  or 7x to 7*x.
 3388 Syntax error messages may be generated and stored in student_ans
 3389 Additional syntax error messages are stored in {ans_message} and duplicated in {error_message}
 3390 
 3391 
 3392 =cut
 3393 
 3394 sub check_syntax {
 3395         my $rh_ans = shift;
 3396         my %options = @_;
 3397         unless ( defined( $rh_ans->{student_ans} ) ) {
 3398           warn "Check_syntax requires an equation in the field {student_ans} or input";
 3399           $rh_ans->throw_error("1","{student_ans} field not defined");
 3400           return $rh_ans;
 3401         }
 3402         my $in = $rh_ans->{student_ans};
 3403     my $parser = new AlgParserWithImplicitExpand;
 3404     my $ret = $parser -> parse($in);      #for use with loops
 3405 
 3406     if ( ref($ret) )  {   ## parsed successfully
 3407       $parser -> tostring();
 3408       $parser -> normalize();
 3409       $rh_ans->input( $parser -> tostring() );
 3410       $rh_ans->{preview_text_string} = $in;
 3411       $rh_ans->{preview_latex_string} = $parser -> tolatex();
 3412 
 3413     } else {          ## error in parsing
 3414 
 3415       $rh_ans->{'student_ans'}      = 'syntax error:'. $parser->{htmlerror},
 3416       $rh_ans->{'ans_message'}      = $parser -> {error_msg},
 3417       $rh_ans->{'preview_text_string'}  = '',
 3418       $rh_ans->{'preview_latex_string'} = '',
 3419       $rh_ans->throw_error('SYNTAX',  'syntax error in answer:'. $parser->{htmlerror} . "$BR" .$parser -> {error_msg});
 3420     }
 3421 $rh_ans;
 3422 
 3423 }
 3424 
 3425 =head4 check_strings
 3426 
 3427   check_strings ($rh_ans, %options)
 3428     returns $rh_ans
 3429 
 3430 =cut
 3431 
 3432 sub check_strings {
 3433   my ($rh_ans, %options) = @_;
 3434 
 3435   # if the student's answer is a number, simply return the answer hash (unchanged).
 3436 
 3437   if  ( $rh_ans->{student_ans} =~ m/[\d+\-*\/^(){}\[\]]|^\s*e\s*$|^\s*pi\s*$/)   {
 3438     if ( $rh_ans->{answerIsString} == 1) {
 3439         #$rh_ans->throw_error('STRING','Incorrect Answer'); # student's answer is a number
 3440     }
 3441     return $rh_ans;
 3442   }
 3443   # the student's answer is recognized as a string
 3444   my $ans = $rh_ans->{student_ans};
 3445 
 3446 # OVERVIEW of reminder of function:
 3447 # if answer is correct, return correct.  (adjust score to 1)
 3448 # if answer is incorect:
 3449 # 1) determine if the answer is sensible.  if it is, return incorrect.
 3450 # 2) if the answer is not sensible (and incorrect), then return an error message indicating so.
 3451 # no matter what:  throw a 'STRING' error to skip numerical evaluations.  (error flag skips remainder of pre_filters and evaluators)
 3452 # last: 'STRING' post_filter will clear the error (avoiding pink screen.)
 3453 
 3454   my $sensibleAnswer = 0;
 3455   $ans = str_filters( $ans, 'compress_whitespace' );  # remove trailing, leading, and double spaces.
 3456   my ($ans_eval) = str_cmp($rh_ans->{correct_ans});
 3457   my $temp_ans_hash = &$ans_eval($ans);
 3458   $rh_ans->{test} = $temp_ans_hash;
 3459   if ($temp_ans_hash->{score} ==1 ) {     # students answer matches the correct answer.
 3460     $rh_ans->{score} = 1;
 3461     $sensibleAnswer = 1;
 3462   } else {            # students answer does not match the correct answer.
 3463     my $legalString = '';       # find out if string makes sense
 3464     my @legalStrings = @{$options{strings}};
 3465     foreach $legalString (@legalStrings) {
 3466       if ( uc($ans) eq uc($legalString) ) {
 3467         $sensibleAnswer = 1;
 3468         last;
 3469         }
 3470       }
 3471     $sensibleAnswer = 1 unless $ans =~ /\S/;  ## empty answers are sensible
 3472     $rh_ans->throw_error('EVAL', "Your answer is not a recognized answer") unless ($sensibleAnswer);
 3473     # $temp_ans_hash -> setKeys( 'ans_message' => 'Your answer is not a recognized answer' ) unless ($sensibleAnswer);
 3474     # $temp_ans_hash -> setKeys( 'student_ans' => uc($ans) );
 3475   }
 3476   $rh_ans->{student_ans} = $ans;
 3477   if ($sensibleAnswer) {
 3478     $rh_ans->throw_error('STRING', "The student's answer $rh_ans->{student_ans} is interpreted as a string.");
 3479   }
 3480   # warn ("\$rh_ans->{answerIsString} = $rh_ans->{answerIsString}");
 3481   $rh_ans;
 3482 }
 3483 
 3484 =head4 check_units
 3485 
 3486   check_strings ($rh_ans, %options)
 3487     returns $rh_ans
 3488 
 3489 
 3490 =cut
 3491 
 3492 sub check_units {
 3493   my ($rh_ans, %options) = @_;
 3494   my %correct_units = %{$rh_ans-> {rh_correct_units}};
 3495   my $ans = $rh_ans->{student_ans};
 3496   # $ans = '' unless defined ($ans);
 3497   $ans = str_filters ($ans, 'trim_whitespace');
 3498   my $original_student_ans = $ans;
 3499   $rh_ans->{original_student_ans} = $original_student_ans;
 3500 
 3501   # it surprises me that the match below works since the first .* is greedy.
 3502   my ($num_answer, $units) = $ans =~ /^(.*)\s+([^\s]*)$/;
 3503 
 3504   unless ( defined($num_answer) && $units ) {
 3505     # there is an error reading the input
 3506     if ( $ans =~ /\S/ )  {  # the answer is not blank
 3507       $rh_ans -> setKeys( 'ans_message' => "The answer \"$ans\" could not be interpreted " .
 3508         "as a number or an arithmetic expression followed by a unit specification. " .
 3509         "Your answer must contain units." );
 3510       $rh_ans->throw_error('UNITS', "The answer \"$ans\" could not be interpreted " .
 3511         "as a number or an arithmetic expression followed by a unit specification. " .
 3512         "Your answer must contain units." );
 3513     }
 3514     return $rh_ans;
 3515   }
 3516 
 3517   # we have been able to parse the answer into a numerical part and a unit part
 3518 
 3519   # $num_answer = $1;   #$1 and $2 from the regular expression above
 3520   # $units    = $2;
 3521 
 3522   my %units = Units::evaluate_units($units);
 3523   if ( defined( $units{'ERROR'} ) ) {
 3524      # handle error condition
 3525           $units{'ERROR'} = clean_up_error_msg($units{'ERROR'});
 3526     $rh_ans -> setKeys( 'ans_message' => "$units{'ERROR'}" );
 3527     $rh_ans -> throw_error('UNITS', "$units{'ERROR'}");
 3528     return $rh_ans;
 3529   }
 3530 
 3531   my $units_match = 1;
 3532   my $fund_unit;
 3533   foreach $fund_unit (keys %correct_units) {
 3534     next if $fund_unit eq 'factor';
 3535     $units_match = 0 unless $correct_units{$fund_unit} == $units{$fund_unit};
 3536   }
 3537 
 3538   if ( $units_match ) {
 3539         # units are ok.  Evaluate the numerical part of the answer
 3540     $rh_ans->{'tolerance'} = $rh_ans->{'tolerance'}* $correct_units{'factor'}/$units{'factor'}  if
 3541           $rh_ans->{'tolType'} eq 'absolute'; # the tolerance is in the units specified by the instructor.
 3542     $rh_ans->{correct_ans} =  prfmt($rh_ans->{correct_ans}*$correct_units{'factor'}/$units{'factor'});
 3543     $rh_ans->{student_units} = $units;
 3544     $rh_ans->{student_ans} = $num_answer;
 3545 
 3546   } else {
 3547         $rh_ans -> setKeys( ans_message => 'There is an error in the units for this answer.' );
 3548         $rh_ans -> throw_error ( 'UNITS', 'There is an error in the units for this answer.' );
 3549   }
 3550 
 3551   return $rh_ans;
 3552 }
 3553 
 3554 
 3555 
 3556 =head2 Filter utilities
 3557 
 3558 These two subroutines can be used in filters to set default options.  They
 3559 help make filters perform in uniform, predictable ways, and also make it
 3560 easy to recognize from the code which options a given filter expects.
 3561 
 3562 
 3563 =head4 assign_option_aliases
 3564 
 3565 Use this to assign aliases for the standard options.  It must come before set_default_options
 3566 within the subroutine.
 3567 
 3568     assign_option_aliases(\%options,
 3569         'alias1'  => 'option5'
 3570         'alias2'  => 'option7'
 3571     );
 3572 
 3573 
 3574 If the subroutine is called with an option  " alias1 => 23 " it will behave as if it had been
 3575 called with the option " option5 => 23 "
 3576 
 3577 =cut
 3578 
 3579 
 3580 
 3581 sub assign_option_aliases {
 3582   my $rh_options = shift;
 3583   warn "The first entry to set_default_options must be a reference to the option hash" unless ref($rh_options) eq 'HASH';
 3584   my @option_aliases = @_;
 3585   while (@option_aliases) {
 3586     my $alias = shift @option_aliases;
 3587     my $option_key = shift @option_aliases;
 3588 
 3589     if (defined($rh_options->{$alias} )) {                       # if the alias appears in the option list
 3590       if (not defined($rh_options->{$option_key}) ) {          # and the option itself is not defined,
 3591         $rh_options->{$option_key} = $rh_options->{$alias};  # insert the value defined by the alias into the option value
 3592                                                              # the FIRST alias for a given option takes precedence
 3593                                                              # (after the option itself)
 3594       } else {
 3595         warn "option $option_key is already defined as", $rh_options->{$option_key}, "<br>\n",
 3596              "The attempt to override this option with the alias $alias with value ", $rh_options->{$alias},
 3597              " was ignored.";
 3598       }
 3599     }
 3600     delete($rh_options->{$alias});                               # remove the alias from the initial list
 3601   }
 3602 
 3603 }
 3604 
 3605 =head4 set_default_options
 3606 
 3607     set_default_options(\%options,
 3608         '_filter_name'  =>  'filter',
 3609         'option5'   =>  .0001,
 3610         'option7'   =>  'ascii',
 3611         'allow_unknown_options  =>  0,
 3612     }
 3613 
 3614 Note that the first entry is a reference to the options with which the filter was called.
 3615 
 3616 The option5 is set to .0001 unless the option is explicitly set when the subroutine is called.
 3617 
 3618 The B<'_filter_name'> option should always be set, although there is no error if it is missing.
 3619 It is used mainly for debugging answer evaluators and allows
 3620 you to keep track of which filter is currently processing the answer.
 3621 
 3622 If B<'allow_unknown_options'> is set to 0 then if the filter is called with options which do NOT appear in the
 3623 set_default_options list an error will be signaled and a warning message will be printed out.  This provides
 3624 error checking against misspelling an option and is generally what is desired for most filters.
 3625 
 3626 Occasionally one wants to write a filter which accepts a long list of options, not all of which are known in advance,
 3627 but only uses a subset of the options
 3628 provided.  In this case, setting 'allow_unkown_options' to 1 prevents the error from being signaled.
 3629 
 3630 =cut
 3631 
 3632 sub set_default_options {
 3633   my $rh_options = shift;
 3634   warn "The first entry to set_default_options must be a reference to the option hash" unless ref($rh_options) eq 'HASH';
 3635   my %default_options = @_;
 3636   unless ( defined($default_options{allow_unknown_options}) and $default_options{allow_unknown_options} == 1 ) {
 3637     foreach  my $key1 (keys %$rh_options) {
 3638       warn "This option |$key1| is not recognized in this subroutine<br> ", pretty_print($rh_options) unless exists($default_options{$key1});
 3639     }
 3640   }
 3641   foreach my $key (keys %default_options) {
 3642     if  ( not defined($rh_options->{$key} ) and defined( $default_options{$key} )  ) {
 3643       $rh_options->{$key} = $default_options{$key};  #this allows     tol   => undef to allow the tol option, but doesn't define
 3644                                                      # this key unless tol is explicitly defined.
 3645     }
 3646   }
 3647 }
 3648 
 3649 =head2 Problem Grader Subroutines
 3650 
 3651 =cut
 3652 
 3653 ## Problem Grader Subroutines
 3654 
 3655 #####################################
 3656 # This is a model for plug-in problem graders
 3657 #####################################
 3658 sub install_problem_grader {
 3659   my $rf_problem_grader = shift;
 3660   $main::PG_FLAGS{PROBLEM_GRADER_TO_USE} = $rf_problem_grader;
 3661 }
 3662 
 3663 =head4 std_problem_grader
 3664 
 3665 This is an all-or-nothing grader.  A student must get all parts of the problem write
 3666 before receiving credit.  You should make sure to use this grader on multiple choice
 3667 and true-false questions, otherwise students will be able to deduce how many
 3668 answers are correct by the grade reported by webwork.
 3669 
 3670 
 3671   install_problem_grader(~~&std_problem_grader);
 3672 
 3673 =cut
 3674 
 3675 sub std_problem_grader {
 3676   my $rh_evaluated_answers = shift;
 3677   my $rh_problem_state = shift;
 3678   my %form_options = @_;
 3679   my %evaluated_answers = %{$rh_evaluated_answers};
 3680   #  The hash $rh_evaluated_answers typically contains:
 3681   #    'answer1' => 34, 'answer2'=> 'Mozart', etc.
 3682 
 3683   # By default the  old problem state is simply passed back out again.
 3684   my %problem_state = %$rh_problem_state;
 3685 
 3686   # %form_options might include
 3687   # The user login name
 3688   # The permission level of the user
 3689   # The studentLogin name for this psvn.
 3690   # Whether the form is asking for a refresh or is submitting a new answer.
 3691 
 3692   # initial setup of the answer
 3693   my %problem_result = ( score    => 0,
 3694                errors   => '',
 3695              type   => 'std_problem_grader',
 3696              msg    => '',
 3697   );
 3698   # Checks
 3699 
 3700   my $ansCount = keys %evaluated_answers;  # get the number of answers
 3701 
 3702   unless ($ansCount > 0 ) {
 3703 
 3704     $problem_result{msg} = "This problem did not ask any questions.";
 3705     return(\%problem_result,\%problem_state);
 3706   }
 3707 
 3708   if ($ansCount > 1 ) {
 3709     $problem_result{msg} = 'In order to get credit for this problem all answers must be correct.' ;
 3710   }
 3711 
 3712   unless ($form_options{answers_submitted} == 1) {
 3713     return(\%problem_result,\%problem_state);
 3714   }
 3715 
 3716   my $allAnswersCorrectQ=1;
 3717   foreach my $ans_name (keys %evaluated_answers) {
 3718   # I'm not sure if this check is really useful.
 3719     if ( ( ref($evaluated_answers{$ans_name} ) eq 'HASH' ) or ( ref($evaluated_answers{$ans_name}) eq 'AnswerHash' ) )  {
 3720       $allAnswersCorrectQ = 0 unless( 1 == $evaluated_answers{$ans_name}->{score} );
 3721     }
 3722     else {
 3723       die "Error at file ",__FILE__,"line ", __LINE__,":  Answer |$ans_name| is not a hash reference\n".
 3724          $evaluated_answers{$ans_name} .
 3725          "This probably means that the answer evaluator for this answer\n" .
 3726          "is not working correctly.";
 3727       $problem_result{error} = "Error: Answer $ans_name is not a hash: $evaluated_answers{$ans_name}";
 3728     }
 3729   }
 3730   # report the results
 3731   $problem_result{score} = $allAnswersCorrectQ;
 3732 
 3733   # I don't like to put in this bit of code.
 3734   # It makes it hard to construct error free problem graders
 3735   # I would prefer to know that the problem score was numeric.
 3736   unless (defined($problem_state{recorded_score}) and $problem_state{recorded_score} =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/ ) {
 3737     $problem_state{recorded_score} = 0;  # This gets rid of non-numeric scores
 3738   }
 3739   #
 3740   if ($allAnswersCorrectQ == 1 or $problem_state{recorded_score} == 1) {
 3741     $problem_state{recorded_score} = 1;
 3742   }
 3743   else {
 3744     $problem_state{recorded_score} = 0;
 3745   }
 3746 
 3747   $problem_state{num_of_correct_ans}++ if $allAnswersCorrectQ == 1;
 3748   $problem_state{num_of_incorrect_ans}++ if $allAnswersCorrectQ == 0;
 3749   (\%problem_result, \%problem_state);
 3750 }
 3751 
 3752 =head4 std_problem_grader2
 3753 
 3754 This is an all-or-nothing grader.  A student must get all parts of the problem write
 3755 before receiving credit.  You should make sure to use this grader on multiple choice
 3756 and true-false questions, otherwise students will be able to deduce how many
 3757 answers are correct by the grade reported by webwork.
 3758 
 3759 
 3760   install_problem_grader(~~&std_problem_grader2);
 3761 
 3762 The only difference between the two versions
 3763 is at the end of the subroutine, where std_problem_grader2
 3764 records the attempt only if there have been no syntax errors,
 3765 whereas std_problem_grader records it regardless.
 3766 
 3767 =cut
 3768 
 3769 
 3770 
 3771 sub std_problem_grader2 {
 3772   my $rh_evaluated_answers = shift;
 3773   my $rh_problem_state = shift;
 3774   my %form_options = @_;
 3775   my %evaluated_answers = %{$rh_evaluated_answers};
 3776   #  The hash $rh_evaluated_answers typically contains:
 3777   #    'answer1' => 34, 'answer2'=> 'Mozart', etc.
 3778 
 3779   # By default the  old problem state is simply passed back out again.
 3780   my %problem_state = %$rh_problem_state;
 3781 
 3782   # %form_options might include
 3783   # The user login name
 3784   # The permission level of the user
 3785   # The studentLogin name for this psvn.
 3786   # Whether the form is asking for a refresh or is submitting a new answer.
 3787 
 3788   # initial setup of the answer
 3789   my %problem_result = ( score        => 0,
 3790              errors       => '',
 3791              type       => 'std_problem_grader',
 3792              msg        => '',
 3793   );
 3794 
 3795   # syntax errors are not counted.
 3796   my $record_problem_attempt = 1;
 3797   # Checks
 3798 
 3799   my $ansCount = keys %evaluated_answers;  # get the number of answers
 3800   unless ($ansCount > 0 ) {
 3801     $problem_result{msg} = "This problem did not ask any questions.";
 3802     return(\%problem_result,\%problem_state);
 3803   }
 3804 
 3805   if ($ansCount > 1 ) {
 3806     $problem_result{msg} = 'In order to get credit for this problem all answers must be correct.' ;
 3807   }
 3808 
 3809   unless ($form_options{answers_submitted} == 1) {
 3810     return(\%problem_result,\%problem_state);
 3811   }
 3812 
 3813   my  $allAnswersCorrectQ=1;
 3814   foreach my $ans_name (keys %evaluated_answers) {
 3815   # I'm not sure if this check is really useful.
 3816     if ( ( ref($evaluated_answers{$ans_name} ) eq 'HASH' ) or ( ref($evaluated_answers{$ans_name}) eq 'AnswerHash' ) )  {
 3817       $allAnswersCorrectQ = 0 unless( 1 == $evaluated_answers{$ans_name}->{score} );
 3818     }
 3819     else {
 3820       die "Error at file ",__FILE__,"line ", __LINE__,":  Answer |$ans_name| is not a hash reference\n".
 3821          $evaluated_answers{$ans_name} .
 3822          "This probably means that the answer evaluator for this answer\n" .
 3823          "is not working correctly.";
 3824       $problem_result{error} = "Error: Answer $ans_name is not a hash: $evaluated_answers{$ans_name}";
 3825     }
 3826   }
 3827   # report the results
 3828   $problem_result{score} = $allAnswersCorrectQ;
 3829 
 3830   # I don't like to put in this bit of code.
 3831   # It makes it hard to construct error free problem graders
 3832   # I would prefer to know that the problem score was numeric.
 3833   unless ($problem_state{recorded_score} =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/ ) {
 3834     $problem_state{recorded_score} = 0;  # This gets rid of non-numeric scores
 3835   }
 3836   #
 3837   if ($allAnswersCorrectQ == 1 or $problem_state{recorded_score} == 1) {
 3838     $problem_state{recorded_score} = 1;
 3839   }
 3840   else {
 3841     $problem_state{recorded_score} = 0;
 3842   }
 3843   # record attempt only if there have been no syntax errors.
 3844 
 3845   if ($record_problem_attempt == 1) {
 3846     $problem_state{num_of_correct_ans}++ if $allAnswersCorrectQ == 1;
 3847     $problem_state{num_of_incorrect_ans}++ if $allAnswersCorrectQ == 0;
 3848   }
 3849   else {
 3850     $problem_result{show_partial_correct_answers} = 0 ;  # prevent partial correct answers from being shown for syntax errors.
 3851   }
 3852   (\%problem_result, \%problem_state);
 3853 }
 3854 
 3855 =head4 avg_problem_grader
 3856 
 3857 This grader gives a grade depending on how many questions from the problem are correct.  (The highest
 3858 grade is the one that is kept.  One can never lower the recorded grade on a problem by repeating it.)
 3859 Many professors (and almost all students :-)  ) prefer this grader.
 3860 
 3861 
 3862   install_problem_grader(~~&avg_problem_grader);
 3863 
 3864 =cut
 3865 
 3866 
 3867 sub avg_problem_grader {
 3868     my $rh_evaluated_answers = shift;
 3869   my $rh_problem_state = shift;
 3870   my %form_options = @_;
 3871   my %evaluated_answers = %{$rh_evaluated_answers};
 3872   #  The hash $rh_evaluated_answers typically contains:
 3873   #    'answer1' => 34, 'answer2'=> 'Mozart', etc.
 3874 
 3875   # By default the  old problem state is simply passed back out again.
 3876   my %problem_state = %$rh_problem_state;
 3877 
 3878 
 3879   # %form_options might include
 3880   # The user login name
 3881   # The permission level of the user
 3882   # The studentLogin name for this psvn.
 3883   # Whether the form is asking for a refresh or is submitting a new answer.
 3884 
 3885   # initial setup of the answer
 3886   my  $total=0;
 3887   my %problem_result = ( score        => 0,
 3888              errors       => '',
 3889              type       => 'avg_problem_grader',
 3890              msg        => '',
 3891   );
 3892   my $count = keys %evaluated_answers;
 3893   $problem_result{msg} = 'You can earn partial credit on this problem.' if $count >1;
 3894   # Return unless answers have been submitted
 3895   unless ($form_options{answers_submitted} == 1) {
 3896     return(\%problem_result,\%problem_state);
 3897   }
 3898 
 3899   # Answers have been submitted -- process them.
 3900   foreach my $ans_name (keys %evaluated_answers) {
 3901     # I'm not sure if this check is really useful.
 3902     if ( ( ref($evaluated_answers{$ans_name} ) eq 'HASH' ) or ( ref($evaluated_answers{$ans_name}) eq 'AnswerHash' ) )  {
 3903       $total += $evaluated_answers{$ans_name}->{score};
 3904     }
 3905     else {
 3906       die "Error: Answer |$ans_name| is not a hash reference\n".
 3907          $evaluated_answers{$ans_name} .
 3908          "This probably means that the answer evaluator for this answer\n" .
 3909          "is not working correctly.";
 3910       $problem_result{error} = "Error: Answer $ans_name is not a hash: $evaluated_answers{$ans_name}";
 3911     }
 3912   }
 3913   # Calculate score rounded to three places to avoid roundoff problems
 3914   $problem_result{score} = $total/$count if $count;
 3915   # increase recorded score if the current score is greater.
 3916   $problem_state{recorded_score} = $problem_result{score} if $problem_result{score} > $problem_state{recorded_score};
 3917 
 3918 
 3919   $problem_state{num_of_correct_ans}++ if $total == $count;
 3920   $problem_state{num_of_incorrect_ans}++ if $total < $count ;
 3921   warn "Error in grading this problem the total $total is larger than $count" if $total > $count;
 3922   (\%problem_result, \%problem_state);
 3923 }
 3924 
 3925 =head2 Utility subroutines
 3926 
 3927 =head4
 3928 
 3929   warn pretty_print( $rh_hash_input)
 3930 
 3931 This can be very useful for printing out messages about objects while debugging
 3932 
 3933 =cut
 3934 
 3935 sub pretty_print {
 3936     my $r_input = shift;
 3937     my $out = '';
 3938     if ( not ref($r_input) ) {
 3939       $out = $r_input;    # not a reference
 3940     } elsif ("$r_input" =~/hash/i) {  # this will pick up objects whose '$self' is hash and so works better than ref($r_iput).
 3941       local($^W) = 0;
 3942     $out .= "$r_input " ."<TABLE border = \"2\" cellpadding = \"3\" BGCOLOR = \"#FFFFFF\">";
 3943     foreach my $key (lex_sort( keys %$r_input )) {
 3944       $out .= "<tr><TD> $key</TD><TD>=&gt;</td><td>&nbsp;".pretty_print($r_input->{$key}) . "</td></tr>";
 3945     }
 3946     $out .="</table>";
 3947   } elsif (ref($r_input) eq 'ARRAY' ) {
 3948     my @array = @$r_input;
 3949     $out .= "( " ;
 3950     while (@array) {
 3951       $out .= pretty_print(shift @array) . " , ";
 3952     }
 3953     $out .= " )";
 3954   } elsif (ref($r_input) eq 'CODE') {
 3955     $out = "$r_input";
 3956   } else {
 3957     $out = $r_input;
 3958   }
 3959     $out;
 3960 }
 3961 
 3962 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9