[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 35 - (download) (as text) (annotate)
Wed Jun 20 19:04:08 2001 UTC (12 years ago) by chris
File size: 143169 byte(s)
Modified num_cmp to use filters for string comparsions.  Routed std_num_str_cmp through num_cmp routines.

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9