[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 5 - (download) (as text) (annotate)
Thu Jun 14 23:45:41 2001 UTC (11 years, 11 months ago) by gage
File size: 131188 byte(s)
dev-1-7-01

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9