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