From: dpvc v. a. <we...@ma...> - 2005-07-04 20:15:25
|
Log Message: ----------- This is an attempt at making the traditional answer checkers call the new Parser in place of their original ones. That is, if you follow the instructions in the README file, then problems that use std_num_cmp(), fun_cmp(), etc. will really be using the new Parser instead of the original PGanswermacros.pl versions. The old answer checkers are still available, and can be switched back on a site-wide, course-wide, or problem-by-problem basis. See the README for details. Added Files: ----------- pg/lib/Parser/Legacy: LimitedNumeric.pm NumberWithUnits.pm PGanswermacros.pl README Revision Data ------------- --- /dev/null +++ lib/Parser/Legacy/LimitedNumeric.pm @@ -0,0 +1,87 @@ +########################################################## +# +# Implements a context in which numbers can be entered, +# but no operations are permitted between them. +# +# There are two versions: one a number with no operations, +# and one for fractions of integers. Select them using +# one of the following commands: +# +# Context("LimiteNumeric"); +# Context("LimitedNumeric-Fraction"); +# + + +# +# Minus can only appear in front of a number +# +package Parser::Legacy::LimitedNumeric::UOP::minus; +our @ISA = qw(Parser::UOP::minus); + +sub _check { + my $self = shift; + $self->SUPER::_check; + my $uop = $self->{def}{string} || $self->{uop}; + $self->Error("You can only use '$uop' with (non-negative) numbers") + unless $self->{op}->class =~ /Number|DIVIDE/; +} + +sub class {'MINUS'}; + + +# +# Divides can only appear between numbers or a negative +# number and a number +# +package Parser::Legacy::LimitedNumeric::BOP::divide; +our @ISA = qw(Parser::BOP::divide); + +sub _check { + my $self = shift; + $self->SUPER::_check; + my $bop = $self->{def}{string} || $self->{bop}; + $self->Error("You can only use '$bop' between (non-negative) numbers") + unless $self->{lop}->class =~ /Number|MINUS/ && + $self->{rop}->class eq 'Number'; +} + +sub class {'DIVIDE'}; + + +package Parser::Legacy::LimitedNumeric; + +# +# LimitedNumeric context uses the modified minus +# and removes all other operatots, functions and parentheses +# +my $context = $Parser::Context::Default::context{Numeric}->copy; +$Parser::Context::Default::context{'LimitedNumeric'} = $context; +$context->operators->set('u-' => {class => 'Parser::Legacy::LimitedNumeric::UOP::minus'}); +$context->operators->undefine( + '+', '-', '*', '* ', ' *', ' ', '/', '/ ', ' /', '^', '**', + 'U', '.', '><', 'u+', '!', '_', ',', +); +$context->parens->undefine('|','{','(','['); +$context->functions->disable('All'); + +# +# For the Fraction versions, allow the modified division, and +# make sure numbers are just integers +# +$context = $Parser::Context::Default::context{Numeric}->copy; +$Parser::Context::Default::context{'LimitedNumeric-Fraction'} = $context; +$context->operators->set( + 'u-' => {class => 'Parser::Legacy::LimitedNumeric::UOP::minus'}, + '/' => {class => 'Parser::Legacy::LimitedNumeric::BOP::divide'}, + ' /' => {class => 'Parser::Legacy::LimitedNumeric::BOP::divide'}, + '/ ' => {class => 'Parser::Legacy::LimitedNumeric::BOP::divide'}, +); +$context->operators->undefine( + '+', '-', '*', '* ', ' *', ' ', '^', '**', + 'U', '.', '><', 'u+', '!', '_', ',', +); +$context->parens->undefine('|','{','['); +$context->functions->disable('All'); +Parser::Number::NoDecimals($context); + +1; --- /dev/null +++ lib/Parser/Legacy/PGanswermacros.pl @@ -0,0 +1,4755 @@ +# This file is PGanswermacros.pl +# This includes the subroutines for the ANS macros, that +# is, macros allowing a more flexible answer checking +#################################################################### +# Copyright @ 1995-2000 University of Rochester +# All Rights Reserved +#################################################################### +#$Id: PGanswermacros.pl,v 1.1 2005/07/04 20:12:22 dpvc Exp $ + +=head1 NAME + + PGanswermacros.pl -- located in the courseScripts directory + +=head1 SYNPOSIS + + Number Answer Evaluators: + num_cmp() -- uses an input hash to determine parameters + + std_num_cmp(), std_num_cmp_list(), std_num_cmp_abs, std_num_cmp_abs_list() + frac_num_cmp(), frac_num_cmp_list(), frac_num_cmp_abs, frac_num_cmp_abs_list() + arith_num_cmp(), arith_num_cmp_list(), arith_num_cmp_abs, arith_num_cmp_abs_list() + strict_num_cmp(), strict_num_cmp_list(), strict_num_cmp_abs, strict_num_cmp_abs_list() + numerical_compare_with_units() -- requires units as part of the answer + std_num_str_cmp() -- also accepts a set of strings as possible answers + + Function Answer Evaluators: + fun_cmp() -- uses an input hash to determine parameters + + function_cmp(), function_cmp_abs() + function_cmp_up_to_constant(), function_cmp_up_to_constant_abs() + multivar_function_cmp() + + String Answer Evaluators: + str_cmp() -- uses an input hash to determine parameters + + std_str_cmp(), std_str_cmp_list(), std_cs_str_cmp(), std_cs_str_cmp_list() + strict_str_cmp(), strict_str_cmp_list() + ordered_str_cmp(), ordered_str_cmp_list(), ordered_cs_str_cmp(), ordered_cs_str_cmp_list() + unordered_str_cmp(), unordered_str_cmp_list(), unordered_cs_str_cmp(), unordered_cs_str_cmp_list() + + Miscellaneous Answer Evaluators: + checkbox_cmp() + radio_cmp() + +=cut + +=head1 DESCRIPTION + +This file adds subroutines which create "answer evaluators" for checking +answers. Each answer evaluator accepts a single input from a student answer, +checks it and creates an output hash %ans_hash with seven or eight entries +(the preview_latex_string is optional). The output hash is now being created +with the AnswerHash package "class", which is located at the end of this file. +This class is currently just a wrapper for the hash, but this might change in +the future as new capabilities are added. + + score => $correctQ, + correct_ans => $originalCorrEqn, + student_ans => $modified_student_ans + original_student_ans => $original_student_answer, + ans_message => $PGanswerMessage, + type => 'typeString', + preview_text_string => $preview_text_string, + preview_latex_string => $preview_latex_string + + + $ans_hash{score} -- a number between 0 and 1 indicating + whether the answer is correct. Fractions + allow the implementation of partial + credit for incorrect answers. + $ans_hash{correct_ans} -- The correct answer, as supplied by the + instructor and then formatted. This can + be viewed by the student after the answer date. + $ans_hash{student_ans} -- This is the student answer, after reformatting; + for example the answer might be forced + to capital letters for comparison with + the instructors answer. For a numerical + answer, it gives the evaluated answer. + This is displayed in the section reporting + the results of checking the student answers. + $ans_hash{original_student_ans} -- This is the original student answer. This is displayed + on the preview page and may be used for sticky answers. + $ans_hash{ans_message} -- Any error message, or hint provided by the answer evaluator. + This is also displayed in the section reporting + the results of checking the student answers. + $ans_hash{type} -- A string indicating the type of answer evaluator. This + helps in preprocessing the student answer for errors. + Some examples: + 'number_with_units' + 'function' + 'frac_number' + 'arith_number' + $ans_hash{preview_text_string} -- This typically shows how the student answer was parsed. It is + displayed on the preview page. For a student answer of 2sin(3x) + this would be 2*sin(3*x). For string answers it is typically the + same as $ans_hash{student_ans}. + $ans_hash{preview_latex_string} -- THIS IS OPTIONAL. This is latex version of the student answer + which is used to show a typeset view on the answer on the preview + page. For a student answer of 2/3, this would be \frac{2}{3}. + +Technical note: the routines in this file are not actually answer evaluators. Instead, they create +answer evaluators. An answer evaluator is an anonymous subroutine, referenced by a named scalar. The +routines in this file build the subroutine and return a reference to it. Later, when the student +actually enters an answer, the problem processor feeds that answer to the referenced subroutine, which +evaluates it and returns a score (usually 0 or 1). For most users, this distinction is unimportant, but +if you plan on writing your own answer evaluators, you should understand this point. + +=cut + +BEGIN { + be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. +} + + +my ($BR , # convenient localizations. + $PAR , + $numRelPercentTolDefault , + $numZeroLevelDefault , + $numZeroLevelTolDefault , + $numAbsTolDefault , + $numFormatDefault , + $functRelPercentTolDefault , + $functZeroLevelDefault , + $functZeroLevelTolDefault , + $functAbsTolDefault , + $functNumOfPoints , + $functVarDefault , + $functLLimitDefault , + $functULimitDefault , + $functMaxConstantOfIntegration , + $CA , + $rh_envir , + $useBaseTenLog , + $inputs_ref , + $QUESTIONNAIRE_ANSWERS , +$user_context, +$Context, +); + + + + +sub _PGanswermacros_init { + + $BR = main::PG_restricted_eval(q!$main::BR!); + $PAR = main::PG_restricted_eval(q!$main::PAR!); + + # import defaults + # these are now imported from the %envir variable + $numRelPercentTolDefault = main::PG_restricted_eval(q!$main::numRelPercentTolDefault!); + $numZeroLevelDefault = main::PG_restricted_eval(q!$main::numZeroLevelDefault!); + $numZeroLevelTolDefault = main::PG_restricted_eval(q!$main::numZeroLevelTolDefault!); + $numAbsTolDefault = main::PG_restricted_eval(q!$main::numAbsTolDefault!); + $numFormatDefault = main::PG_restricted_eval(q!$main::numFormatDefault!); + $functRelPercentTolDefault = main::PG_restricted_eval(q!$main::functRelPercentTolDefault!); + $functZeroLevelDefault = main::PG_restricted_eval(q!$main::functZeroLevelDefault!); + $functZeroLevelTolDefault = main::PG_restricted_eval(q!$main::functZeroLevelTolDefault!); + $functAbsTolDefault = main::PG_restricted_eval(q!$main::functAbsTolDefault!); + $functNumOfPoints = main::PG_restricted_eval(q!$main::functNumOfPoints!); + $functVarDefault = main::PG_restricted_eval(q!$main::functVarDefault!); + $functLLimitDefault = main::PG_restricted_eval(q!$main::functLLimitDefault!); + $functULimitDefault = main::PG_restricted_eval(q!$main::functULimitDefault!); + $functMaxConstantOfIntegration = main::PG_restricted_eval(q!$main::functMaxConstantOfIntegration!); + $rh_envir = main::PG_restricted_eval(q!\%main::envir!); + $useBaseTenLog = main::PG_restricted_eval(q!$main::useBaseTenLog!); + $inputs_ref = main::PG_restricted_eval(q!$main::inputs_ref!); + $QUESTIONNAIRE_ANSWERS = ''; + + if (!main::PG_restricted_eval(q!$main::useOldAnswerMacros!)) { + # + # Force loading of Parser.pl and get the Context command + # + main::PG_restricted_eval(q!loadMacros("Parser.pl")!); + $Context = main::PG_restricted_eval(q!\&Context!); + } +} + + + +########################################################################## + +#Note use $rh_envir to read environment variables + +########################################################################## +## Number answer evaluators + +=head2 Number Answer Evaluators + +Number answer evaluators take in a numerical answer, compare it to the correct answer, +and return a score. In addition, they can choose to accept or reject an answer based on +its format, closeness to the correct answer, and other criteria. There are two types +of numerical answer evaluators: num_cmp(), which takes a hash of named options as parameters, +and the "mode"_num_cmp() variety, which use different functions to access different sets of +options. In addition, there is the special case of std_num_str_cmp(), which can evaluate +both numbers and strings. + +Numerical Comparison Options + + correctAnswer -- This is the correct answer that the student answer will + be compared to. However, this does not mean that the + student answer must match this exactly. How close the + student answer must be is determined by the other + options, especially tolerance and format. + + tolerance -- These options determine how close the student answer + must be to the correct answer to qualify. There are two + types of tolerance: relative and absolute. Relative + tolerances are given in percentages. A relative + tolerance of 1 indicates that the student answer must + be within 1% of the correct answer to qualify as correct. + In other words, a student answer is correct when + abs(studentAnswer - correctAnswer) <= abs(.01*relpercentTol*correctAnswer) + Using absolute tolerance, the student answer must be a + fixed distance from the correct answer to qualify. + For example, an absolute tolerance of 5 means that any + number which is +-5 of the correct answer qualifies as correct. + Final (rarely used) tolerance options are zeroLevel + and zeroLevelTol, used in conjunction with relative + tolerance. if correctAnswer has absolute value less than + or equal to zeroLevel, then the student answer must be, + in absolute terms, within zeroLevelTol of correctAnswer, i.e., + abs(studentAnswer - correctAnswer) <= zeroLevelTol. + In other words, if the correct answer is very near zero, + an absolute tolerance will be used. One must do this to + handle floating point answers very near zero, because of + the inaccuracy of floating point arithmetic. However, the + default values are almost always adequate. + + mode -- This determines the allowable methods for entering an + answer. Answers which do not meet this requirement will + be graded as incorrect, regardless of their numerical + value. The recognized modes are: + 'std' (default) -- allows any expression which evaluates + to a number, including those using + elementary functions like sin() and + exp(), as well as the operations of + arithmetic (+, -, *, /, ^) + 'strict' -- only decimal numbers are allowed + 'frac' -- whole numbers and fractions are allowed + 'arith' -- arithmetic expressions are allowed, but + no functions + Note that all modes allow the use of "pi" and "e" as + constants, and also the use of "E" to represent scientific + notation. + + format -- The format to use when displaying the correct and + submitted answers. This has no effect on how answers are + evaluated; it is only for cosmetic purposes. The + formatting syntax is the same as Perl uses for the sprintf() + function. Format strings are of the form '%m.nx' or '%m.nx#', + where m and n are described below, and x is a formatter. + Esentially, m is the minimum length of the field + (make this negative to left-justify). Note that the decimal + point counts as a character when determining the field width. + If m begins with a zero, the number will be padded with zeros + instead of spaces to fit the field. + The precision specifier (n) works differently, depending + on which formatter you are using. For d, i, o, u, x and X + formatters (non-floating point formatters), n is the minimum + number of digits to display. For e and f, it is the number of + digits that appear after the decimal point (extra digits will + be rounded; insufficient digits will be padded with spaces--see + '#' below). For g, it is the number of significant digits to + display. + The full list of formatters can be found in the manpage + for printf(3), or by typing "perldoc -f sprintf" at a + terminal prompt. The following is a brief summary of the + most frequent formatters: + d -- decimal number + ld -- long decimal number + u -- unsigned decimal number + lu -- long unsigned decimal number + x -- hexadecimal number + o -- octal number + e -- floating point number in scientific notation + f -- floating point number + g -- either e or f, whichever takes less space + Technically, g will use e if the exponent is less than -4 or + greater than or equal to the precision. Trailing zeros are + removed in this mode. + If the format string ends in '#', trailing zeros will be + removed in the decimal part. Note that this is not a standard + syntax; it is handled internally by WeBWorK and not by Perl + (although this should not be a concern to end users). + The default format is '%0.5f#', which displays as a floating + point number with 5 digits of precision and no trailing zeros. + Other useful format strings might be '%0.2f' for displaying + dollar amounts, or '%010d' to display an integer with leading + zeros. Setting format to an empty string ( '' ) means no + formatting will be used; this will show 'arbitrary' precision + floating points. + +Default Values (As of 7/24/2000) (Option -- Variable Name -- Value) + + Format -- $numFormatDefault -- "%0.5f#" + Relative Tolerance -- $numRelPercentTolDefault -- .1 + Absolute Tolerance -- $numAbsTolDefault -- .001 + Zero Level -- $numZeroLevelDefault -- 1E-14 + Zero Level Tolerance -- $numZeroLevelTolDefault -- 1E-12 + +=cut + + +=head3 num_cmp() + +Compares a number or a list of numbers, using a named hash of options to set +parameters. This can make for more readable code than using the "mode"_num_cmp() +style, but some people find one or the other easier to remember. + +ANS( num_cmp( answer or answer_array_ref, options_hash ) ); + + 1. the correct answer, or a reference to an array of correct answers + 2. a hash with the following keys (all optional): + mode -- 'std' (default) (allows any expression evaluating to + a number) + 'strict' (only numbers are allowed) + 'frac' (fractions are allowed) + 'arith' (arithmetic expressions allowed) + format -- '%0.5f#' (default); defines formatting for the + correct answer + tol -- an absolute tolerance, or + relTol -- a relative tolerance + units -- the units to use for the answer(s) + strings -- a reference to an array of strings which are valid + answers (works like std_num_str_cmp() ) + zeroLevel -- if the correct answer is this close to zero, + then zeroLevelTol applies + zeroLevelTol -- absolute tolerance to allow when answer is close + to zero + + debug -- if set to 1, provides verbose listing of + hash entries throughout fliters. + + Returns an answer evaluator, or (if given a reference to an array of + answers), a list of answer evaluators. Note that a reference to an array of + answers results is just a shortcut for writing a separate <code>num_cmp()</code> for each + answer. + +EXAMPLES: + + num_cmp( 5 ) -- correct answer is 5, using defaults + for all options + num_cmp( [5,6,7] ) -- correct answers are 5, 6, and 7, + using defaults for all options + num_cmp( 5, mode => 'strict' ) -- correct answer is 5, mode is strict + num_cmp( [5,6], relTol => 5 ) -- correct answers are 5 and 6, + both with 5% relative tolerance + num_cmp( 6, strings => ["Inf", "Minf", "NaN"] ) + -- correct answer is 6, "Inf", "Minf", + and "NaN" recognized as valid, but + incorrect answers. + num_cmp( "-INF", strings => ["INF", "-INF"] ) + -- correct answer is "-INF", "INF" and + numerical expressions recognized as valid, + but incorrect answers. + + +=cut + +sub num_cmp { + my $correctAnswer = shift @_; + $CA = $correctAnswer; + my @opt = @_; + my %out_options; + +######################################################################### +# Retain this first check for backword compatibility. Allows input of the form +# num_cmp($ans, 1, '%0.5f') but warns against it +######################################################################### + my %known_options = ( + 'mode' => 'std', + 'format' => $numFormatDefault, + 'tol' => $numAbsTolDefault, + 'relTol' => $numRelPercentTolDefault, + 'units' => undef, + 'strings' => undef, + 'zeroLevel' => $numZeroLevelDefault, + 'zeroLevelTol' => $numZeroLevelTolDefault, + 'tolType' => 'relative', + 'tolerance' => 1, + 'reltol' => undef, #alternate spelling + 'unit' => undef, #alternate spelling + 'debug' => 0 + ); + + my @output_list; + my( $relPercentTol, $format, $zeroLevel, $zeroLevelTol) = @opt; + + unless( ref($correctAnswer) eq 'ARRAY' || scalar( @opt ) == 0 || + ( defined($opt[0]) and exists $known_options{$opt[0]} ) ) { + # unless the first parameter is a list of arrays + # or the second parameter is a known option or + # no options were used, + # use the old num_cmp which does not use options, but has inputs + # $relPercentTol,$format,$zeroLevel,$zeroLevelTol + warn "This method of using num_cmp() is deprecated. Please rewrite this" . + " problem using the options style of parameter passing (or" . + " check that your first option is spelled correctly)."; + + %out_options = ( 'relTol' => $relPercentTol, + 'format' => $format, + 'zeroLevel' => $zeroLevel, + 'zeroLevelTol' => $zeroLevelTol, + 'mode' => 'std' + ); + } + +######################################################################### +# Now handle the options assuming they are entered in the form +# num_cmp($ans, relTol=>1, format=>'%0.5f') +######################################################################### + %out_options = @opt; + assign_option_aliases( \%out_options, + 'reltol' => 'relTol', + 'unit' => 'units', + 'abstol' => 'tol', + ); + + set_default_options( \%out_options, + 'tolType' => (defined($out_options{'tol'}) ) ? 'absolute' : 'relative', # the existence of "tol" means that we use absolute tolerance mode + 'tolerance' => (defined($out_options{'tolType'}) && $out_options{'tolType'} eq 'absolute' ) ? $numAbsTolDefault : $numRelPercentTolDefault, # relative tolerance is the default + 'mode' => 'std', + 'format' => $numFormatDefault, + 'tol' => undef, + 'relTol' => undef, + 'units' => undef, + 'strings' => undef, + 'zeroLevel' => $numZeroLevelDefault, + 'zeroLevelTol' => $numZeroLevelTolDefault, + 'debug' => 0, + ); + + # can't use both units and strings + if( defined( $out_options{'units'} ) && defined( $out_options{'strings'} ) ) { + warn "Can't use both 'units' and 'strings' in the same problem " . + "(check your parameters to num_cmp() )"; + } + + # absolute tolType and relTol are incompatible. So are relative tolType and tol + if( defined( $out_options{'relTol'} ) && $out_options{'tolType'} eq 'absolute' ) { + warn "The 'tolType' 'absolute' is not compatible with 'relTol' " . + "(check your parameters to num_cmp() )"; + } + if( defined( $out_options{'tol'} ) && $out_options{'tolType'} eq 'relative' ) { + warn "The 'tolType' 'relative' is not compatible with 'tol' " . + "(check your parameters to num_cmp() )"; + } + + + # Handle legacy options + if ($out_options{tolType} eq 'absolute') { + $out_options{'tolerance'}=$out_options{'tol'} if defined($out_options{'tol'}); + delete($out_options{'relTol'}) if exists( $out_options{'relTol'} ); + } else { + $out_options{'tolerance'}=$out_options{'relTol'} if defined($out_options{'relTol'}); + # delete($out_options{'tol'}) if exists( $out_options{'tol'} ); + } + # end legacy options + + # thread over lists + my @ans_list = (); + + if ( ref($correctAnswer) eq 'ARRAY' ) { + @ans_list = @{$correctAnswer}; + } + else { push( @ans_list, $correctAnswer ); + } + + # produce answer evaluators + foreach my $ans (@ans_list) { + if( defined( $out_options{'units'} ) ) { + $ans = "$ans $out_options{'units'}"; + + push( @output_list, NUM_CMP( 'correctAnswer' => $ans, + 'tolerance' => $out_options{'tolerance'}, + 'tolType' => $out_options{'tolType'}, + 'format' => $out_options{'format'}, + 'mode' => $out_options{'mode'}, + 'zeroLevel' => $out_options{'zeroLevel'}, + 'zeroLevelTol' => $out_options{'zeroLevelTol'}, + 'debug' => $out_options{'debug'}, + 'units' => $out_options{'units'}, + ) + ); + } elsif( defined( $out_options{'strings'} ) ) { + + + push( @output_list, NUM_CMP( 'correctAnswer' => $ans, + 'tolerance' => $out_options{tolerance}, + 'tolType' => $out_options{tolType}, + 'format' => $out_options{'format'}, + 'mode' => $out_options{'mode'}, + 'zeroLevel' => $out_options{'zeroLevel'}, + 'zeroLevelTol' => $out_options{'zeroLevelTol'}, + 'debug' => $out_options{'debug'}, + 'strings' => $out_options{'strings'}, + ) + ); + } else { + push(@output_list, + NUM_CMP( 'correctAnswer' => $ans, + 'tolerance' => $out_options{tolerance}, + 'tolType' => $out_options{tolType}, + 'format' => $out_options{'format'}, + 'mode' => $out_options{'mode'}, + 'zeroLevel' => $out_options{'zeroLevel'}, + 'zeroLevelTol' => $out_options{'zeroLevelTol'}, + 'debug' => $out_options{'debug'}, + ), + ); + } + } + + return (wantarray) ? @output_list : $output_list[0]; +} + +#legacy code for compatability purposes +sub num_rel_cmp { # compare numbers + std_num_cmp( @_ ); +} + + +=head3 "mode"_num_cmp() functions + +There are 16 functions total, 4 for each mode (std, frac, strict, arith). Each mode has +one "normal" function, one which accepts a list of answers, one which uses absolute +rather than relative tolerance, and one which uses absolute tolerance and accepts a list. +The "std" family is documented below; all others work precisely the same. + + std_num_cmp($correctAnswer) OR + std_num_cmp($correctAnswer, $relPercentTol) OR + std_num_cmp($correctAnswer, $relPercentTol, $format) OR + std_num_cmp($correctAnswer, $relPercentTol, $format, $zeroLevel) OR + std_num_cmp($correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol) + + $correctAnswer -- the correct answer + $relPercentTol -- the tolerance, as a percentage (optional) + $format -- the format of the displayed answer (optional) + $zeroLevel -- if the correct answer is this close to zero, then zeroLevelTol applies (optional) + $zeroLevelTol -- absolute tolerance to allow when correct answer is close to zero (optional) + + std_num_cmp() uses standard mode (arithmetic operations and elementary + functions allowed) and relative tolerance. Options are specified by + one or more parameters. Note that if you wish to set an option which + is later in the parameter list, you must set all previous options. + + std_num_cmp_abs($correctAnswer) OR + std_num_cmp_abs($correctAnswer, $absTol) OR + std_num_cmp_abs($correctAnswer, $absTol, $format) + + $correctAnswer -- the correct answer + $absTol -- an absolute tolerance (optional) + $format -- the format of the displayed answer (optional) + + std_num_cmp_abs() uses standard mode and absolute tolerance. Options + are set as with std_num_cmp(). Note that $zeroLevel and $zeroLevelTol + do not apply with absolute tolerance. + + std_num_cmp_list($relPercentTol, $format, @answerList) + + $relPercentTol -- the tolerance, as a percentage + $format -- the format of the displayed answer(s) + @answerList -- a list of one or more correct answers + + std_num_cmp_list() uses standard mode and relative tolerance. There + is no way to set $zeroLevel or $zeroLevelTol. Note that no + parameters are optional. All answers in the list will be + evaluated with the same set of parameters. + + std_num_cmp_abs_list($absTol, $format, @answerList) + + $absTol -- an absolute tolerance + $format -- the format of the displayed answer(s) + @answerList -- a list of one or more correct answers + + std_num_cmp_abs_list() uses standard mode and absolute tolerance. + Note that no parameters are optional. All answers in the list will be + evaluated with the same set of parameters. + + arith_num_cmp(), arith_num_cmp_list(), arith_num_cmp_abs(), arith_num_cmp_abs_list() + strict_num_cmp(), strict_num_cmp_list(), strict_num_cmp_abs(), strict_num_cmp_abs_list() + frac_num_cmp(), frac_num_cmp_list(), frac_num_cmp_abs(), frac_num_cmp_abs_list() + +Examples: + + ANS( strict_num_cmp( 3.14159 ) ) -- The student answer must be a number + in decimal or scientific notation which is within .1 percent of 3.14159. + This assumes $numRelPercentTolDefault has been set to .1. + ANS( strict_num_cmp( $answer, .01 ) ) -- The student answer must be a + number within .01 percent of $answer (e.g. 3.14159 if $answer is 3.14159 + or $answer is "pi" or $answer is 4*atan(1)). + ANS( frac_num_cmp( $answer) ) or ANS( frac_num_cmp( $answer,.01 )) -- + The student answer can be a number or fraction, e.g. 2/3. + ANS( arith_num_cmp( $answer) ) or ANS( arith_num_cmp( $answer,.01 )) -- + The student answer can be an arithmetic expression, e.g. (2+3)/7-2^.5 . + ANS( std_num_cmp( $answer) ) or ANS( std_num_cmp( $answer,.01 )) -- + The student answer can contain elementary functions, e.g. sin(.3+pi/2) + +=cut + +sub std_num_cmp { # compare numbers allowing use of elementary functions + my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; + + my %options = ( 'relTol' => $relPercentTol, + 'format' => $format, + 'zeroLevel' => $zeroLevel, + 'zeroLevelTol' => $zeroLevelTol + ); + + set_default_options( \%options, + 'tolType' => 'relative', + 'tolerance' => $numRelPercentTolDefault, + 'mode' => 'std', + 'format' => $numFormatDefault, + 'relTol' => $numRelPercentTolDefault, + 'zeroLevel' => $numZeroLevelDefault, + 'zeroLevelTol' => $numZeroLevelTolDefault, + 'debug' => 0, + ); + + num_cmp([$correctAnswer], %options); +} + +## Similar to std_num_cmp but accepts a list of numbers in the form +## std_num_cmp_list(relpercentTol,format,ans1,ans2,ans3,...) +## format is of the form "%10.3g" or "", i.e., a format suitable for sprintf(). Use "" for default +## You must enter a format and tolerance + +sub std_num_cmp_list { + my ( $relPercentTol, $format, @answerList) = @_; + + my %options = ( 'relTol' => $relPercentTol, + 'format' => $format, + ); + + set_default_options( \%options, + 'tolType' => 'relative', + 'tolerance' => $numRelPercentTolDefault, + 'mode' => 'std', + 'format' => $numFormatDefault, + 'relTol' => $numRelPercentTolDefault, + 'zeroLevel' => $numZeroLevelDefault, + 'zeroLevelTol' => $numZeroLevelTolDefault, + 'debug' => 0, + ); + + num_cmp(\@answerList, %options); + +} + +sub std_num_cmp_abs { # compare numbers allowing use of elementary functions with absolute tolerance + my ( $correctAnswer, $absTol, $format) = @_; + my %options = ( 'tolerance' => $absTol, + 'format' => $format + ); + + set_default_options (\%options, + 'tolType' => 'absolute', + 'tolerance' => $absTol, + 'mode' => 'std', + 'format' => $numFormatDefault, + 'zeroLevel' => 0, + 'zeroLevelTol' => 0, + 'debug' => 0, + ); + + num_cmp([$correctAnswer], %options); +} + +## See std_num_cmp_list for usage + +sub std_num_cmp_abs_list { + my ( $absTol, $format, @answerList ) = @_; + + my %options = ( 'tolerance' => $absTol, + 'format' => $format, + ); + + set_default_options( \%options, + 'tolType' => 'absolute', + 'tolerance' => $absTol, + 'mode' => 'std', + 'format' => $numFormatDefault, + 'zeroLevel' => 0, + 'zeroLevelTol' => 0, + 'debug' => 0, + ); + + num_cmp(\@answerList, %options); +} + +sub frac_num_cmp { # only allow fractions and numbers as submitted answer + + my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; + + my %options = ( 'relTol' => $relPercentTol, + 'format' => $format, + 'zeroLevel' => $zeroLevel, + 'zeroLevelTol' => $zeroLevelTol + ); + + set_default_options( \%options, + 'tolType' => 'relative', + 'tolerance' => $relPercentTol, + 'mode' => 'frac', + 'format' => $numFormatDefault, + 'zeroLevel' => $numZeroLevelDefault, + 'zeroLevelTol' => $numZeroLevelTolDefault, + 'relTol' => $numRelPercentTolDefault, + 'debug' => 0, + ); + + num_cmp([$correctAnswer], %options); +} + +## See std_num_cmp_list for usage +sub frac_num_cmp_list { + my ( $relPercentTol, $format, @answerList ) = @_; + + my %options = ( 'relTol' => $relPercentTol, + 'format' => $format + ); + + set_default_options( \%options, + 'tolType' => 'relative', + 'tolerance' => $relPercentTol, + 'mode' => 'frac', + 'format' => $numFormatDefault, + 'zeroLevel' => $numZeroLevelDefault, + 'zeroLevelTol' => $numZeroLevelTolDefault, + 'relTol' => $numRelPercentTolDefault, + 'debug' => 0, + ); + + num_cmp(\@answerList, %options); +} + +sub frac_num_cmp_abs { # only allow fraction expressions as submitted answer with absolute tolerance + my ( $correctAnswer, $absTol, $format ) = @_; + + my %options = ( 'tolerance' => $absTol, + 'format' => $format + ); + + set_default_options (\%options, + 'tolType' => 'absolute', + 'tolerance' => $absTol, + 'mode' => 'frac', + 'format' => $numFormatDefault, + 'zeroLevel' => 0, + 'zeroLevelTol' => 0, + 'debug' => 0, + ); + + num_cmp([$correctAnswer], %options); +} + +## See std_num_cmp_list for usage + +sub frac_num_cmp_abs_list { + my ( $absTol, $format, @answerList ) = @_; + + my %options = ( 'tolerance' => $absTol, + 'format' => $format + ); + + set_default_options (\%options, + 'tolType' => 'absolute', + 'tolerance' => $absTol, + 'mode' => 'frac', + 'format' => $numFormatDefault, + 'zeroLevel' => 0, + 'zeroLevelTol' => 0, + 'debug' => 0, + ); + + num_cmp(\@answerList, %options); +} + + +sub arith_num_cmp { # only allow arithmetic expressions as submitted answer + + my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; + + my %options = ( 'relTol' => $relPercentTol, + 'format' => $format, + 'zeroLevel' => $zeroLevel, + 'zeroLevelTol' => $zeroLevelTol + ); + + set_default_options( \%options, + 'tolType' => 'relative', + 'tolerance' => $relPercentTol, + 'mode' => 'arith', + 'format' => $numFormatDefault, + 'zeroLevel' => $numZeroLevelDefault, + 'zeroLevelTol' => $numZeroLevelTolDefault, + 'relTol' => $numRelPercentTolDefault, + 'debug' => 0, + ); + + num_cmp([$correctAnswer], %options); +} + +## See std_num_cmp_list for usage +sub arith_num_cmp_list { + my ( $relPercentTol, $format, @answerList ) = @_; + + my %options = ( 'relTol' => $relPercentTol, + 'format' => $format, + ); + + set_default_options( \%options, + 'tolType' => 'relative', + 'tolerance' => $relPercentTol, + 'mode' => 'arith', + 'format' => $numFormatDefault, + 'zeroLevel' => $numZeroLevelDefault, + 'zeroLevelTol' => $numZeroLevelTolDefault, + 'relTol' => $numRelPercentTolDefault, + 'debug' => 0, + ); + + num_cmp(\@answerList, %options); +} + +sub arith_num_cmp_abs { # only allow arithmetic expressions as submitted answer with absolute tolerance + my ( $correctAnswer, $absTol, $format ) = @_; + + my %options = ( 'tolerance' => $absTol, + 'format' => $format + ); + + set_default_options (\%options, + 'tolType' => 'absolute', + 'tolerance' => $absTol, + 'mode' => 'arith', + 'format' => $numFormatDefault, + 'zeroLevel' => 0, + 'zeroLevelTol' => 0, + 'debug' => 0, + ); + + num_cmp([$correctAnswer], %options); +} + +## See std_num_cmp_list for usage +sub arith_num_cmp_abs_list { + my ( $absTol, $format, @answerList ) = @_; + + my %options = ( 'tolerance' => $absTol, + 'format' => $format + ); + + set_default_options (\%options, + 'tolType' => 'absolute', + 'tolerance' => $absTol, + 'mode' => 'arith', + 'format' => $numFormatDefault, + 'zeroLevel' => 0, + 'zeroLevelTol' => 0, + 'debug' => 0, + ); + + num_cmp(\@answerList, %options); +} + +sub strict_num_cmp { # only allow numbers as submitted answer + my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; + + my %options = ( 'relTol' => $relPercentTol, + 'format' => $format, + 'zeroLevel' => $zeroLevel, + 'zeroLevelTol' => $zeroLevelTol + ); + + set_default_options( \%options, + 'tolType' => 'relative', + 'tolerance' => $relPercentTol, + 'mode' => 'strict', + 'format' => $numFormatDefault, + 'zeroLevel' => $numZeroLevelDefault, + 'zeroLevelTol' => $numZeroLevelTolDefault, + 'relTol' => $numRelPercentTolDefault, + 'debug' => 0, + ); + num_cmp([$correctAnswer], %options); + +} + +## See std_num_cmp_list for usage +sub strict_num_cmp_list { # compare numbers + my ( $relPercentTol, $format, @answerList ) = @_; + + my %options = ( 'relTol' => $relPercentTol, + 'format' => $format, + ); + + set_default_options( \%options, + 'tolType' => 'relative', + 'tolerance' => $relPercentTol, + 'mode' => 'strict', + 'format' => $numFormatDefault, + 'zeroLevel' => $numZeroLevelDefault, + 'zeroLevelTol' => $numZeroLevelTolDefault, + 'relTol' => $numRelPercentTolDefault, + 'debug' => 0, + ); + + num_cmp(\@answerList, %options); +} + + +sub strict_num_cmp_abs { # only allow numbers as submitted answer with absolute tolerance + my ( $correctAnswer, $absTol, $format ) = @_; + + my %options = ( 'tolerance' => $absTol, + 'format' => $format + ); + + set_default_options (\%options, + 'tolType' => 'absolute', + 'tolerance' => $absTol, + 'mode' => 'strict', + 'format' => $numFormatDefault, + 'zeroLevel' => 0, + 'zeroLevelTol' => 0, + 'debug' => 0, + ); + num_cmp([$correctAnswer], %options); + +} + +## See std_num_cmp_list for usage +sub strict_num_cmp_abs_list { # compare numbers + my ( $absTol, $format, @answerList ) = @_; + + my %options = ( 'tolerance' => $absTol, + 'format' => $format + ); + + set_default_options (\%options, + 'tolType' => 'absolute', + 'tolerance' => $absTol, + 'mode' => 'strict', + 'format' => $numFormatDefault, + 'zeroLevel' => 0, + 'zeroLevelTol' => 0, + 'debug' => 0, + ); + + num_cmp(\@answerList, %options); +} + +## sub numerical_compare_with_units +## Compares a number with units +## Deprecated; use num_cmp() +## +## IN: a string which includes the numerical answer and the units +## a hash with the following keys (all optional): +## mode -- 'std', 'frac', 'arith', or 'strict' +## format -- the format to use when displaying the answer +## tol -- an absolute tolerance, or +## relTol -- a relative tolerance +## zeroLevel -- if the correct answer is this close to zero, then zeroLevelTol applies +## zeroLevelTol -- absolute tolerance to allow when correct answer is close to zero + +# This mode is depricated. send input through num_cmp -- it can handle units. + +sub numerical_compare_with_units { + my $correct_answer = shift; # the answer is a string which includes both the numerical answer and the units. + my %options = @_; # all of the other inputs are (key value) pairs + + # Prepare the correct answer + $correct_answer = str_filters( $correct_answer, 'trim_whitespace' ); + + # it surprises me that the match below works since the first .* is greedy. + my ($correct_num_answer, $correct_units) = $correct_answer =~ /^(.*)\s+([^\s]*)$/; + $options{units} = $correct_units; + + num_cmp($correct_num_answer, %options); +} + + +=head3 std_num_str_cmp() + +NOTE: This function is maintained for compatibility. num_cmp() with the + 'strings' parameter is slightly preferred. + +std_num_str_cmp() is used when the correct answer could be either a number or a +string. For example, if you wanted the student to evaluate a function at number +of points, but write "Inf" or "Minf" if the function is unbounded. This routine +will provide error messages that do not give a hint as to whether the correct +answer is a string or a number. For numerical comparisons, std_num_cmp() is +used internally; for string comparisons, std_str_cmp() is used. String answers +must consist entirely of letters except that an initial minus sign is allowed. +E.g. "inf" and "-inf" are valid strings where as "too-big" is not. + + std_num_str_cmp( $correctAnswer ) OR + std_num_str_cmp( $correctAnswer, $ra_legalStrings ) OR + std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol ) OR + std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol, $format ) OR + std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol, $format, $zeroLevel ) OR + std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol, $format, + $zeroLevel, $zeroLevelTol ) + + $correctAnswer -- the correct answer + $ra_legalStrings -- a reference to an array of legal strings, e.g. ["str1", "str2"] + $relPercentTol -- the error tolerance as a percentage + $format -- the display format + $zeroLevel -- if the correct answer is this close to zero, then zeroLevelTol applies + $zeroLevelTol -- absolute tolerance to allow when correct answer is close to zero + +Examples: + ANS( std_num_str_cmp( $ans, ["Inf", "Minf", "NaN"] ) ); + ANS( std_num_str_cmp( $ans, ["INF", "-INF"] ) ); + +=cut + +sub std_num_str_cmp { + my ( $correctAnswer, $ra_legalStrings, $relpercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; + # warn ('This method is depreciated. Use num_cmp instead.'); + return num_cmp ($correctAnswer, strings=>$ra_legalStrings, relTol=>$relpercentTol, format=>$format, + zeroLevel=>$zeroLevel, zeroLevelTol=>$zeroLevelTol); +} + +sub NUM_CMP { # low level numeric compare (now uses Parser) + return ORIGINAL_NUM_CMP(@_) + if main::PG_restricted_eval(q!$main::useOldAnswerMacros!); + + my %num_params = @_; + + # + # check for required parameters + # + my @keys = qw(correctAnswer tolerance tolType format mode zeroLevel zeroLevelTol debug); + foreach my $key (@keys) { + warn "$key must be defined in options when calling NUM_CMP" + unless defined($num_params{$key}); + } + + my $correctAnswer = $num_params{correctAnswer}; + my $mode = $num_params{mode}; + my %options = (debug => $num_params{debug}); + + # + # Get an apppropriate context based on the mode + # + my $context; + for ($mode) { + /^strict$/i and do { + $context = &$Context("LimitedNumeric")->copy; + last; + }; + /^arith$/i and do { + $context = &$Context("Numeric")->copy; + $context->functions->disable('All'); + last; + }; + /^frac$/i and do { + $context = &$Context("LimitedNumeric-Fraction")->copy; + last; + }; + + # default + $context = &$Context("Numeric")->copy; + } + $context->{format}{number} = $num_params{'format'}; + $context->strings->clear; + # FIXME: should clear variables as well? Copy them from the current context? + + # + # Add the strings to the context + # + if (defined($num_params{strings}) && $num_params{strings}) { + foreach my $string (@{$num_params{strings}}) { + my %tex = ($string =~ m/(-?)inf(inity)?/i)? (TeX => "$1\\infty"): (); + $context->strings->add(uc($string) => {%tex}); + } + } + + # + # Set the tolerances + # + if ($num_params{tolType} eq 'relative') { + $context->flags->set( + tolerance => .01*$num_params{tolerance}, + tolType => 'relative', + ); + } else { + $context->flags->set( + tolerance => $num_params{tolerance}, + tolType => 'absolute', + ); + } + $context->flags->set( + zeroLevel => $num_params{zeroLevel}, + zeroLevelTol => $num_params{zeroLevelTol}, + ); + + # + # Get the proper Parser object for the professor's answer + # using the initialized context + # + my $oldContext = &$Context($context); my $r; + if (defined($num_params{units}) && $num_params{units}) { + $r = new Parser::Legacy::NumberWithUnits($correctAnswer); + $options{rh_correct_units} = $num_params{units}; + } else { + $r = Value::Formula->new($correctAnswer); + die "The professor's answer can't be a formula" unless $r->isConstant; + $r = $r->eval; $r = new Value::Real($r) unless Value::class($r) eq 'String'; + $r->{correct_ans} = $correctAnswer; + if ($mode eq 'phase_pi') { + my $pi = 4*atan2(1,1); + while ($r > $pi/2) {$r -= $pi} + while ($r < -$pi/2) {$r += $pi} + } + } + # + # Get the answer checker from the parser object + # + my $cmp = $r->cmp(%options); + $cmp->install_pre_filter(sub { + my $rh_ans = shift; + $rh_ans->{original_student_ans} = $rh_ans->{student_ans}; + $rh_ans->{original_correct_ans} = $rh_ans->{correct_ans}; + return $rh_ans; + }); + $cmp->{debug} = $num_params{debug}; + &$Context($oldContext); + + return $cmp; +} + +# +# The original version, for backward compatibility +# (can be removed when the Parser-based version is more fully tested.) +# +sub ORIGINAL_NUM_CMP { # low level numeric compare + my %num_params = @_; + + my @keys = qw ( correctAnswer tolerance tolType format mode zeroLevel zeroLevelTol debug ); + foreach my $key (@keys) { + warn "$key must be defined in options when calling NUM_CMP" unless defined ($num_params{$key}); + } + + my $correctAnswer = $num_params{'correctAnswer'}; + my $format = $num_params{'format'}; + my $mode = $num_params{'mode'}; + + if( $num_params{tolType} eq 'relative' ) { + $num_params{'tolerance'} = .01*$num_params{'tolerance'}; + } + + my $formattedCorrectAnswer; + my $correct_units; + my $correct_num_answer; + my %correct_units; + my $corrAnswerIsString = 0; + + + if (defined($num_params{units}) && $num_params{units}) { + $correctAnswer = str_filters( $correctAnswer, 'trim_whitespace' ); + # units are in form stuff space units where units contains no spaces. + + ($correct_num_answer, $correct_units) = $correctAnswer =~ /^(.*)\s+([^\s]*)$/; + %correct_units = Units::evaluate_units($correct_units); + if ( defined( $correct_units{'ERROR'} ) ) { + warn ("ERROR: The answer \"$correctAnswer\" in the problem definition cannot be parsed:\n" . + "$correct_units{'ERROR'}\n"); + } + # $formattedCorrectAnswer = spf($correct_num_answer,$num_params{'format'}) . " $correct_units"; + $formattedCorrectAnswer = prfmt($correct_num_answer,$num_params{'format'}) . " $correct_units"; + + } elsif (defined($num_params{strings}) && $num_params{strings}) { + my $legalString = ''; + my @legalStrings = @{$num_params{strings}}; + $correct_num_answer = $correctAnswer; + $formattedCorrectAnswer = $correctAnswer; + foreach $legalString (@legalStrings) { + if ( uc($correctAnswer) eq uc($legalString) ) { + $corrAnswerIsString = 1; + + last; + } + } ## at this point $corrAnswerIsString = 0 iff correct answer is numeric + } else { + $correct_num_answer = $correctAnswer; + $formattedCorrectAnswer = prfmt( $correctAnswer, $num_params{'format'} ); + } + + $correct_num_answer = math_constants($correct_num_answer); + + my $PGanswerMessage = ''; + + my ($inVal,$correctVal,$PG_eval_errors,$PG_full_error_report); + + if (defined($correct_num_answer) && $correct_num_answer =~ /\S/ && $corrAnswerIsString == 0 ) { + ($correctVal, $PG_eval_errors,$PG_full_error_report) = PG_answer_eval($correct_num_answer); + } else { # case of a string answer + $PG_eval_errors = ' '; + $correctVal = $correctAnswer; + } + + if ( ($PG_eval_errors && $corrAnswerIsString == 0) or ((not is_a_number($correctVal)) && $corrAnswerIsString == 0)) { + ##error message from eval or above + warn "Error in 'correct' answer: $PG_eval_errors<br> + The answer $correctAnswer evaluates to $correctVal, + which cannot be interpreted as a number. "; + + } + ######################################################################### + + #construct the answer evaluator + my $answer_evaluator = new AnswerEvaluator; + $answer_evaluator->{debug} = $num_params{debug}; + $answer_evaluator->ans_hash( + correct_ans => $correctVal, + type => "${mode}_number", + tolerance => $num_params{tolerance}, + tolType => $num_params{tolType}, + units => $correct_units, + original_correct_ans => $formattedCorrectAnswer, + rh_correct_units => \%correct_units, + answerIsString => $corrAnswerIsString, + ); + my ($in, $formattedSubmittedAnswer); + $answer_evaluator->install_pre_filter(sub {my $rh_ans = shift; + $rh_ans->{original_student_ans} = $rh_ans->{student_ans}; $rh_ans;} + ); + + + + if (defined($num_params{units}) && $num_params{units}) { + $answer_evaluator->install_pre_filter(\&check_units); + } + if (defined($num_params{strings}) && $num_params{strings}) { + $answer_evaluator->install_pre_filter(\&check_strings, %num_params); + } + + ## FIXME? - this pre filter was moved before check_units to allow + ## for latex preview of answers with no units. + ## seems to work but may have unintended side effects elsewhere. + + ## Actually it caused trouble with the check strings package so it has been moved back + # We'll try some other method -- perhaps add code to fix_answer for display + $answer_evaluator->install_pre_filter(\&check_syntax); + + $answer_evaluator->install_pre_filter(\&math_constants); + + if ($mode eq 'std') { + # do nothing + } elsif ($mode eq 'strict') { + $answer_evaluator->install_pre_filter(\&is_a_number); + } elsif ($mode eq 'arith') { + $answer_evaluator->install_pre_filter(\&is_an_arithmetic_expression); + } elsif ($mode eq 'frac') { + $answer_evaluator->install_pre_filter(\&is_a_fraction); + + } elsif ($mode eq 'phase_pi') { + $answer_evaluator->install_pre_filter(\&phase_pi); + + } else { + $PGanswerMessage = 'Tell your professor that there is an error in his or her answer mechanism. No mode was specified.'; + $formattedSubmittedAnswer = $in; + } + + if ($corrAnswerIsString == 0 ){ # avoiding running compare_numbers when correct answer is a string. + $answer_evaluator->install_evaluator(\&compare_numbers, %num_params); + } + + +############################################################################### +# We'll leave these next lines out for now, so that the evaluated versions of the student's and professor's +# can be displayed in the answer message. This may still cause a few anomolies when strings are used +# +############################################################################### + + $answer_evaluator->install_post_filter(\&fix_answers_for_display); + + $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; + return $rh_ans unless $rh_ans->catch_error('EVAL'); + $rh_ans->{student_ans} = $rh_ans->{original_student_ans}. ' '. $rh_ans->{error_message}; + $rh_ans->clear_error('EVAL'); } ); + $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; $rh_ans->clear_error('SYNTAX'); } ); + $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; $rh_ans->clear_error('UNITS'); } ); + $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; $rh_ans->clear_error('NUMBER'); } ); + $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; $rh_ans->clear_error('STRING'); } ); + $answer_evaluator; +} + + + +########################################################################## +########################################################################## +## Function answer evaluators + +=head2 Function Answer Evaluators + +Function answer evaluators take in a function, compare it numerically to a +correct function, and return a score. They can require an exactly equivalent +function, or one that is equal up to a constant. They can accept or reject an +answer based on specified tolerances for numerical deviation. + +Function Comparison Options + + correctEqn -- The correct equation, specified as a string. It may include + all basic arithmetic operations, as well as elementary + functions. Variable usage is described below. + + Variables -- The independent variable(s). When comparing the correct + equation to the student equation, each variable will be + replaced by a certain number of numerical values. If + the student equation agrees numerically with the correct + equation, they are considered equal. Note that all + comparison is numeric; it is possible (although highly + unlikely and never a practical concern) for two unequal + functions to yield the same numerical results. + + Limits -- The limits of evaluation for the independent variables. + Each variable is evaluated only in the half-open interval + [lower_limit, upper_limit). This is useful if the function + has a singularity or is not defined in a certain range. + For example, the function "sqrt(-1-x)" could be evaluated + in [-2,-1). + + Tolerance -- Tolerance in function comparisons works exactly as in + numerical comparisons; see the numerical comparison + documentation for a complete description. Note that the + tolerance does applies to the function as a whole, not + each point individually. + + Number of -- Specifies how many points to evaluate each variable at. This + Points is typically 3, but can be set higher if it is felt that + there is a strong possibility of "false positives." + + Maximum -- Sets the maximum size of the constant of integration. For + Constant of technical reasons concerning floating point arithmetic, if + Integration the additive constant, i.e., the constant of integration, is + greater (in absolute value) than maxConstantOfIntegration + AND is greater than maxConstantOfIntegration times the + correct value, WeBWorK will give an error message saying + that it can not handle such a large constant of integration. + This is to prevent e.g. cos(x) + 1E20 or even 1E20 as being + accepted as a correct antiderivatives of sin(x) since + floating point arithmetic cannot tell the difference + between cos(x) + 1E20, 1E20, and -cos(x) + 1E20. + +Technical note: if you examine the code for the function routines, you will see +that most subroutines are simply doing some basic error-checking and then +passing the parameters on to the low-level FUNCTION_CMP(). Because this routine +is set up to handle multivariable functions, with single-variable functions as +a special case, it is possible to pass multivariable parameters to single- +variable functions. This usage is strongly discouraged as unnecessarily +confusing. Avoid it. + +Default Values (As of 7/24/2000) (Option -- Variable Name -- Value) + + Variable -- $functVarDefault -- 'x' + Relative Tolerance -- $functRelPercentTolDefault -- .1 + Absolute Tolerance -- $functAbsTolDefault -- .001 + Lower Limit -- $functLLimitDefault -- .0000001 + Upper Limit -- $functULimitDefault -- 1 + Number of Points -- $functNumOfPoints -- 3 + Zero Level -- $functZeroLevelDefault -- 1E-14 + Zero Level Tolerance -- $functZeroLevelTolDefault -- 1E-12 + Maximum Constant -- $functMaxConstantOfIntegration -- 1E8 + of Integration + +=cut + + + +=head3 fun_cmp() + +Compares a function or a list of functions, using a named hash of options to set +parameters. This can make for more readable code than using the function_cmp() +style, but some people find one or the other easier to remember. + +ANS( fun_cmp( answer or answer_array_ref, options_hash ) ); + + 1. a string containing the correct function, or a reference to an + array of correct functions + 2. a hash containing the following items (all optional): + var -- either the number of variables or a reference to an + array of variable names (see below) + limits -- reference to an array of arrays of limits (see below), or: + mode -- 'std' (default) (function must match exactly), or: + 'antider' (function must match up to a constant) + relTol -- (default) a relative tolerance (as a percentage), or: + tol -- an absolute tolerance for error + numPoints -- the number of points to evaluate the function at + maxConstantOfIntegration -- maximum size of the constant of integration + zeroLevel -- if the correct answer is this close to zero, then + zeroLevelTol applies + zeroLevelTol -- absolute tolerance to allow when answer is close to zero + test_points -- a list of points to use in checking the function, or a list of lists when there is more than one variable. + params an array of "free" parameters which can be used to adapt + the correct answer to the submitted answer. (e.g. ['c'] for + a constant of in... [truncated message content] |