--- trunk/webwork/system/courseScripts/PGanswermacros.pl 2001/06/14 23:45:41 5 +++ trunk/webwork/system/courseScripts/PGanswermacros.pl 2001/06/20 19:04:08 35 @@ -1,4 +1,4 @@ -#!/usr/local/bin/perl -w +#!/usr/local/bin/webwork-perl # This file is PGanswermacros.pl # This includes the subroutines for the ANS macros, that @@ -7,6 +7,7 @@ # Copyright @ 1995-2000 University of Rochester # All Rights Reserved #################################################################### +#$Id$ =head1 NAME @@ -108,49 +109,76 @@ 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 -); -sub _PGanswermacros_init { - $BR = $main::BR; # convenient localizations. - $PAR = $main::PAR; - - # import defaults - # these are now imported from the %envir variable - $numRelPercentTolDefault = $main::numRelPercentTolDefault; - $numZeroLevelDefault = $main::numZeroLevelDefault; - $numZeroLevelTolDefault = $main::numZeroLevelTolDefault; - $numAbsTolDefault = $main::numAbsTolDefault; - $numFormatDefault = $main::numFormatDefault; - - $functRelPercentTolDefault = $main::functRelPercentTolDefault; - $functZeroLevelDefault = $main::functZeroLevelDefault; - $functZeroLevelTolDefault = $main::functZeroLevelTolDefault; - $functAbsTolDefault = $main::functAbsTolDefault; - $functNumOfPoints = $main::functNumOfPoints; - $functVarDefault = $main::functVarDefault; - $functLLimitDefault = $main::functLLimitDefault; - $functULimitDefault = $main::functULimitDefault; - $functMaxConstantOfIntegration = $main::functMaxConstantOfIntegration; +sub _PGanswermacros_export { + my @EXPORT = ( + '&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', + '&std_num_str_cmp', '&num_cmp', '&num_rel_cmp', '&NUM_CMP', + '&NUM_CMP_LIST', '&adaptive_function_cmp', '&function_cmp', + '&function_cmp_up_to_constant', '&function_cmp_abs', + '&function_cmp_up_to_constant_abs', '&multivar_function_cmp', + '&fun_cmp', '&FUNCTION_CMP', '&is_array', '&check_syntax', + '&std_num_filter', '&std_num_array_filter', '&function_from_string2', + '&is_zero_array', '&best_approx_parameters', + '&calculate_difference_vector', '&str_filters', '&remove_whitespace', + '&compress_whitespace', '&trim_whitespace', '&ignore_case', + '&ignore_order', '&std_str_cmp', '&std_str_cmp_list', '&std_cs_str_cmp', + '&std_cs_str_cmp_list', '&strict_str_cmp', '&strict_str_cmp_list', + '&unordered_str_cmp', '&unordered_str_cmp_list', + '&unordered_cs_str_cmp', '&unordered_cs_str_cmp_list', + '&ordered_str_cmp', '&ordered_str_cmp_list', '&ordered_cs_str_cmp', + '&ordered_cs_str_cmp_list', '&str_cmp', '&STR_CMP', '&checkbox_cmp', + '&radio_cmp', '&store_ans_at', '&DUMMY_ANSWER', '&escapeHTML', + '&anstext', '&ansradio', '&mail_answers_to', '&mail_answers_to2', + '&install_problem_grader', '&std_problem_grader', + '&std_problem_grader2', '&avg_problem_grader', '&get_var_array', + '&get_limits_array', '&check_option_list', '&function_invalid_params', + '&is_a_number', '&is_a_fraction', '&is_an_arithmetic_expression', + '&math_constants', '&clean_up_error_msg', '&prfmt', '&pretty_print', + '&set_default_options', '&assign_option_aliases', + ); + @EXPORT; +} + +my ($BR, $PAR,$numRelPercentTolDefault,$numZeroLevelDefault,$numZeroLevelTolDefault, + $numAbsTolDefault,$numFormatDefault,$functRelPercentTolDefault,$functZeroLevelDefault, + $functZeroLevelTolDefault,$functAbsTolDefault,$functNumOfPoints,$functVarDefault, + $functLLimitDefault, $functULimitDefault, $functMaxConstantOfIntegration, + ); + +sub _PGanswermacros_init { + + $BR = $main::BR; # convenient localizations. + $PAR = $main::PAR; + + # import defaults + # these are now imported from the %envir variable + $numRelPercentTolDefault = PG_restricted_eval(q{$main::numRelPercentTolDefault}); + $numZeroLevelDefault = PG_restricted_eval(q{$main::numZeroLevelDefault}); + $numZeroLevelTolDefault = PG_restricted_eval(q{$main::numZeroLevelTolDefault}); + $numAbsTolDefault = PG_restricted_eval(q{$main::numAbsTolDefault}); + $numFormatDefault = PG_restricted_eval(q{$main::numFormatDefault}); + + $functRelPercentTolDefault = PG_restricted_eval(q{$main::functRelPercentTolDefault}); + $functZeroLevelDefault = PG_restricted_eval(q{$main::functZeroLevelDefault}); + $functZeroLevelTolDefault = PG_restricted_eval(q{$main::functZeroLevelTolDefault}); + $functAbsTolDefault = PG_restricted_eval(q{$main::functAbsTolDefault}); + $functNumOfPoints = PG_restricted_eval(q{$main::functNumOfPoints}); + $functVarDefault = PG_restricted_eval(q{$main::functVarDefault}); + $functLLimitDefault = PG_restricted_eval(q{$main::functLLimitDefault}); + $functULimitDefault = PG_restricted_eval(q{$main::functULimitDefault}); + $functMaxConstantOfIntegration = PG_restricted_eval(q{$main::functMaxConstantOfIntegration}); + + + } -_PGanswermacros_init(); ########################################################################## ########################################################################## @@ -351,16 +379,26 @@ =cut sub std_num_cmp { # compare numbers allowing use of elementary functions - my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; + my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; - NUM_CMP( 'correctAnswer' => $correctAnswer, - 'tolerance' => $relPercentTol, - 'tolType' => 'relative', - 'format' => $format, - 'mode' => 'std', - 'zeroLevel' => $zeroLevel, - 'zeroLevelTol' => $zeroLevelTol - ); + my %options = ( 'tolerance' => $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 @@ -370,205 +408,337 @@ sub std_num_cmp_list { my ( $relPercentTol, $format, @answerList) = @_; - NUM_CMP_LIST( 'tolerance' => $relPercentTol, - 'tolType' => 'relative', - 'format' => $format, - 'mode' => 'std', - 'zeroLevel' => $numZeroLevelDefault, - 'zeroLevelTol' => $numZeroLevelTolDefault, - 'answerList' => \@answerList - ); + my %options = ( 'tolerance' => $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 +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' => $correctAnswer, - 'tolerance' => $absTol, - 'tolType' => 'absolute', - 'format' => $format, - 'mode' => 'std', - 'zeroLevel' => 0, - 'zeroLevelTol' => 0 - ); + num_cmp([$correctAnswer], %options); } ## See std_num_cmp_list for usage + sub std_num_cmp_abs_list { my ( $absTol, $format, @answerList ) = @_; - NUM_CMP_LIST( 'tolerance' => $absTol, - 'tolType' => 'absolute', - 'format' => $format, - 'mode' => 'std', - 'zeroLevel' => 0, - 'zeroLevelTol' => 0, - 'answerList' => \@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 ) = @_; - NUM_CMP( 'correctAnswer' => $correctAnswer, - 'tolerance' => $relPercentTol, - 'tolType' => 'relative', - 'format' => $format, - 'mode' => 'frac', - 'zeroLevel' => $zeroLevel, - 'zeroLevelTol' => $zeroLevelTol - ); + my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; + + my %options = ( 'tolerance' => $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 ) = @_; - - NUM_CMP_LIST( 'tolerance' => $relPercentTol, - 'tolType' => 'relative', - 'format' => $format, - 'mode' => 'frac', - 'zeroLevel' => $numZeroLevelDefault, - 'zeroLevelTol' => $numZeroLevelTolDefault, - 'answerList' => \@answerList - ); + my ( $relPercentTol, $format, @answerList ) = @_; + + my %options = ( 'tolerance' => $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 ) = @_; - NUM_CMP( 'correctAnswer' => $correctAnswer, - 'tolerance' => $absTol, - 'tolType' => 'absolute', - 'format' => $format, - 'mode' => 'frac', - 'zeroLevel' => 0, - 'zeroLevelTol' => 0 - ); -} +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 ) = @_; - - NUM_CMP_LIST( 'tolerance' => $absTol, - 'tolType' => 'absolute', - 'format' => $format, - 'mode' => 'frac', - 'zeroLevel' => 0, - 'zeroLevelTol' => 0, - 'answerList' => \@answerList - ); + 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 ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; + + my %options = ( 'tolerance' => $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' => $correctAnswer, - 'tolerance' => $relPercentTol, - 'tolType' => 'relative', - 'format' => $format, - 'mode' => 'arith', - 'zeroLevel' => $zeroLevel, - 'zeroLevelTol' => $zeroLevelTol - ); + num_cmp([$correctAnswer], %options); } ## See std_num_cmp_list for usage sub arith_num_cmp_list { - my ( $relPercentTol, $format, @answerList ) = @_; + my ( $relPercentTol, $format, @answerList ) = @_; - NUM_CMP_LIST( 'tolerance' => $relPercentTol, - 'tolType' => 'relative', - 'format' => $format, - 'mode' => 'arith', - 'zeroLevel' => $numZeroLevelDefault, - 'zeroLevelTol' => $numZeroLevelTolDefault, - 'answerList' => \@answerList - ); + my %options = ( 'tolerance' => $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 ) = @_; +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); - NUM_CMP( 'correctAnswer' => $correctAnswer, - 'tolerance' => $absTol, - 'tolType' => 'absolute', - 'format' => $format, - 'mode' => 'arith', - 'zeroLevel' => 0, - 'zeroLevelTol' => 0 - ); + } ## See std_num_cmp_list for usage sub arith_num_cmp_abs_list { - my ( $absTol, $format, @answerList ) = @_; - - NUM_CMP_LIST( 'tolerance' => $absTol, - 'tolType' => 'absolute', - 'format' => $format, - 'mode' => 'arith', - 'zeroLevel' => 0, - 'zeroLevelTol' => 0, - 'answerList' => \@answerList - ); + 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 ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; + + my %options = ( 'tolerance' => $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); - NUM_CMP( 'correctAnswer' => $correctAnswer, - 'tolerance' => $relPercentTol, - 'tolType' => 'relative', - 'format' => $format, - 'mode' => 'strict', - 'zeroLevel' => $zeroLevel, - 'zeroLevelTol' => $zeroLevelTol - ); } ## See std_num_cmp_list for usage sub strict_num_cmp_list { # compare numbers my ( $relPercentTol, $format, @answerList ) = @_; + + my %options = ( 'tolerance' => $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); + } - NUM_CMP_LIST( 'tolerance' => $relPercentTol, - 'tolType' => 'relative', - 'format' => $format, - 'mode' => 'strict', - 'zeroLevel' => $numZeroLevelDefault, - 'zeroLevelTol' => $numZeroLevelTolDefault, - 'answerList' => \@answerList - ); -} sub strict_num_cmp_abs { # only allow numbers as submitted answer with absolute tolerance - my ( $correctAnswer, $absTol, $format ) = @_; - NUM_CMP( 'correctAnswer' => $correctAnswer, - 'tolerance' => $absTol, - 'tolType' => 'absolute', - 'format' => $format, - 'mode' => 'strict', - 'zeroLevel' => 0, - 'zeroLevelTol' => 0 - ); + 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 ( $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); + + - NUM_CMP_LIST( 'tolerance' => $absTol, - 'tolType' => 'absolute', - 'format' => $format, - 'mode' => 'strict', - 'zeroLevel' => 0, - 'zeroLevelTol' => 0, - 'answerList' => \@answerList - ); } @@ -583,153 +753,150 @@ ## 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 -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 - # handle the defaults - $options{'mode'} = 'std' unless defined( $options{'mode'} ); - $options{'format'} = $numFormatDefault unless defined( $options{'format'} ); - $options{'zeroLevel'} = $numZeroLevelDefault unless defined( $options{'zeroLevel'} ); - $options{'zeroLevelTol'} = $numZeroLevelTolDefault unless defined( $options{'zeroLevelTol'} ); - - # both spellings are maintained for backward compatibility - # relTol is preferred - if( defined $options{'reltol'} ) { - $options{'relTol'} = $options{'reltol'}; - delete $options{'reltol'}; - } - - my ($tol, $tolerance_mode); - if ( defined $options{'tol'} ) { - $tol = $options{'tol'}; - $tolerance_mode = 'absolute'; - } - elsif( defined $options{'relTol'} ) { - $tol = $options{'relTol'}; - $tolerance_mode = 'relative'; - } - else { #the default is a relative tolerance - $tol = $numRelPercentTolDefault; - $tolerance_mode = 'relative'; + +sub check_strings { + my ($rh_ans, %options) = @_; + + # if the student's answer is a number, simply return the answer hash (unchanged). + + if ( $rh_ans->{student_ans} =~ m/[\d+\-*\/^(){}\[\]]|^\s*e\s*$|^\s*pi\s*$/) { + if ( $rh_ans->{answerIsString} == 1) { + $rh_ans->throw_error('STRING','Incorrect Answer'); # student's answer is a number + } + return $rh_ans; + } + # the student's answer is recognized as a string + my $ans = $rh_ans->{student_ans}; + +# OVERVIEW of remindar of function: +# if answer is correct, return correct. (adjust score to 1) +# if answer is incorect: +# 1) determine if the answer is sensible. if it is, return incorrect. +# 2) if the answer is not sensible (and incorrect), then return an error message indicating so. +# no matter what: throw a 'STRING' error to skip numerical evaluations. (error flag skips remainder of pre_filters and evaluators) +# last: 'STRING' post_filter will clear the error (avoiding pink screen.) + + my $sensibleAnswer = 0; + $ans = str_filters( $ans, 'compress_whitespace' ); # remove trailing, leading, and double spaces. + my ($ans_eval) = str_cmp($rh_ans->{correct_ans}); + my $temp_ans_hash = &$ans_eval($ans); + $rh_ans->{test} = $temp_ans_hash; + if ($temp_ans_hash->{score} ==1 ) { # students answer matches the correct answer. + $rh_ans->{score} = 1; + $sensibleAnswer = 1; + } else { # students answer does not match the correct answer. + ## find out if string makes sense + my $legalString = ''; + my @legalStrings = @{$options{strings}}; + foreach $legalString (@legalStrings) { + if ( uc($ans) eq uc($legalString) ) { + $sensibleAnswer = 1; + last; + } + } + $sensibleAnswer = 1 unless $ans =~ /\S/; ## empty answers are sensible + $rh_ans->throw_error('EVAL', "$BR Your answer is not a recognized answer") unless ($sensibleAnswer); + # $temp_ans_hash -> setKeys( 'ans_message' => 'Your answer is not a recognized answer' ) unless ($sensibleAnswer); + # $temp_ans_hash -> setKeys( 'student_ans' => uc($ans) ); + } + $rh_ans->{student_ans} = $ans; + if ($sensibleAnswer) { + $rh_ans->throw_error('STRING', "The student's answer $rh_ans->{student_ans} is interpreted as a string."); } + # warn ("\$rh_ans->{answerIsString} = $rh_ans->{answerIsString}"); + + $rh_ans; + +} - # Prepare the correct answer - $correct_answer = str_filters( $correct_answer, 'trim_whitespace' ); + +sub check_units { + my ($rh_ans, %options) = @_; + + my %correct_units = %{$rh_ans-> {rh_correct_units}}; + + my $ans = $rh_ans->{student_ans}; + # $ans = '' unless defined ($ans); + $ans = str_filters ($ans, 'trim_whitespace'); + my $original_student_ans = $ans; + + $rh_ans->{original_student_ans} = $original_student_ans; + # it surprises me that the match below works since the first .* is greedy. - my ($correct_num_answer, $correct_units) = $correct_answer =~ /^(.*)\s+([^\s]*)$/; + my ($num_answer, $units) = $ans =~ /^(.*)\s+([^\s]*)$/; + + unless ( defined($num_answer) && $units ) { + # there is an error reading the input + if ( $ans =~ /\S/ ) { # the answer is not blank + $rh_ans -> setKeys( 'ans_message' => "The answer \"$ans\" could not be interpreted " . + "as a number or an arithmetic expression followed by a unit specification. " . + "Your answer must contain units." ); + $rh_ans->throw_error('UNITS', "The answer \"$ans\" could not be interpreted " . + "as a number or an arithmetic expression followed by a unit specification. " . + "Your answer must contain units." ); + } - my %correct_units = Units::evaluate_units($correct_units); - if ( defined( $correct_units{'ERROR'} ) ) { - die "ERROR: The answer \"$correct_answer\" in the problem definition cannot be parsed:\n" . - "$correct_units{'ERROR'}\n"; + return $rh_ans; } - my $ans_evaluator = sub { + # we have been able to parse the answer into a numerical part and a unit part - my $ans = shift; - $ans = '' unless defined($ans); - my $original_student_ans = $ans; - - $ans = str_filters( $ans, 'trim_whitespace' ); - - my $ans_hash = new AnswerHash( - 'score' => 0, - 'correct_ans' => spf($correct_num_answer,$options{'format'}) . " $correct_units", - 'student_ans' => $ans, - 'ans_message' => '', - 'type' => 'num_cmp_with_units', - 'preview_text_string' => '', - 'original_student_ans' => $original_student_ans - ); + # $num_answer = $1; #$1 and $2 from the regular expression above + # $units = $2; - # it surprises me that the match below works since the first .* is greedy. - my ($num_answer, $units) = $ans =~ /^(.*)\s+([^\s]*)$/; + my %units = Units::evaluate_units($units); + if ( defined( $units{'ERROR'} ) ) { + # handle error condition + $units{'ERROR'} = clean_up_error_msg($units{'ERROR'}); + $rh_ans -> setKeys( 'ans_message' => "$units{'ERROR'}" ); + return $rh_ans; + } - unless ( defined($num_answer) && $units ) { - # there is an error reading the input - if ( $ans =~ /\S/ ) { # the answer is not blank - $ans_hash -> setKeys( 'ans_message' => "The answer \"$ans\" could not be interpreted " . - "as a number or an arithmetic expression followed by a unit specification. " . - "Your answer must contain units." ); - } + my $units_match = 1; + my $fund_unit; + foreach $fund_unit (keys %correct_units) { + next if $fund_unit eq 'factor'; + $units_match = 0 unless $correct_units{$fund_unit} == $units{$fund_unit}; + } + + if ( $units_match ) { + # units are ok. Evaluate the numerical part of the answer + $rh_ans->{'tolerance'} = $rh_ans->{'tolerance'}* $correct_units{'factor'}/$units{'factor'} if + $rh_ans->{'tolType'} eq 'absolute'; # the tolerance is in the units specified by the instructor. + $rh_ans->{correct_ans} = prfmt($rh_ans->{correct_ans}*$correct_units{'factor'}/$units{'factor'}); + $rh_ans->{student_ans} = $num_answer; - return $ans_hash; + } else { + $rh_ans -> setKeys( ans_message => 'There is an error in the units for this answer.' ); + $rh_ans -> throw_error ( 'UNITS', 'There is an error in the units for this answer.' ); } + + return $rh_ans; + } - # we have been able to parse the answer into a numerical part and a unit part - - $num_answer = $1; #$1 and $2 from the regular expression above - $units = $2; - - my %units = Units::evaluate_units($units); - if ( defined( $units{'ERROR'} ) ) { - # handle error condition - $units{'ERROR'} = clean_up_error_msg($units{'ERROR'}); - - $ans_hash -> setKeys( 'ans_message' => "$units{'ERROR'}" ); - return $ans_hash; - } +# 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 - my $units_match = 1; - my $fund_unit; - foreach $fund_unit (keys %correct_units) { - next if $fund_unit eq 'factor'; - $units_match = 0 unless $correct_units{$fund_unit} == $units{$fund_unit}; - } - - if ( $units_match ) { - - # units are ok. Evaluate the numerical part of the answer - $tol = $tol * $correct_units{'factor'}/$units{'factor'} if - $tolerance_mode eq 'absolute'; # the tolerance is in the units specified by the instructor. - - my $numerical_answer_evaluator = NUM_CMP( 'correctAnswer' => $correct_num_answer*$correct_units{'factor'}/$units{'factor'}, - 'tolerance' => $tol, - 'tolType' => $tolerance_mode, - 'format' => $options{'format'}, - 'mode' => $options{'mode'}, - 'zeroLevel' => $options{'zeroLevel'}, - 'zeroLevelTol' => $options{'zeroLevelTol'} ); - - # because num_answer may contain an arithmetic expression rather than - # a number we can't multiply it by the $units{'factor'} - # instead we divide the correct answer by this amount; - # this is also why the numerical_answer_evaluator is not defined outside this subroutine. - - $ans_hash = &$numerical_answer_evaluator($num_answer); - - # now we need to doctor the correct answer in order to add units - # to it and correct for the division we did before - $ans_hash -> {correct_ans} = - prfmt( ( $ans_hash->{'correct_ans'} )*$units{'factor'}/$correct_units{'factor'}, - $options{'format'} ) . " $correct_units"; - # we also need to doctor the submitted answer to get it back in its original format. - - # we don't add the units on if there is an error message from numerical_answer_evaluator - if ( ( $ans_hash -> {ans_message} ) =~ /^\s*$/ ) { - $ans_hash -> {student_ans} = $ans_hash -> {student_ans} . " $units"; - $ans_hash -> setKeys( original_student_ans => $ans ); - } - else { - # error message from numerical_answer_evaluator doesn't have units tacked on - $ans_hash -> setKeys( original_student_ans => $ans ); - } - } - else { - $ans_hash -> setKeys( ans_message => 'There is an error in the units for this answer.' ); - } + # Prepare the correct answer + $correct_answer = str_filters( $correct_answer, 'trim_whitespace' ); - return $ans_hash; - }; + # it surprises me that the match below works since the first .* is greedy. + my ($correct_num_answer, $correct_units) = $correct_answer =~ /^(.*)\s+([^\s]*)$/; - $ans_evaluator; + $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. @@ -760,111 +927,121 @@ =cut -sub std_num_str_cmp { +sub std_num_str_cmp { my ( $correctAnswer, $ra_legalStrings, $relpercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; - - $ra_legalStrings = [''] unless defined $ra_legalStrings; - my @legalStrings = @{$ra_legalStrings}; - - my $ans_evaluator = sub { - - my $ans = shift; - my $ans_hash; - my $corrAnswerIsString = 0; -# my $studAnswerIsString = 0; ## uses new incorrect logic - my $studAnswerIsString = 1; - - my $legalString = ''; - foreach $legalString (@legalStrings) { - if ( uc($correctAnswer) eq uc($legalString) ) { - $corrAnswerIsString = 1; - last; - } - } ## at this point $corrAnswerIsString = 0 iff correct answer is numeric - - # Neither of these is perfect; the first is more general, but - # has problems with certain special strings like "ee", while the - # second doesn't support arithmetic expressions. - # -# if( $ans !~ m/^\s*([\+\-\*\/\^\(\)\[\]\{\}\s\d\.Ee]*|e|pi)\s*$/ ) { -# $studAnswerIsString = 1; + # 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 old_std_num_str_cmp { +# my ( $correctAnswer, $ra_legalStrings, $relpercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; +# +# $ra_legalStrings = [''] unless defined $ra_legalStrings; +# my @legalStrings = @{$ra_legalStrings}; +# +# my $ans_evaluator = sub { +# +# my $ans = shift; +# my $ans_hash; +# my $corrAnswerIsString = 0; +## my $studAnswerIsString = 0; ## uses new incorrect logic +# my $studAnswerIsString = 1; +# +# my $legalString = ''; +# foreach $legalString (@legalStrings) { +# if ( uc($correctAnswer) eq uc($legalString) ) { +# $corrAnswerIsString = 1; +# last; +# } +# } ## at this point $corrAnswerIsString = 0 iff correct answer is numeric +# +# # Neither of these is perfect; the first is more general, but +# # has problems with certain special strings like "ee", while the +# # second doesn't support arithmetic expressions. +# # +## if( $ans !~ m/^\s*([\+\-\*\/\^\(\)\[\]\{\}\s\d\.Ee]*|e|pi)\s*$/ ) { +## $studAnswerIsString = 1; +## } +# #if( $ans !~ m/^\s*([\d+\-*\/^()]|e|pi)\s*$/ ) { +# # $studAnswerIsString = 1; +# #} +# +# ## Both the above new versions are incorrect. We replace this by the original logic namely that +# ## an answer that contain any of the symbols +# ## a digit(0-9), +, -, *, /, ^, (, ), {, }, [, ] +# ## or an answer that consists of "pi" or "e" alone +# ## will be considered an arithmetic expression rather than a string answer. +# +# if ($ans =~ m/[\d+\-*\/^(){}\[\]]|^\s*e\s*$|^\s*pi\s*$/) {$studAnswerIsString = 0;} +# +# +# ## at this point $studAnswerIsString = 0 iff correct answer is numeric +# +# if( $studAnswerIsString ) { +# $ans = str_filters( $ans, 'compress_whitespace' ) # } - #if( $ans !~ m/^\s*([\d+\-*\/^()]|e|pi)\s*$/ ) { - # $studAnswerIsString = 1; - #} - - ## Both the above new versions are incorrect. We replace this by the original logic namely that - ## an answer that contain any of the symbols - ## a digit(0-9), +, -, *, /, ^, (, ), {, }, [, ] - ## or an answer that consists of "pi" or "e" alone - ## will be considered an arithmetic expression rather than a string answer. - - if ($ans =~ m/[\d+\-*\/^(){}\[\]]|^\s*e\s*$|^\s*pi\s*$/) {$studAnswerIsString = 0;} - - - ## at this point $studAnswerIsString = 0 iff correct answer is numeric - - if( $studAnswerIsString ) { - $ans = str_filters( $ans, 'compress_whitespace' ) - } - - if ( ($corrAnswerIsString == 1) and ($studAnswerIsString == 1) ) { - my $string_answer_evaluator = std_str_cmp( $correctAnswer ); - $ans_hash = &$string_answer_evaluator( $ans ); - - if( ($ans_hash -> {score}) != 1 ) { ## find out if string makes sense - my $sensibleAnswer = 0; - foreach $legalString (@legalStrings) { - if ( uc($ans) eq uc($legalString) ) { - $sensibleAnswer = 1; - last; - } - } - $sensibleAnswer = 1 unless $ans =~ /\S/; ## empty answers are sensible - - $ans_hash -> setKeys( 'ans_message' => 'Your answer is not a recognized answer' ) - unless ($sensibleAnswer); - $ans_hash -> setKeys( 'student_ans' => uc($ans) ); - } - } - elsif ( ($corrAnswerIsString == 0) and ($studAnswerIsString == 0) ) { - my $numeric_answer_evaluator = std_num_cmp($correctAnswer,$relpercentTol,$format,$zeroLevel,$zeroLevelTol); - $ans_hash = &$numeric_answer_evaluator($ans); - } - elsif ( ($corrAnswerIsString == 1) and ($studAnswerIsString == 0) ) { - my $numeric_answer_evaluator = std_num_cmp(1); - $ans_hash = &$numeric_answer_evaluator($ans); - $ans_hash -> setKeys( 'score' => 0, - 'correct_ans' => $correctAnswer - ); - } - elsif ( ($corrAnswerIsString == 0) and ($studAnswerIsString == 1) ) { - my $string_answer_evaluator = std_str_cmp('bad'); - $ans_hash = &$string_answer_evaluator($ans); - - $ans_hash -> setKeys( 'score' => 0, - 'correct_ans' => $correctAnswer - ); - - ## find out if string makes sense - my $sensibleAnswer = 0; - foreach $legalString (@legalStrings) { - if ( uc($ans) eq uc($legalString) ) { - $sensibleAnswer = 1; - last; - } - } - $sensibleAnswer = 1 unless $ans =~ /\S/; ## empty answers are sensible - - $ans_hash -> setKeys( 'ans_message' => "Your answer is not a recognized answer" ) - unless $sensibleAnswer; - } - - return $ans_hash; - }; - - return $ans_evaluator; -} +# +# +# +# +# if ( ($corrAnswerIsString == 1) and ($studAnswerIsString == 1) ) { +# my $string_answer_evaluator = std_str_cmp( $correctAnswer ); +# $ans_hash = &$string_answer_evaluator( $ans ); +# +# if( ($ans_hash -> {score}) != 1 ) { ## find out if string makes sense +# my $sensibleAnswer = 0; +# foreach $legalString (@legalStrings) { +# if ( uc($ans) eq uc($legalString) ) { +# $sensibleAnswer = 1; +# last; +# } +# } +# $sensibleAnswer = 1 unless $ans =~ /\S/; ## empty answers are sensible +# +# $ans_hash -> setKeys( 'ans_message' => 'Your answer is not a recognized answer' ) +# unless ($sensibleAnswer); +# $ans_hash -> setKeys( 'student_ans' => uc($ans) ); +# } +# } +# elsif ( ($corrAnswerIsString == 0) and ($studAnswerIsString == 0) ) { +# my $numeric_answer_evaluator = std_num_cmp($correctAnswer,$relpercentTol,$format,$zeroLevel,$zeroLevelTol); +# $ans_hash = &$numeric_answer_evaluator($ans); +# } +# elsif ( ($corrAnswerIsString == 1) and ($studAnswerIsString == 0) ) { +# my $numeric_answer_evaluator = std_num_cmp(1); +# $ans_hash = &$numeric_answer_evaluator($ans); +# $ans_hash -> setKeys( 'score' => 0, +# 'correct_ans' => $correctAnswer +# ); +# } +# elsif ( ($corrAnswerIsString == 0) and ($studAnswerIsString == 1) ) { +# my $string_answer_evaluator = std_str_cmp('bad'); +# $ans_hash = &$string_answer_evaluator($ans); +# +# $ans_hash -> setKeys( 'score' => 0, +# 'correct_ans' => $correctAnswer +# ); +# +# ## find out if string makes sense +# my $sensibleAnswer = 0; +# foreach $legalString (@legalStrings) { +# if ( uc($ans) eq uc($legalString) ) { +# $sensibleAnswer = 1; +# last; +# } +# } +# $sensibleAnswer = 1 unless $ans =~ /\S/; ## empty answers are sensible +# +# $ans_hash -> setKeys( 'ans_message' => "Your answer is not a recognized answer" ) +# unless $sensibleAnswer; +# } +# +# return $ans_hash; +# }; +# +# return $ans_evaluator; +#} =head3 num_cmp() @@ -909,23 +1086,29 @@ sub num_cmp { my $correctAnswer = shift @_; 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, - - 'reltol' => undef, #alternate spelling - 'unit' => undef #alternate spelling - ); - my %in_options; + '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 + my @output_list; - my %out_options; - + 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 @@ -936,7 +1119,7 @@ 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)."; - my( $relPercentTol, $format, $zeroLevel, $zeroLevelTol) = @opt; + %out_options = ( 'relTol' => $relPercentTol, 'format' => $format, @@ -945,56 +1128,72 @@ 'mode' => 'std' ); } - else { - # handle options +# else { +# # handle options +# +# +# @opt = ( 'relTol' => $relPercentTol, +# 'format' => $format, +# 'zeroLevel' => $numZeroLevelDefault, +# 'zeroLevelTol' => $numZeroLevelTolDefault, +# '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', + ); - check_option_list( @opt ); - %in_options = @opt; - # both spellings maintained for compatibility - # relTol is preferred - if( defined( $in_options{'reltol'} ) ) { - $in_options{'relTol'} = $in_options{'reltol'}; - delete $in_options{'reltol'}; - } - # both spellings maintained for compatibility - # units is preferred - if( defined( $in_options{'unit'} ) ) { - $in_options{'units'} = $in_options{'unit'}; - delete $in_options{'unit'}; - } - # can't use both units and strings - if( defined( $in_options{'units'} ) && defined( $in_options{'strings'} ) ) { - warn "Can't use both 'units' and 'strings' in the same problem " . - "(check your parameters to num_cmp() )"; - } + set_default_options( \%out_options, + 'tolType' => (defined($out_options{tol}) ) ? 'absolute' : 'relative', + 'tolerance' => (defined($out_options{tol}) ) ? $numAbsTolDefault : $numRelPercentTolDefault, + 'mode' => 'std', + 'format' => $numFormatDefault, + 'tol' => $numAbsTolDefault, + 'relTol' => $numRelPercentTolDefault, + 'units' => undef, + 'strings' => undef, + 'zeroLevel' => $numZeroLevelDefault, + 'zeroLevelTol' => $numZeroLevelTolDefault, + 'debug' => 0, + + ); - #%out_options = %known_options; - foreach my $opt_name (keys %in_options) { - if( exists( $known_options{$opt_name} ) ) { - $out_options{$opt_name} = $in_options{$opt_name}; - } - else { - die "Option $opt_name is not defined for num_cmp. Answer is $correctAnswer; " . - "Default options are:
", pretty_print(\%known_options); - } - } - } - # set tolerance flags -- note that the order of testing means that - # relative tolerance is the default - my ($tolType, $tol); - if ( defined( $out_options{'tol'} ) ) { - $tolType = 'absolute'; - $tol = $out_options{'tol'}; + + + # 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() )"; + } - else { - $tolType = 'relative'; - $tol = $out_options{'relTol'}; + + + # my ($tolType, $tol); + if ($out_options{tolType} eq 'absolute') { + # $tolType = 'absolute'; + # $out_options{tolType} = 'absolute'; + # $tol = $out_options{'tol'}; + $out_options{'tolerance'}=$out_options{'tol'}; + delete($out_options{'relTol'}) if exists( $out_options{'relTol'} ); + } else { + # $tolType = 'relative'; + # $out_options{tolType} = 'relative'; + # $tol = $out_options{'relTol'}; + # $out_options{'tolType'} = $out_options{'relative'}; + $out_options{'tolerance'}=$out_options{'relTol'}; + # delete($out_options{'tol'}) if exists( $out_options{'tol'} ); } # thread over lists @@ -1006,46 +1205,67 @@ 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, numerical_compare_with_units($ans, %out_options) ); - } - elsif( defined( $out_options{'strings'} ) ) { - if( defined $out_options{'tol'} ) { - warn "You are using 'tol' (for absolute tolerance) with a num/str " . - "compare, which currently only uses relative tolerance. The default " . - "tolerance will be used."; - } - - push( @output_list, std_num_str_cmp( $ans, $out_options{'strings'}, - $out_options{'relTol'}, - $out_options{'format'}, - $out_options{'zeroLevel'}, - $out_options{'zeroLevelTol'} - ) - ); - } else { + 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'} ) ) { + #if( defined $out_options{'tol'} ) { + # warn "You are using 'tol' (for absolute tolerance) with a num/str " . + # "compare, which currently only uses relative tolerance. The default " . + # "tolerance will be used."; + #} + + 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' => $tol, - 'tolType' => $tolType, - 'format' => $out_options{'format'}, - 'mode' => $out_options{'mode'}, - 'zeroLevel' => $out_options{'zeroLevel'}, - 'zeroLevelTol' => $out_options{'zeroLevelTol'}, - ), - ); - } + 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 @output_list; -} + } #legacy code for compatability purposes sub num_rel_cmp { # compare numbers - std_num_cmp( @_ ); + std_num_cmp( @_ ); } ## LOW-LEVEL ROUTINE -- NOT NORMALLY FOR END USERS -- USE WITH CAUTION @@ -1059,243 +1279,198 @@ ## determines allowable formats for the input ## zeroLevel -- if the correct answer is this close to zero, then zeroLevelTol applies ## zeroLevelTol -- absolute tolerance to allow when answer is close to zero -sub NUM_CMP { # low level numeric compare - my %num_params = @_; - - my $correctAnswer = $num_params{'correctAnswer'}; - my $tol = $num_params{'tolerance'}; - my $tolType = $num_params{'tolType'}; - my $format = $num_params{'format'}; - my $mode = $num_params{'mode'}; - my $zeroLevel = $num_params{'zeroLevel'}; - my $zeroLevelTol = $num_params{'zeroLevelTol'}; - - if( $tolType eq 'relative' ) { - $tol = $numRelPercentTolDefault unless defined $tol; - $tol *= .01; +sub compare_numbers { + my ($rh_ans, %options) = @_; + my ($inVal,$PG_eval_errors,$PG_full_error_report) = PG_answer_eval($rh_ans->{student_ans}); + if ($PG_eval_errors) { + $rh_ans->throw_error('EVAL','There is a syntax error in your answer'); + $rh_ans->{ans_message} = clean_up_error_msg($PG_eval_errors); + + } else { - $tol = $numAbsTolDefault unless defined $tol; + $rh_ans->{student_ans} = prfmt($inVal,$options{format}); } - $format = $numFormatDefault unless defined $format; - $mode = 'std' unless defined $mode; - $zeroLevel = $numZeroLevelDefault unless defined $zeroLevel; - $zeroLevelTol = $numZeroLevelTolDefault unless defined $zeroLevelTol; - - my $formattedCorrectAnswer = prfmt( $correctAnswer, $format ); - - my $answer_evaluator = sub { - my $in = shift @_; - $in = '' unless defined $in; - my $score = 0; - my $original_student_answer = $in; - my $parser = new AlgParserWithImplicitExpand; - my $ret = $parser -> parse($in); - my $preview_text_string = ''; - my $preview_latex_string = ''; - - if ( ref($ret) ) { ## parsed successfully - $parser -> tostring(); - $parser -> normalize(); - $in = $parser -> tostring(); - $preview_text_string = $in; - $preview_latex_string = $parser -> tolatex(); - - } - else { ## error in parsing - my $ans_hash = new AnswerHash( - 'score' => $score, - 'correct_ans' => $formattedCorrectAnswer, - 'student_ans' => "error: $parser->{htmlerror}", - 'ans_message' => $parser -> {error_msg}, - 'type' => "${mode}_number", - 'preview_text_string' => $preview_text_string, - 'preview_latex_string' => $preview_latex_string, - 'original_student_ans' => $original_student_answer - ); - - return $ans_hash; - } - - my $PGanswerMessage = ''; - - my ($inVal,$correctVal,$PG_eval_errors,$PG_full_error_report); - - $inVal = ''; - $correctAnswer = math_constants($correctAnswer); - my $formattedSubmittedAnswer = ''; + + my $permitted_error; + + if ($rh_ans->{tolType} eq 'absolute') { + $permitted_error = $rh_ans->{tolerance}; + + } + elsif ( abs($rh_ans->{correct_ans}) <= $options{zeroLevel}) { + $permitted_error = $options{zeroLevelTol}; ## want $tol to be non zero + } + else { + $permitted_error = abs($rh_ans->{tolerance}*$rh_ans->{correct_ans}); + } + + my $is_a_number = is_a_number($inVal); + $rh_ans->{score} = 1 if ( ($is_a_number) and + (abs( $inVal - $rh_ans->{correct_ans} ) <= $permitted_error) ); + if (not $is_a_number) { + $rh_ans->throw_error('EVAL','Your answer does not evaluate to a number'); + } + + $rh_ans; +} - #special variable $@ holds the last error from a Perl eval statement - $@=''; +sub 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}); + } - if ($correctAnswer =~ /\S/) { - ($correctVal, $PG_eval_errors,$PG_full_error_report) = PG_answer_eval($correctAnswer); - } - else { - $PG_eval_errors = ' '; - } + my $correctAnswer = $num_params{'correctAnswer'}; + my $format = $num_params{'format'}; + my $mode = $num_params{'mode'}; + + # my $tol = $num_params{'tolerance'}; + # my $tolType = $num_params{'tolType'}; + # my $zeroLevel = $num_params{'zeroLevel'}; + # my $zeroLevelTol = $num_params{'zeroLevelTol'}; + + if( $num_params{tolType} eq 'relative' ) { + $num_params{'tolerance'} = .01*$num_params{'tolerance'}; + } - if ( $PG_eval_errors or not is_a_number($correctVal) ) { ##error message from eval or above - $formattedSubmittedAnswer = $PG_eval_errors; - $formattedSubmittedAnswer = clean_up_error_msg($formattedSubmittedAnswer); - $PGanswerMessage = 'Tell your professor that there is an error in this problem'; - my $ans_hash = new AnswerHash( - 'score' => $score, - 'correct_ans' => $formattedCorrectAnswer, - 'student_ans' => $formattedSubmittedAnswer, - 'ans_message' => $PGanswerMessage, - 'type' => 'number', - 'preview_text_string' => $preview_text_string, - 'preview_latex_string' => $preview_latex_string, - 'original_student_ans' => $original_student_answer - ); + #$format = $numFormatDefault unless defined $format; + #$mode = 'std' unless defined $mode; + #$zeroLevel = $numZeroLevelDefault unless defined $zeroLevel; + #$zeroLevelTol = $numZeroLevelTolDefault unless defined $zeroLevelTol; + + my $formattedCorrectAnswer; + my $correct_units; + my $correct_num_answer; + my %correct_units; + my $corrAnswerIsString = 0; + - return $ans_hash; + 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"); } - - $in = &math_constants($in); - - MODE_CASE: { ## bare block for "case" statement - if ($mode eq 'std') { - last MODE_CASE; - } - elsif ($mode eq 'strict') { - unless (is_a_number($in)) { - $PGanswerMessage = 'You must enter a number, e.g. -6, 5.3, or 6.12E-3'; - $formattedSubmittedAnswer = 'Incorrect number format'; - } - else { - last MODE_CASE; - } - } - elsif ($mode eq 'arith') { - unless (is_an_arithmetic_expression($in)) { - $PGanswerMessage = 'You must enter an arithmetic expression, e.g. -6 or (2.3*4+5/3)^2'; - $formattedSubmittedAnswer = 'Not an arithmetic expression'; - } - else { - last MODE_CASE; - } - } - elsif ($mode eq 'frac') { - unless (is_a_fraction($in)) { - $PGanswerMessage = 'You must enter a number or fraction , e.g. -6 or 7/13'; - $formattedSubmittedAnswer = 'Not a number or fraction'; - } - else { - last MODE_CASE; - } - } - else { - $PGanswerMessage = 'Tell your professor that there is an error in his or her answer mechanism. No mode was specified.'; - $formattedSubmittedAnswer = $in; + # $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 + - my $ans_hash = new AnswerHash( - score => $score, - correct_ans => $formattedCorrectAnswer, - student_ans => $formattedSubmittedAnswer, - ans_message => $PGanswerMessage, - type => "${mode}_number", - preview_text_string => $preview_text_string, - preview_latex_string => $preview_latex_string, - original_student_ans => $original_student_answer - ); + } else { + $correct_num_answer = $correctAnswer; + $formattedCorrectAnswer = prfmt( $correctAnswer, $num_params{'format'} ); + } - return $ans_hash; - } # end of MODE_CASES bare block + $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 { + $PG_eval_errors = ' '; + } - $@ = ''; - if ($in =~ /\S/) { + 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
+ The answer $correctAnswer evaluates to $correctVal, + which cannot be interpreted as a number. "; + + } + ######################################################################### - ($inVal,$PG_eval_errors,$PG_full_error_report) = PG_answer_eval($in); - } - else { - $PG_eval_errors = ' '; - } + #construct the answer evaluator + my $answer_evaluator = new AnswerEvaluator; + $answer_evaluator->{debug} = $num_params{debug}; + $answer_evaluator->ans_hash( correct_ans => $correct_num_answer, + 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); + } - if ($PG_eval_errors) { ##error message from eval or above - $formattedSubmittedAnswer = $PG_eval_errors; - $formattedSubmittedAnswer =clean_up_error_msg($formattedSubmittedAnswer); - $PGanswerMessage = 'There is a syntax error in your answer'; - $PGanswerMessage = '' if $PG_eval_errors eq ' '; - my $ans_hash = new AnswerHash( - 'score' => $score, - 'correct_ans' => $formattedCorrectAnswer, - 'student_ans' => $formattedSubmittedAnswer, - 'ans_message' => $PGanswerMessage, - 'type' => "${mode}_number", - 'preview_text_string' => $preview_text_string, - 'preview_latex_string' => $preview_latex_string, - 'original_student_ans' => $original_student_answer - ); + + $answer_evaluator->install_pre_filter(\&check_syntax); + + $answer_evaluator->install_pre_filter(\&math_constants); - return $ans_hash; - } - else { - $formattedSubmittedAnswer = prfmt($inVal,$format); - } - my $permitted_error; - if (defined($tolType) && $tolType eq 'absolute') { - $permitted_error = $tol; - } - elsif ( abs($correctVal) <= $zeroLevel) { - $permitted_error = $zeroLevelTol; ## want $tol to be non zero - } - else { - $permitted_error = abs($tol*$correctVal); - } - my $is_a_number = is_a_number($inVal); - $score = 1 if ( ($is_a_number) and - (abs( $inVal - $correctVal ) <= $permitted_error) ); - if ($PG_eval_errors) { - $PGanswerMessage = 'There is a syntax error in your answer'; - } - elsif (not $is_a_number) { - $PGanswerMessage = 'Your answer does not evaluate to a number'; - } + 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); - my $ans_hash = new AnswerHash( - 'score' => $score, - 'correct_ans' => $formattedCorrectAnswer, - 'student_ans' => $formattedSubmittedAnswer, - 'ans_message' => $PGanswerMessage, - 'type' => "${mode}_number", - 'preview_text_string' => $preview_text_string, - 'preview_latex_string' => $preview_latex_string, - 'original_student_ans' => $original_student_answer - ); + } 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); + } - return $ans_hash; - }; + $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; + + $rh_ans->{student_ans} = $rh_ans->{original_student_ans}; + $rh_ans->{correct_ans} = $rh_ans->{original_correct_ans}; + $rh_ans;} + ); - return $answer_evaluator; + $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; } -## LOW-LEVEL ROUTINE -- NOT NORMALLY FOR END USERS -- USE WITH CAUTION -sub NUM_CMP_LIST { # low level numeric list compare - my %num_params = @_; - - my @outputList; - my $ans; - - while ( @{$num_params{'answerList'}} ) { - $ans = shift @{$num_params{'answerList'}}; - push( @outputList, NUM_CMP( 'correctAnswer' => $ans, - 'tolerance' => $num_params{'tolerance'}, - 'tolType' => $num_params{'tolType'}, - 'format' => $num_params{'format'}, - 'mode' => $num_params{'mode'}, - 'zeroLevel' => $num_params{'zeroLevel'}, - 'zeroLevelTol' => $num_params{'zeroLevelTol'} - ) - ); - } - return @outputList; -} @@ -1489,7 +1664,7 @@ 'reltol' => $main::functRelPercentTolDefault, 'numPoints' => $main::functNumOfPoints, 'zeroLevel' => $main::functZeroLevelDefault, - 'zeroLevelTol' => $main::functZeroLevelTolDefault, + 'zeroLevelTol' => $main::functZeroLevelTolDefault, 'debug' => 0, ); @@ -1501,19 +1676,19 @@ my $zeroLevel = $options{'zeroLevel'}; my $zeroLevelTol = $options{'zeroLevelTol'}; - FUNCTION_CMP( 'correctEqn' => $correctEqn, - 'var' => $var_ref, - 'limits' => $limit_ref, - 'tolerance' => $relPercentTol, - 'tolType' => 'relative', - 'numPoints' => $numPoints, - 'mode' => 'std', - 'maxConstantOfIntegration' => 10**100, - 'zeroLevel' => $zeroLevel, - 'zeroLevelTol' => $zeroLevelTol, - 'scale_norm' => 1, - 'params' => $ra_params, - 'debug' => $options{debug} , + FUNCTION_CMP( 'correctEqn' => $correctEqn, + 'var' => $var_ref, + 'limits' => $limit_ref, + 'tolerance' => $relPercentTol, + 'tolType' => 'relative', + 'numPoints' => $numPoints, + 'mode' => 'std', + 'maxConstantOfIntegration' => 10**100, + 'zeroLevel' => $zeroLevel, + 'zeroLevelTol' => $zeroLevelTol, + 'scale_norm' => 1, + 'params' => $ra_params, + 'debug' => $options{debug} , ); } @@ -1525,16 +1700,16 @@ function_invalid_params( $correctEqn ); } else { - FUNCTION_CMP( 'correctEqn' => $correctEqn, - 'var' => $var, - 'limits' => [$llimit, $ulimit], - 'tolerance' => $relPercentTol, - 'tolType' => 'relative', - 'numPoints' => $numPoints, - 'mode' => 'std', - 'maxConstantOfIntegration' => 0, - 'zeroLevel' => $zeroLevel, - 'zeroLevelTol' => $zeroLevelTol + FUNCTION_CMP( 'correctEqn' => $correctEqn, + 'var' => $var, + 'limits' => [$llimit, $ulimit], + 'tolerance' => $relPercentTol, + 'tolType' => 'relative', + 'numPoints' => $numPoints, + 'mode' => 'std', + 'maxConstantOfIntegration' => 0, + 'zeroLevel' => $zeroLevel, + 'zeroLevelTol' => $zeroLevelTol ); } } @@ -1546,16 +1721,16 @@ function_invalid_params( $correctEqn ); } else { - FUNCTION_CMP( 'correctEqn' => $correctEqn, - 'var' => $var, - 'limits' => [$llimit, $ulimit], - 'tolerance' => $relPercentTol, - 'tolType' => 'relative', - 'numPoints' => $numPoints, - 'mode' => 'antider', - 'maxConstantOfIntegration' => $maxConstantOfIntegration, - 'zeroLevel' => $zeroLevel, - 'zeroLevelTol' => $zeroLevelTol + FUNCTION_CMP( 'correctEqn' => $correctEqn, + 'var' => $var, + 'limits' => [$llimit, $ulimit], + 'tolerance' => $relPercentTol, + 'tolType' => 'relative', + 'numPoints' => $numPoints, + 'mode' => 'antider', + 'maxConstantOfIntegration' => $maxConstantOfIntegration, + 'zeroLevel' => $zeroLevel, + 'zeroLevelTol' => $zeroLevelTol ); } } @@ -1592,16 +1767,16 @@ } else { - FUNCTION_CMP( 'correctEqn' => $correctEqn, - 'var' => $var, - 'limits' => [$llimit, $ulimit], - 'tolerance' => $absTol, - 'tolType' => 'absolute', - 'numPoints' => $numPoints, - 'mode' => 'antider', - 'maxConstantOfIntegration' => $maxConstantOfIntegration, - 'zeroLevel' => 0, - 'zeroLevelTol' => 0 + FUNCTION_CMP( 'correctEqn' => $correctEqn, + 'var' => $var, + 'limits' => [$llimit, $ulimit], + 'tolerance' => $absTol, + 'tolType' => 'absolute', + 'numPoints' => $numPoints, + 'mode' => 'antider', + 'maxConstantOfIntegration' => $maxConstantOfIntegration, + 'zeroLevel' => 0, + 'zeroLevelTol' => 0 ); } } @@ -1737,25 +1912,25 @@ my %opt = @_; assign_option_aliases( \%opt, - 'vars' => 'var', # set the standard option 'var' to the one specified as vars - 'domain' => 'limits', # set the standard option 'limits' to the one specified as domain - 'reltol' => 'relTol', + 'vars' => 'var', # set the standard option 'var' to the one specified as vars + 'domain' => 'limits', # set the standard option 'limits' to the one specified as domain + 'reltol' => 'relTol', 'param' => 'params', ); set_default_options( \%opt, - 'var' => $functVarDefault, - 'params' => [], - 'limits' => [[$functLLimitDefault, $functULimitDefault]], - 'mode' => 'std', - 'tolType' => (defined($opt{tol}) ) ? 'absolute' : 'relative', - 'tol' => .01, # default mode should be relative, to obtain this tol must not be defined - 'relTol' => $functRelPercentTolDefault, - 'numPoints' => $functNumOfPoints, + 'var' => $functVarDefault, + 'params' => [], + 'limits' => [[$functLLimitDefault, $functULimitDefault]], + 'mode' => 'std', + 'tolType' => (defined($opt{tol}) ) ? 'absolute' : 'relative', + 'tol' => .01, # default mode should be relative, to obtain this tol must not be defined + 'relTol' => $functRelPercentTolDefault, + 'numPoints' => $functNumOfPoints, 'maxConstantOfIntegration' => $functMaxConstantOfIntegration, - 'zeroLevel' => $functZeroLevelDefault, - 'zeroLevelTol' => $functZeroLevelTolDefault, - 'debug' => 0, + 'zeroLevel' => $functZeroLevelDefault, + 'zeroLevelTol' => $functZeroLevelTolDefault, + 'debug' => 0, ); @@ -1780,8 +1955,6 @@ delete($out_options{'tol'}) if exists( $out_options{'tol'} ); } - - my @output_list = (); # thread over lists my @ans_list = (); @@ -1793,23 +1966,21 @@ push( @ans_list, $correctAnswer ); } - - # produce answer evaluators foreach my $ans (@ans_list) { push(@output_list, - FUNCTION_CMP( 'correctEqn' => $ans, - 'var' => $out_options{'var'}, - 'limits' => $out_options{'limits'}, - 'tolerance' => $tol, - 'tolType' => $tolType, - 'numPoints' => $out_options{'numPoints'}, - 'mode' => $out_options{'mode'}, - 'maxConstantOfIntegration' => $out_options{'maxConstantOfIntegration'}, - 'zeroLevel' => $out_options{'zeroLevel'}, - 'zeroLevelTol' => $out_options{'zeroLevelTol'}, - 'params' => $out_options{'params'}, - 'debug' => $out_options{'debug'}, + FUNCTION_CMP( 'correctEqn' => $ans, + 'var' => $out_options{'var'}, + 'limits' => $out_options{'limits'}, + 'tolerance' => $tol, + 'tolType' => $tolType, + 'numPoints' => $out_options{'numPoints'}, + 'mode' => $out_options{'mode'}, + 'maxConstantOfIntegration' => $out_options{'maxConstantOfIntegration'}, + 'zeroLevel' => $out_options{'zeroLevel'}, + 'zeroLevelTol' => $out_options{'zeroLevelTol'}, + 'params' => $out_options{'params'}, + 'debug' => $out_options{'debug'}, ), ); } @@ -1841,13 +2012,13 @@ my %func_params = @_; my $correctEqn = $func_params{'correctEqn'}; - my $var = $func_params{'var'}; + my $var = $func_params{'var'}; my $ra_limits = $func_params{'limits'}; - my $tol = $func_params{'tolerance'}; - my $tolType = $func_params{'tolType'}; + my $tol = $func_params{'tolerance'}; + my $tolType = $func_params{'tolType'}; my $numPoints = $func_params{'numPoints'}; - my $mode = $func_params{'mode'}; - my $maxConstantOfIntegration = $func_params{'maxConstantOfIntegration'}; + my $mode = $func_params{'mode'}; + my $maxConstantOfIntegration = $func_params{'maxConstantOfIntegration'}; my $zeroLevel = $func_params{'zeroLevel'}; my $zeroLevelTol = $func_params{'zeroLevelTol'}; @@ -1888,15 +2059,16 @@ $zeroLevel = $functZeroLevelDefault unless defined $zeroLevel; $zeroLevelTol = $functZeroLevelTolDefault unless defined $zeroLevelTol; - $func_params{'var'} = $var; - $func_params{'limits'} = \@limits; - $func_params{'tolerance'}= $tol; - $func_params{'tolType'} = $tolType; - $func_params{'numPoints'}= $numPoints; - $func_params{'mode'} = $mode; - $func_params{'maxConstantOfIntegration'} = $maxConstantOfIntegration; - $func_params{'zeroLevel'} = $zeroLevel; - $func_params{'zeroLevelTol'} = $zeroLevelTol; + $func_params{'var'} = $var; + $func_params{'limits'} = \@limits; + $func_params{'tolerance'} = $tol; + $func_params{'tolType'} = $tolType; + $func_params{'numPoints'} = $numPoints; + $func_params{'mode'} = $mode; + $func_params{'maxConstantOfIntegration'} = $maxConstantOfIntegration; + $func_params{'zeroLevel'} = $zeroLevel; + $func_params{'zeroLevelTol'} = $zeroLevelTol; + ######################################################## # End of cleanup of calling parameters ######################################################## @@ -1905,7 +2077,7 @@ my $originalCorrEqn = $correctEqn; #prepare the correct answer and check it's syntax - my $rh_correct_ans = new AnswerHash; + my $rh_correct_ans = new AnswerHash; $rh_correct_ans->input($correctEqn); $rh_correct_ans = check_syntax($rh_correct_ans); warn $rh_correct_ans->{error_message} if $rh_correct_ans->{error_flag}; @@ -1918,7 +2090,7 @@ #create the evaluation points my $random_for_answers = new PGrandom($main::PG_original_problemSeed); - my $NUMBER_OF_STEPS_IN_RANDOM = 1000; # determines the granularity of the random_for_answers number generator + my $NUMBER_OF_STEPS_IN_RANDOM = 1000; # determines the granularity of the random_for_answers number generator my (@evaluation_points); for( my $count = 0; $count < @PARAMS+1+$numPoints; $count++ ) { my (@vars,$iteration_limit); @@ -1937,7 +2109,7 @@ my $evaluation_points = Matrix->new_from_array_ref(\@evaluation_points); #my $COEFFS = determine_param_coeffs($correct_eqn_sub,$evaluation_points[0],$numOfParameters); - #warn "coeff", join(" | ", @{$COEFFS}); + #warn "coeff", join(" | ", @{$COEFFS}); #construct the answer evaluator my $answer_evaluator = new AnswerEvaluator; @@ -2100,7 +2272,7 @@ } if ($PGanswerMessage) { $rh_ans->input( "( " . join(", ", @out ) . " )" ); - $rh_ans->throw_error('SYTNAX', 'There is a syntax error in your answer.'); + $rh_ans->throw_error('SYNTAX', 'There is a syntax error in your answer.'); } else { $rh_ans->input( [@out] ); } @@ -2442,7 +2614,7 @@ if (defined($options{tolType}) and $options{tolType} eq 'relative' ) { #relative tolerance #warn "diff = $diff"; - $diff = abs(( $inVal - ($correctVal-$tol_val ) )/$tol_val -1 ) if abs($tol_val) > $options{zeroLevel}; + $diff = ( $inVal - ($correctVal-$tol_val ) )/abs($tol_val) -1 if abs($tol_val) > $options{zeroLevel}; #$diff = ( $inVal - ($correctVal-$tol_val- $tol_val ) )/abs($tol_val) if abs($tol_val) > $options{zeroLevel}; #warn "diff = $diff, ", abs( &$rf_correct_fun(@inputs) ) , "-- $correctVal"; } @@ -2457,9 +2629,9 @@ push(@tol_values,$tol_val); } $rh_ans ->{ra_differences} = \@differences; - $rh_ans ->{ra_student_values} = \@student_values; # values from student function - $rh_ans ->{ra_adjusted_instructor_values} = \@correct_values; #values - $rh_ans->{ra_instructor_values}=\@tol_values; + $rh_ans ->{ra_student_values} = \@student_values; + $rh_ans ->{ra_adjusted_student_values} = \@correct_values; + $rh_ans->{ra_tol_values}=\@tol_values; $rh_ans->throw_error('EVAL', $errors) if defined($errors); $rh_ans; } @@ -2525,10 +2697,10 @@ my $stringToFilter = shift @_; my @filters_to_use = @_; my %known_filters = ( 'remove_whitespace' => undef, - 'compress_whitespace' => undef, - 'trim_whitespace' => undef, - 'ignore_case' => undef, - 'ignore_order' => undef + 'compress_whitespace' => undef, + 'trim_whitespace' => undef, + 'ignore_case' => undef, + 'ignore_order' => undef ); #test for unknown filters @@ -2660,8 +2832,8 @@ my @filters = ( 'compress_whitespace', 'ignore_case' ); my $type = 'std_str_cmp'; STR_CMP( 'correctAnswer' => $correctAnswer, - 'filters' => \@filters, - 'type' => $type + 'filters' => \@filters, + 'type' => $type ); } @@ -2874,38 +3046,30 @@ ## filters -- reference to an array containing the filters to be applied ## type -- a string containing the type of answer evaluator in use ## OUT: a reference to an answer evaluator subroutine + sub STR_CMP { my %str_params = @_; - $str_params{'correctAnswer'} = str_filters( $str_params{'correctAnswer'}, @{$str_params{'filters'}} ); - my $answer_evaluator = sub { my $in = shift @_; $in = '' unless defined $in; my $original_student_ans = $in; - $in = str_filters( $in, @{$str_params{'filters'}} ); - my $correctQ = ( $in eq $str_params{'correctAnswer'} ) ? 1: 0; - my $ans_hash = new AnswerHash( - 'score' => $correctQ, + my $ans_hash = new AnswerHash( 'score' => $correctQ, 'correct_ans' => $str_params{'correctAnswer'}, 'student_ans' => $in, 'ans_message' => '', - 'type' => $str_params{'type'}, - 'preview_text_string' => $in, - 'preview_latex_string' => $in, - 'original_student_ans' => $original_student_ans + 'type' => $str_params{'type'}, + 'preview_text_string' => $in, + 'preview_latex_string' => $in, + 'original_student_ans' => $original_student_ans ); - return $ans_hash; }; - return $answer_evaluator; } - - ########################################################################## ########################################################################## ## Miscellaneous answer evaluators @@ -3193,7 +3357,7 @@ # I don't like to put in this bit of code. # It makes it hard to construct error free problem graders # I would prefer to know that the problem score was numeric. - unless (defined($problem_state{recorded_score}) and $problem_state{recorded_score} =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/ ) { + unless ($problem_state{recorded_score} =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/ ) { $problem_state{recorded_score} = 0; # This gets rid of non-numeric scores } # @@ -3426,6 +3590,7 @@ ## an array of array references -- ([llim,ulim], [llim,ulim]) ## an array of limits -- (llim,ulim) ## OUT: an array of array references -- ([llim,ulim], [llim,ulim]) or ([llim,ulim]) + sub get_limits_array { my $in = shift @_; my @out; @@ -3481,18 +3646,21 @@ return $error_response; } -# outputs a hash to the screen -# sub display_options { -# my %options = @_; -# my $out_string = ""; -# foreach my $key (keys %options) { -# $out_string .= " $key => $options{$key},
"; -# } -# return $out_string; -# } + +######################################################################### +# Filters for answer evaluators +######################################################################### + sub is_a_number { - my ($num) = @_; + my ($num,%options) = @_; + my $process_ans_hash = ( ref( $num ) eq 'AnswerHash' ) ? 1 : 0 ; + my ($rh_ans); + if ($process_ans_hash) { + $rh_ans = $num; + $num = $rh_ans->{student_ans}; + } + my $is_a_number = 0; return $is_a_number unless defined($num); $num =~ s/^\s*//; ## remove initial spaces @@ -3502,43 +3670,109 @@ if ($num =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/){ $is_a_number = 1; } - - return $is_a_number; + + if ($process_ans_hash) { + if ($is_a_number == 1 ) { + $rh_ans->{student_ans}=$num; + return $rh_ans; + } else { + $rh_ans->{student_ans} = "Incorrect number format: You must enter a number, e.g. -6, 5.3, or 6.12E-3"; + $rh_ans->throw_error('NUMBER', 'You must enter a number, e.g. -6, 5.3, or 6.12E-3'); + return $rh_ans; + } + } else { + return $is_a_number; + } } sub is_a_fraction { - - ## does not test for validity, just for allowed characters - ## note that an integer will qualify as a fraction - my ($exp) = @_; - my $is_a_fraction = 0; - return $is_a_fraction unless defined($exp); - if ($exp =~ /^\s*\-?\s*[\/\d\.Ee\s]*$/) { + my ($num,%options) = @_; + my $process_ans_hash = ( ref( $num ) eq 'AnswerHash' ) ? 1 : 0 ; + my ($rh_ans); + if ($process_ans_hash) { + $rh_ans = $num; + $num = $rh_ans->{student_ans}; + } + + my $is_a_fraction = 0; + return $is_a_fraction unless defined($num); + $num =~ s/^\s*//; ## remove initial spaces + $num =~ s/\s*$//; ## remove trailing spaces + + if ($num =~ /^\s*\-?\s*[\/\d\.Ee\s]*$/) { $is_a_fraction = 1; } - - return $is_a_fraction; + + if ($process_ans_hash) { + if ($is_a_fraction == 1 ) { + $rh_ans->{student_ans}=$num; + return $rh_ans; + } else { + $rh_ans->{student_ans} = "Not a number of fraction: You must enter a number or fraction, e.g. -6 or 7/13"; + $rh_ans->throw_error('NUMBER', 'You must enter a number, e.g. -6, 5.3, or 6.12E-3'); + return $rh_ans; + } + + } else { + return $is_a_fraction; + } } -sub is_an_arithmetic_expression { - ## does not test for validity, just for allowed characters - my ($exp) = @_; - my $is_an_arithmetic_expression = 0; - if ($exp =~ /^[+\-*\/\^\(\)\[\]\{\}\s\d\.Ee]*$/) { + +sub is_an_arithmetic_expression { + my ($num,%options) = @_; + my $process_ans_hash = ( ref( $num ) eq 'AnswerHash' ) ? 1 : 0 ; + my ($rh_ans); + if ($process_ans_hash) { + $rh_ans = $num; + $num = $rh_ans->{student_ans}; + } + + my $is_an_arithmetic_expression = 0; + return $is_an_arithmetic_expression unless defined($num); + $num =~ s/^\s*//; ## remove initial spaces + $num =~ s/\s*$//; ## remove trailing spaces + + if ($num =~ /^[+\-*\/\^\(\)\[\]\{\}\s\d\.Ee]*$/) { $is_an_arithmetic_expression = 1; } - - return $is_an_arithmetic_expression; + + if ($process_ans_hash) { + if ($is_an_arithmetic_expression == 1 ) { + $rh_ans->{student_ans}=$num; + return $rh_ans; + } else { + + $rh_ans->{student_ans} = "Not an arithmetic expression: You must enter an arithmetic expression, e.g. -6 or (2.3*4+5/3)^2"; + $rh_ans->throw_error('NUMBER', 'You must enter an arithmetic expression, e.g. -6 or (2.3*4+5/3)^2'); + return $rh_ans; + } + + } else { + return $is_an_arithmetic_expression; + } } #replaces pi, e, and ^ with their Perl equivalents sub math_constants { - my($in) = @_; + my($in,%options) = @_; + my $rh_ans; + my $process_ans_hash = ( ref( $in ) eq 'AnswerHash' ) ? 1 : 0 ; + if ($process_ans_hash) { + $rh_ans = $in; + $in = $rh_ans->{student_ans}; + } + $in =~s/\bpi\b/(4*atan2(1,1))/ge; $in =~s/\be\b/(exp(1))/ge; $in =~s/\^/**/g; - - return $in; + + if ($process_ans_hash) { + $rh_ans->{student_ans}=$in; + return $rh_ans; + } else { + return $in; + } } sub clean_up_error_msg { @@ -3576,7 +3810,6 @@ } else { $out = $number; - $out =~ s/e/E/g; # only use capital E's for exponents. Little e is for 2.71828... } return $out; @@ -3628,8 +3861,7 @@ } foreach my $key (keys %default_options) { if ( not defined($rh_options->{$key} ) and defined( $default_options{$key} ) ) { - $rh_options->{$key} = $default_options{$key}; # using 'defined' instead of 'exists' allows - # tol => undef to allow the tol option, but doesn't define + $rh_options->{$key} = $default_options{$key}; #this allows tol => undef to allow the tol option, but doesn't define # this key unless tol is explicitly defined. } }