[system] / trunk / pg / lib / Parser / Legacy / PGanswermacros.pl Repository:
ViewVC logotype

View of /trunk/pg/lib/Parser/Legacy/PGanswermacros.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3443 - (download) (as text) (annotate)
Mon Aug 1 12:42:21 2005 UTC (7 years, 10 months ago) by dpvc
File size: 175134 byte(s)
Added postfilter to Parser-based implementation of NUM_CMP so that the
student's answer is updated to be the result of evaluating the answer
(when there is no error in evaluating the answer).  This correctly
reflects the behaviour of the original NUM_CMP.

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9