[system] / trunk / pg / macros / PGanswermacros.pl Repository:
ViewVC logotype

View of /trunk/pg/macros/PGanswermacros.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3759 - (download) (as text) (annotate)
Sat Nov 12 01:31:10 2005 UTC (7 years, 6 months ago) by gage
File size: 176306 byte(s)
Roll back my changes.  Use Davide's method of detecting the preview Button

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9