Parent Directory
|
Revision Log
Default behavior of number_list_cmp set to be backward compatible (no hints of any kind or partial credit). These features can now be activated by optional arguments. Also improvements on parser-based interval_cmp.
1 loadMacros('Parser.pl'); 2 3 # This is extraAnswerEvaluators.pl 4 5 # Most of the work is done in special namespaces 6 # At the end, we provide one global function, the interval answer evaluator 7 8 # To do: 9 # Convert these to AnswerEvaluator objects 10 # Better error checking/messages 11 # Simplify checks so we don't make so much use of num_cmp and cplx_cmp. 12 # When they change, these functions may have to change. 13 14 =head1 NAME 15 16 extraAnswerEvaluators.pl -- located in the courseScripts directory 17 18 =head1 SYNPOSIS 19 20 Answer Evaluators for intervals, lists of numbers, lists of points, 21 and equations. 22 23 interval_cmp() -- checks answers which are unions of intervals. 24 It can also be used for checking an ordered pair or 25 list of ordered pairs. 26 27 number_list_cmp() -- checks a comma separated list of numbers. By use of 28 optional arguments, you can request that order be 29 important, that complex numbers be allowed, and 30 specify extra arguments to be sent to num_cmp (or 31 cplx_cmp) for checking individual entries. 32 33 equation_cmp() -- provides a limited facility for checking equations. 34 It makes no pretense of checking to see if the real locus 35 of the student's equation matches the real locus of the 36 instructor's equation. The student's equation must be 37 of the same general type as the instructors to get credit. 38 39 40 =cut 41 42 =head1 DESCRIPTION 43 44 This file adds subroutines which create "answer evaluators" for checking student 45 answers of various "exotic" types. 46 47 =cut 48 49 50 { 51 package Intervals; 52 53 # We accept any of the following as infinity (case insensitive) 54 @infinitywords = ("i", "inf", "infty", "infinity"); 55 $infinityre = join '|', @infinitywords; 56 $infinityre = "^([-+m]?)($infinityre)\$"; 57 58 sub new { 59 my $class = shift; 60 my $base_string = shift; 61 my $self = {}; 62 $self->{'original'} = $base_string; 63 return bless $self, $class; 64 } 65 66 # Not object oriented. It just returns the structure 67 sub new_interval { # must call with 4 arguments 68 my($l,$r,$lec, $rec) = @_; 69 return [[$l,$r],[$lec,$rec]]; 70 } 71 72 # error routine copied from AlgParser 73 sub error { 74 my($self, @args) = @_; 75 # we cheat to use error from algparser 76 my($ap) = new AlgParser(); 77 $ap->inittokenizer($self->{'original'}); 78 $ap->error(@args); 79 $self->{htmlerror} = $ap->{htmlerror}; 80 $self->{error_msg} = $ap->{error_msg}; 81 } 82 83 # Determine if num_cmp detected a parsing/syntax type error 84 85 sub has_errors { 86 my($ah) = shift; 87 88 if($ah->{'student_ans'} =~ /error/) { 89 return 1; 90 } 91 my($am) = $ah->{'ans_message'}; 92 if($am =~ /error/) { 93 return 2; 94 } 95 if($am =~ /must enter/) { 96 return 3; 97 } 98 if($am =~ /does not evaluate/) { 99 return 4; 100 } 101 return 0; 102 } 103 104 105 ## Parse a string into a bunch of intervals 106 ## We do it by hand to avoid problems of nested parentheses 107 ## This also builds a normalized version of the string, one with values, 108 ## and a latex version. 109 ## 110 ## Return value simply says whether or not this was successful 111 sub parse_intervals { 112 my($self) = shift; 113 my(%opts) = @_; 114 my($str) = $self->{'original'}; 115 my(@ans_list) = (); 116 delete($opts{'sloppy'}); 117 delete($opts{'ordered'}); 118 my($unions) = 1; 119 if (defined($opts{'unions'}) and ($opts{'unions'} eq 'no')) { 120 $unions = 0; 121 } 122 # Sometimes we use this for lists of points 123 delete($opts{'unions'}); 124 my($b1str,$b2str) = (', ', ', '); 125 if($unions) { 126 ($b1str,$b2str) = (' U ', ' \cup '); 127 } 128 129 my($tmp_ae) = main::num_cmp(1, %opts); 130 $self->{'normalized'} = ''; 131 $self->{'value'} = ''; 132 $self->{'latex'} = ''; 133 $self->{'htmlerror'} = ''; 134 $self->{'error_msg'} = ''; 135 my($pmi) = 0; 136 my(@cur) = ("",""); 137 my($lb,$rb) = (0,0); 138 my($level,$spot,$hold,$char,$lr) = (0,0,0,"a",0); 139 140 while ($spot < length($str)) { 141 $char = substr($str,$spot,1); 142 if ($char=~ /[\[(,)\]]/) { # Its a special character 143 if ($char eq ",") { 144 if ($level == 1) { # Level 1 comma 145 if ($lr == 1) { 146 $self->error("Not a valid interval; too many commas.",[$spot]); 147 return 0; 148 } else { 149 $lr=1; 150 $cur[0] = substr($str,$hold, $spot-$hold); 151 if($pmi = pminf($cur[0])) { 152 if($pmi<0) { 153 $self->{'value'} .= '-'; 154 $self->{'normalized'} .= '-'; 155 $self->{'latex'} .= '-'; 156 } 157 $self->{'value'} .= 'Infinity, '; 158 $self->{'normalized'} .= 'Infinity, '; 159 $self->{'latex'} .= '\infty, '; 160 } else { 161 my($tmp_ah) = $tmp_ae->evaluate($cur[0]); 162 if(has_errors($tmp_ah)) { 163 $self->error("I could not parse your input correctly",[$hold, $spot]); 164 return 0; 165 } 166 $self->{'normalized'} .= $tmp_ah->{'preview_text_string'}.", "; 167 $self->{'value'} .= $tmp_ah->{'student_ans'}.", "; 168 $self->{'latex'} .= $tmp_ah->{'preview_latex_string'}.", "; 169 } 170 $hold = $spot+1; 171 } 172 } 173 } # end of comma 174 elsif ($char eq "[" or $char eq "(") { #opening 175 if ($level==0) { 176 $lr = 0; 177 if(scalar(@ans_list)) { # this is not the first interval 178 $self->{'normalized'} .= $b1str; 179 $self->{'value'} .= $b1str; 180 $self->{'latex'} .= $b2str; 181 } 182 $self->{'normalized'} .= "$char"; 183 $self->{'value'} .= "$char"; 184 $self->{'latex'} .= "$char"; 185 $hold=$spot+1; 186 if ($char eq "[") { 187 $lb = 1; 188 } else { 189 $lb = 0; 190 } 191 } 192 $level++; 193 } # end of open paren 194 else { # must be closed paren 195 if ($level == 0) { 196 $self->error("Not a valid interval; extra $char when I expected a new interval to open.",[$spot]); 197 return 0; 198 } elsif ($level == 1) { 199 if ($lr != 1) { 200 $self->error("Not a valid interval; closing an interval without a right component.", [$spot]); 201 return 0; 202 } else { 203 $cur[1] = substr($str, $hold, $spot-$hold); 204 if($pmi = pminf($cur[1])) { 205 if($pmi<0) { 206 $self->{'value'} .= '-'; 207 $self->{'normalized'} .= '-'; 208 $self->{'latex'} .= '-'; 209 } 210 $self->{'value'} .= "Infinity$char"; 211 $self->{'normalized'} .= "Infinity$char"; 212 $self->{'latex'} .= '\infty'."$char"; 213 } else { 214 my($tmp_ah) = $tmp_ae->evaluate($cur[1]); 215 if(has_errors($tmp_ah)) { 216 $self->error("I could not parse your input correctly",[$hold, $spot]); 217 return 0; 218 } 219 $self->{'normalized'} .= $tmp_ah->{'preview_text_string'}."$char"; 220 $self->{'value'} .= $tmp_ah->{'student_ans'}."$char"; 221 $self->{'latex'} .= $tmp_ah->{'preview_latex_string'}."$char"; 222 } 223 if ($char eq "]") { 224 $rb = 1; 225 } else { 226 $rb = 0; 227 } 228 push @ans_list, new_interval($cur[0], $cur[1], $lb, $rb); 229 } 230 } 231 $level--; 232 } 233 } 234 $spot++; 235 } 236 237 if($level>0) { 238 $self->error("Your expression ended in the middle of an interval.", 239 [$hold, $spot]); 240 return 0; 241 } 242 $self->{'parsed'} = \@ans_list; 243 return 1; 244 } 245 246 # Is the argument an exceptable +/- infinity 247 # Its sort of multiplies the input by 0 using 0 * oo = 1, 0 * (-oo) = -1. 248 sub pminf { 249 my($val) = shift; 250 $val = "\L$val"; # lowercase 251 $val =~ s/ //g; # remove space 252 if ($val =~ /$infinityre/) { 253 if (($1 eq '-') or ($1 eq 'm')) { 254 return -1; 255 } else { 256 return 1; 257 } 258 } 259 return 0; 260 } 261 262 # inputs are now of type Intervals, and then options 263 264 sub cmp_intervals { 265 my($in1) = shift; 266 my($in2) = shift; 267 my(%opts) = @_; 268 my($strict_ordering) = 0; 269 if (defined($opts{'ordering'}) && $opts{'ordering'} eq 'strict') { 270 $strict_ordering = 1; 271 } 272 delete($opts{'ordering'}); 273 274 my($issloppy) = 0; 275 if (defined($opts{'sloppy'}) && $opts{'sloppy'} eq 'yes') { 276 $issloppy = 1; 277 } 278 delete($opts{'sloppy'}); 279 280 delete($opts{'unions'}); 281 282 283 my(@i1) = @{$in1->{'parsed'}}; 284 my(@i2) = @{$in2->{'parsed'}}; 285 286 my($j,$pm10,$pm11,$pm20,$pm21); 287 # Same number of intervals? 288 if (scalar(@i1) != scalar(@i2)) { 289 return 0; 290 } 291 for ($j=0; $j<scalar(@i1);$j++) { 292 my($lbound) = 0; 293 my($ubound) = scalar(@i1)-1; 294 my($lookformatch) = 1; 295 if ($strict_ordering) { 296 $lbound = $j; 297 $ubound = $j; 298 } 299 for ($k=$lbound; $lookformatch && $k<=$ubound; $k++) { 300 # Do they all have correct inclusions ()[]? 301 if (! $issloppy and ($i1[$j]->[1][0] != $i2[$k]->[1][0] or 302 $i1[$j]->[1][1] != $i2[$k]->[1][1])) { 303 next; 304 } 305 $pm10 = pminf($i1[$j]->[0][0]); 306 $pm11 = pminf($i1[$j]->[0][1]); 307 $pm20 = pminf($i2[$k]->[0][0]); 308 $pm21 = pminf($i2[$k]->[0][1]); 309 if ($pm10 != $pm20) { 310 next; 311 } 312 if ($pm11 != $pm21) { 313 next; 314 } 315 # Now we deal with only numbers, no infinities 316 if ($pm10 == 0) { 317 # $opts{'correctAnswer'} = $i1[$j]->[0][0]; 318 my $ae = main::num_cmp($i1[$j]->[0][0], %opts); 319 my $result = $ae->evaluate($i2[$k]->[0][0]); 320 if ($result->{score} == 0) { 321 next; 322 } 323 } 324 if ($pm11 == 0) { 325 # $opts{'correctAnswer'} = $i1[$j]->[0][1]; 326 my $ae = main::num_cmp($i1[$j]->[0][1], %opts); 327 my $result = $ae->evaluate($i2[$k]->[0][1]); 328 if ($result->{score} == 0) { 329 next; 330 } 331 } 332 $lookformatch=0; 333 } 334 if ($lookformatch) { # still looking ... 335 return 0; 336 } 337 } 338 return 1; 339 } 340 341 sub show_int { 342 my($intt) = shift; 343 my($intstring) = ""; 344 return "|$intt->[0]->[0]%%$intt->[0]->[1]|"; 345 } 346 347 348 349 } # End of package Intervals 350 351 { 352 package Interval_evaluator; 353 354 sub nicify_string { 355 my $str = shift; 356 357 $str = uc($str); 358 $str =~ s/\s//g; # remove white space 359 $str; 360 } 361 362 ##### The answer evaluator 363 364 sub interval_cmp { 365 366 my $right_ans = shift; 367 my %opts = @_; 368 369 $opts{'mode'} = 'std' unless defined($opts{'mode'}); 370 $opts{'tolType'} = 'relative' unless defined($opts{'tolType'}); 371 372 my $ans_eval = sub { 373 my $student = shift; 374 375 my $ans_hash = new AnswerHash( 376 'score'=>0, 377 'correct_ans'=>$right_ans, 378 'student_ans'=>$student, 379 'original_student_ans' => $student, 380 # 'type' => undef, 381 'ans_message'=>'', 382 'preview_text_string'=>'', 383 'preview_latex_string'=>'', 384 ); 385 # Handle string matches separately 386 my($studentisstring, $correctisstring, $tststr) = (0,0,""); 387 my($nicestud, $nicecorrect) = (nicify_string($student), 388 nicify_string($right_ans)); 389 if(defined($opts{'strings'})) { 390 for $tststr (@{$opts{'strings'}}) { 391 $tststr = nicify_string($tststr); 392 if(($tststr eq $nicestud)) {$studentisstring=1;} 393 if(($tststr eq $nicecorrect)) {$correctisstring=1;} 394 } 395 if($studentisstring) { 396 $ans_hash->{'preview_text_string'} = $student; 397 $ans_hash->{'preview_latex_string'} = $student; 398 } 399 } 400 my($student_int, $correct_int); 401 if(!$studentisstring) { 402 $student_int = new Intervals($student); 403 if(! $student_int->parse_intervals(%opts)) { 404 # Error in student input 405 $ans_hash->{'student_ans'} = "error: $student_int->{htmlerror}"; 406 $ans_hash->{'ans_message'} = "$student_int->{error_msg}"; 407 return $ans_hash; 408 } 409 410 $ans_hash->{'student_ans'} = $student_int->{'value'}; 411 $ans_hash->{'preview_text_string'} = $student_int->{'normalized'}; 412 $ans_hash->{'preview_latex_string'} = $student_int->{'latex'}; 413 } 414 415 if(!$correctisstring) { 416 $correct_int = new Intervals($right_ans); 417 if(! $correct_int->parse_intervals(%opts)) { 418 # Cannot parse instuctor's answer! 419 $ans_hash->{'ans_message'} = "Tell your professor that there is an error in this problem."; 420 return $ans_hash; 421 } 422 } 423 if($correctisstring || $studentisstring) { 424 if($nicestud eq $nicecorrect) { 425 $ans_hash -> setKeys('score' => 1); 426 } 427 } else { 428 if (Intervals::cmp_intervals($correct_int, $student_int, %opts)) { 429 $ans_hash -> setKeys('score' => 1); 430 } 431 } 432 433 return $ans_hash; 434 }; 435 436 return $ans_eval; 437 } 438 439 } 440 441 { 442 package Equation_eval; 443 444 sub split_eqn { 445 my $instring = shift; 446 447 split /=/, $instring; 448 } 449 450 451 sub equation_cmp { 452 my $right_ans = shift; 453 my %opts = @_; 454 my $vars = ['x','y']; 455 456 457 $vars = $opts{'vars'} if defined($opts{'vars'}); 458 459 my $ans_eval = sub { 460 my $student = shift; 461 462 my $ans_hash = new AnswerHash( 463 'score'=>0, 464 'correct_ans'=>$right_ans, 465 'student_ans'=>$student, 466 'original_student_ans' => $student, 467 # 'type' => undef, 468 'ans_message'=>'', 469 'preview_text_string'=>'', 470 'preview_latex_string'=>'', 471 ); 472 473 if(! ($student =~ /\S/)) { return $ans_hash; } 474 475 my @right= split_eqn($right_ans); 476 if(scalar(@right) != 2) { 477 $ans_hash->{'ans_message'} = "Tell your professor that there is an error in this problem."; 478 return $ans_hash; 479 } 480 my @studsplit = split_eqn($student); 481 if(scalar(@studsplit) != 2) { 482 $ans_hash->{'ans_message'} = "You did not enter an equation (with an equals sign and two sides)."; 483 return $ans_hash; 484 } 485 486 # Next we should do syntax checks on everyone 487 488 my $ah = new AnswerHash; 489 $ah->input($right[0]); 490 $ah=main::check_syntax($ah); 491 if($ah->{error_flag}) { 492 $ans_hash->{'ans_message'} = "Tell your professor that there is an error in this problem."; 493 return $ans_hash; 494 } 495 496 $ah->input($right[1]); 497 $ah=main::check_syntax($ah); 498 if($ah->{error_flag}) { 499 $ans_hash->{'ans_message'} = "Tell your professor that there is an error in this problem."; 500 return $ans_hash; 501 } 502 503 # Correct answer checks out, now check student's syntax 504 505 my @prevs = ("",""); 506 my @prevtxt = ("",""); 507 $ah->input($studsplit[0]); 508 $ah=main::check_syntax($ah); 509 if($ah->{error_flag}) { 510 $ans_hash->{'ans_message'} = "Syntax error on the left side of your equation."; 511 return $ans_hash; 512 } 513 $prevs[0] = $ah->{'preview_latex_string'}; 514 $prevstxt[0] = $ah->{'preview_text_string'}; 515 516 517 $ah->input($studsplit[1]); 518 $ah=main::check_syntax($ah); 519 if($ah->{error_flag}) { 520 $ans_hash->{'ans_message'} = "Syntax error on the right side of your equation."; 521 return $ans_hash; 522 } 523 $prevs[1] = $ah->{'preview_latex_string'}; 524 $prevstxt[1] = $ah->{'preview_text_string'}; 525 526 $ans_hash->{'preview_latex_string'} = "$prevs[0] = $prevs[1]"; 527 $ans_hash->{'preview_text_string'} = "$prevstxt[0] = $prevstxt[1]"; 528 529 530 # Check for answer equivalent to 0=0 531 # Could be false positive below because of parameter 532 my $ae = main::fun_cmp("0", %opts); 533 my $res = $ae->evaluate("$studsplit[0]-($studsplit[1])"); 534 if($res->{'score'}==1) { 535 # Student is 0=0, is correct answer also like this? 536 $res = $ae->evaluate("$right[0]-($right[1])"); 537 if($res->{'score'}==1) { 538 $ans_hash-> setKeys('score' => $res->{'score'}); 539 } 540 return $ans_hash; 541 } 542 543 # Maybe answer really is 0=0, and student got it wrong, so check that 544 $res = $ae->evaluate("$right[0]-($right[1])"); 545 if($res->{'score'}==1) { 546 return $ans_hash; 547 } 548 549 # Finally, use fun_cmp to check the answers 550 551 $ae = main::fun_cmp("o*($right[0]-($right[1]))", vars=>$vars, params=>['o'], %opts); 552 $res= $ae->evaluate("$studsplit[0]-($studsplit[1])"); 553 $ans_hash-> setKeys('score' => $res->{'score'}); 554 555 return $ans_hash; 556 }; 557 558 return $ans_eval; 559 } 560 } 561 562 =head3 interval_cmp () 563 564 Compares an interval or union of intervals. Typical invocations are 565 566 interval_cmp("(2, 3] U(7, 11)") 567 568 The U is used for union symbol. In fact, any garbage (or nothing at all) 569 can go between intervals. It makes sure open/closed parts of intervals 570 are correct, unless you don't like that. To have it ignore the difference 571 between open and closed endpoints, use 572 573 interval_cmp("(2, 3] U(7, 11)", sloppy=>'yes') 574 575 interval_cmp uses num_cmp on the endpoints. You can pass optional 576 arguments for num_cmp, so to change the tolerance, you can use 577 578 interval_cmp("(2, 3] U(3+4, 11)", relTol=>3) 579 580 The intervals can be listed in any order, unless you want to force a 581 particular order, which is signaled as 582 583 interval_cmp("(2, 3] U(3+4, 11)", ordered=>'strict') 584 585 You can specify infinity as an endpoint. It will do a case-insensitive 586 string match looking for I, Infinity, Infty, or Inf. You can prepend a + 587 or -, as in 588 589 interval_cmp("(-inf, 3] U [e^10, infinity)") 590 or 591 interval_cmp("(-INF, 3] U [e^10, +I)") 592 593 If the question might have an empty set as the answer, you can use 594 the strings option to allow for it. So 595 596 interval_cmp("$ans", strings=>['empty']) 597 598 will not generate an error message if the student enters the string 599 empty. Better still, it will mark a student answer of "empty" as correct 600 iff this matches $ans. 601 602 You can use interval_cmp for ordered pairs, or lists of ordered pairs. 603 Internally, this is just a distinction of whether to put nice union symbols 604 between intervals, or commas. To get commas, use 605 606 interval_cmp("(1,2), (2,3), (4,-1)", unions=>'no') 607 608 Note that interval_cmp makes no attempt at simplifying overlapping intervals. 609 This becomes an important feature when you are really checking lists of 610 ordered pairs. 611 612 =cut 613 614 sub interval_cmp2 { 615 my $correct_ans = shift; 616 617 my %opts = @_; 618 619 my $mode = $num_params{mode} || 'std'; 620 my %options = (debug => $opts{debug}); 621 my $ans_type = ''; # set to List, Union, or Interval below 622 623 # 624 # Get an apppropriate context based on the mode 625 # 626 my $oldContext = Context(); 627 my ($context, $ans_eval); 628 if(defined($opts{unions}) and $opts{unions} eq 'no' ) { 629 # This is really a list of points, not intervals at all 630 $context = $Parser::Context::Default::context{Vector}->copy; 631 $ans_type = 'List'; 632 $options{showCoordinateHints} = 0; 633 $options{showHints} = 0; 634 $options{partialCredit}=0; 635 $options{showLengthHints} = 0; 636 } else { 637 $context = $Parser::Context::Default::context{Numeric}->copy; 638 $context->parens->set( 639 '(' => {type => 'Interval'}, 640 '[' => {type => 'Interval'}, 641 '{' => {type => 'Interval'}, 642 ); 643 $correct_ans =~ tr/u/U/; 644 if($correct_ans =~ /U/) { 645 $context->operators->add('u'=> {precedence => 0.5, associativity => 'left', 646 type => 'bin', isUnion => 1, string => ' U ', TeX => '\cup ', 647 class => 'Parser::BOP::union'}); 648 $ans_type = 'Union'; 649 $options{showHints} = 0; 650 $options{showLengthHints} = 0; 651 $options{showEndpointHints}=0; 652 $options{partialCredit}=0; 653 } else { 654 $ans_type = 'Interval'; 655 $options{showEndpointHints}=0; 656 } 657 } 658 $opts{tolType} = $opts{tolType} || 'relative'; 659 $opts{tolerance} = $opts{tolerance} || $opts{tol} || 660 $opts{reltol} || $opts{relTol} || $opts{abstol} || 1; 661 $opts{zeroLevel} = $opts{zeroLevel} || $opts{zeroLevelTol} || 662 $main::numZeroLevelTolDefault; 663 if ($opts{tolType} eq 'absolute' or defined($opts{tol}) 664 or defined($opts{abstol})) { 665 $context->flags->set( 666 tolerance => $opts{tolerance}, 667 tolType => 'absolute', 668 ); 669 } else { 670 $context->flags->set( 671 tolerance => .01*$opts{tolerance}, 672 tolType => 'relative', 673 ); 674 } 675 $context->flags->set( 676 zeroLevel => $opts{zeroLevel}, 677 zeroLevelTol => $opts{zeroLevelTol}, 678 ); 679 $options{ordered} = 1 if(defined($opts{ordered}) and $opts{ordered}); 680 if (defined($opts{'sloppy'}) && $opts{'sloppy'} eq 'yes') { 681 $options{requireParenMatch} = 0; 682 } 683 $context->strings->add( 684 'i' => {alias=>'infinity'}, 685 'infty' => {alias=>'infinity'}, 686 'minfinity' => {infinite=>1, negative=>1}, 687 'minfty' => {alias=>'minfinity'}, 688 'minf' => {alias=>'minfinity'}, 689 'mi' => {alias=>'minfinity'}, 690 ); 691 Context($context); 692 if($ans_type eq 'List') { 693 $ans_eval = List($correct_ans)->cmp(%options); 694 } elsif($ans_type eq 'Union') { 695 $ans_eval = Union($correct_ans)->cmp(%options); 696 } elsif($ans_type eq 'Interval') { 697 $ans_eval = Interval($correct_ans)->cmp(%options); 698 } else { 699 warn "Bug -- should not be here in interval_cmp"; 700 } 701 702 Context($oldContext); 703 return($ans_eval); 704 705 706 # ToDo: 707 # modes? 708 # strings 709 # infinities 710 #@infinitywords = ("i", "inf", "infty", "infinity"); 711 #$infinityre = join '|', @infinitywords; 712 #$infinityre = "^([-+m]?)($infinityre)\$"; 713 714 715 } 716 717 sub interval_cmp { 718 Interval_evaluator::interval_cmp(@_); 719 } 720 721 =head3 number_list_cmp () 722 723 Checks an answer which is a comma-separated list of numbers. The actual 724 numbers are fed to num_cmp, so all of the flexibilty of num_cmp carries 725 over (values can be expressions to be evaluated). For example, 726 727 number_list_cmp("1, -2") 728 729 will accept "1, -2", "-2, 1", or "-1-1,sqrt(1)". 730 731 number_list_cmp("1^2 + 1, 2^2 + 1, 3^2 + 1", ordered=>'strict') 732 733 will accept "2, 5, 10", but not "5, 2, 10". 734 735 If you want to allow complex number entries, complex=>'ok' will cause it 736 to use cplx_cmp instead: 737 738 number_list_cmp("2, -2, 2i, -2i", complex=>'ok') 739 740 In cases where you set complex=>'ok', be sure the problem file loads 741 PGcomplexmacros.pl. 742 743 Optional arguements for num_cmp (resp. cplx_cmp) can be used as well, 744 such as 745 746 number_list_cmp("cos(3), sqrt(111)", relTol => 3) 747 748 The strings=>['hello'] argument is treated specially. It can be used to 749 replace the entire answer. So 750 751 number_list_cmp("cos(3), sqrt(111)", strings=>['none']) 752 753 will mark "none" wrong, but not generate an error. On the other hand, 754 755 number_list_cmp("none", strings=>['none']) 756 757 will mark "none" as correct. 758 759 =cut 760 761 sub number_list_cmp { 762 my $list = shift; 763 764 my %num_params = @_; 765 766 my $mode = $num_params{mode} || 'std'; 767 my %options = (debug => $num_params{debug}); 768 769 # 770 # Get an apppropriate context based on the mode 771 # 772 my $oldContext = Context(); 773 my $context; 774 for ($mode) { 775 /^strict$/i and do { 776 $context = $Parser::Context::Default::context{LimitedNumeric}->copy; 777 $context->operators->set(',' => {class=> 'Parser::BOP::comma'}); 778 last; 779 }; 780 /^arith$/i and do { 781 $context = $Parser::Context::Default::context{LegacyNumeric}->copy; 782 $context->functions->disable('All'); 783 last; 784 }; 785 /^frac$/i and do { 786 $context = $Parser::Context::Default::context{'LimitedNumeric-Fraction'}->copy; 787 $context->operators->set(',' => {class=> 'Parser::BOP::comma'}); 788 last; 789 }; 790 if(defined($num_params{'complex'}) && 791 ($num_params{'complex'} =~ /(yes|ok)/i)) { 792 $context = $Parser::Context::Default::context{Complex}->copy; 793 last; 794 } 795 796 # default 797 $context = $Parser::Context::Default::context{LegacyNumeric}->copy; 798 } 799 $context->{format}{number} = $num_params{'format'} || $main::numFormatDefault; 800 $context->strings->clear; 801 if ($num_params{strings}) { 802 foreach my $string (@{$num_params{strings}}) { 803 my %tex = ($string =~ m/(-?)inf(inity)?/i)? (TeX => "$1\\infty"): (); 804 $context->strings->add(uc($string) => {%tex}); 805 } 806 } 807 808 $num_params{tolType} = $num_params{tolType} || 'relative'; 809 $num_params{tolerance} = $num_params{tolerance} || $num_params{tol} || 810 $num_params{reltol} || $num_params{relTol} || $num_params{abstol} || 1; 811 $num_params{zeroLevel} = $num_params{zeroLevel} || $num_params{zeroLevelTol} || 812 $main::numZeroLevelTolDefault; 813 if ($num_params{tolType} eq 'absolute' or defined($num_params{tol}) 814 or defined($num_params{abstol})) { 815 $context->flags->set( 816 tolerance => $num_params{tolerance}, 817 tolType => 'absolute', 818 ); 819 } else { 820 $context->flags->set( 821 tolerance => .01*$num_params{tolerance}, 822 tolType => 'relative', 823 ); 824 } 825 $context->flags->set( 826 zeroLevel => $num_params{zeroLevel}, 827 zeroLevelTol => $num_params{zeroLevelTol}, 828 ); 829 $options{ordered} = 1 if(defined($num_params{ordered}) and $opts{ordered}); 830 # These didn't exist before in number_list_cmp so they behaved like 831 # in List()->cmp. Now they can be optionally set 832 $options{showHints}= $num_params{showHints} || 0; 833 $options{showLengthHints}= $num_params{showHints} || 0; 834 $options{partialCredit}= $num_params{showHints} || 0; 835 836 Context($context); 837 my $ans_eval = List($list)->cmp(%options); 838 Context($oldContext); 839 return($ans_eval); 840 } 841 842 843 =head3 equation_cmp () 844 845 Compares an equation. This really piggy-backs off of fun_cmp. It looks 846 at LHS-RHS of the equations to see if they agree up to constant multiple. 847 It also guards against an answer of 0=0 (which technically gives a constant 848 multiple of any equation). It is best suited to situations such as checking 849 the equation of a line which might be vertical and you don't want to give 850 that away, or checking equations of ellipses where the students answer should 851 be quadratic. 852 853 Typical invocation would be: 854 855 equation_com("x^2+(y-1)^2 = 11", vars=>['x','y']) 856 857 =cut 858 859 sub equation_cmp { 860 Equation_eval::equation_cmp(@_); 861 } 862
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |