[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 2 - (download) (as text) (annotate)
Thu Jun 14 17:08:51 2001 UTC (11 years, 11 months ago) by sam
File size: 128984 byte(s)
initial import

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9