[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 49 - (download) (as text) (annotate)
Thu Jun 21 19:53:59 2001 UTC (12 years ago) by chris
File size: 144309 byte(s)
Added "evaluatesToNumber" subroutine.  In conjunction with fix_answer_for_display filter, this now fixes a bug with displaying student_ans in a problem that allows strings for valid answers.

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9