[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 110 - (download) (as text) (annotate)
Thu Aug 9 21:39:26 2001 UTC (18 years, 6 months ago) by gage
File size: 138377 byte(s)
Changes made to best approximation parameters -- added
set_default_options call.  Removed some warning messages.

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9