[system] / trunk / pg / macros / extraAnswerEvaluators.pl Repository: Repository Listing bbplugincoursesdistsnplrochestersystemwww

 1 : gage 1064 2 : apizer 1080 3 : gage 1064 # 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 : apizer 1080 27 : gage 1064 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 : apizer 1080 33 : gage 1064 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 : apizer 1080 53 : gage 1064 # 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 : apizer 1080 129 : gage 1064 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 : apizer 1080 237 : gage 1064 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[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 : apizer 1080 362 : gage 1064 ##### 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 : apizer 1080 372 : gage 1064 my $ans_eval = sub { 373 : my$student = shift; 374 : apizer 1080 375 : gage 1064 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 : apizer 1080 410 : gage 1064 $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 : { 443 : package Number_List; 444 : apizer 1080 445 : gage 1064 sub new { 446 : my $class = shift; 447 : my$base_string = shift; 448 : my $self = {}; 449 :$self->{'original'} = $base_string; 450 : return bless$self, $class; 451 : } 452 : 453 : sub make_complex_number { 454 : my$instring = shift; 455 : 456 : $instring = main::math_constants($instring); 457 : $instring =~ s/e\^/exp /g; 458 : my$parser = new AlgParserWithImplicitExpand; 459 : my $ret =$parser -> parse($instring); 460 :$parser -> tostring(); 461 : $parser -> normalize(); 462 :$instring = $parser -> tostring(); 463 :$instring =~ s/\bi\b/(i)/g; 464 : my ($in,$PG_errors,$PG_errors_long) = main::PG_restricted_eval($instring); 465 : return ($in+0*Complex1::i()); 466 : } 467 : 468 : 469 : apizer 1080 470 : gage 1064 sub parse_number_list { 471 : my($self) = shift; 472 : my(%opts) = @_; 473 : my($str) =$self->{'original'}; 474 : my(@ans_list) = (); 475 : my(@sort_list) = (); 476 : delete($opts{'ordered'}); 477 : 478 : my$complex=0; 479 : if(defined($opts{'complex'}) && 480 : ($opts{'complex'} =~ /(yes|ok)/i)) { 481 : $complex=1; 482 : delete($opts{'mode'}); 483 : } 484 : delete($opts{'complex'}); 485 :$self->{'normalized'} = ''; 486 : $self->{'value'} = ''; 487 :$self->{'latex'} = ''; 488 : $self->{'htmlerror'} = ''; 489 :$self->{'error_msg'} = ''; 490 : my($cur) = ""; 491 : my($level,$spot,$hold,$char) = (1,0,0,"a"); 492 : my($strt, $end) = (0, length($str)); 493 : my($specials) = '[$$$$,\{\}]'; 494 : my($tmp_ae,$tmp_ae2); 495 : if($complex) { 496 : $tmp_ae = main::cplx_cmp(new Complex(1,0), %opts); 497 :$tmp_ae2 = main::cplx_cmp(new Complex(1,0)); 498 : } else { 499 : $tmp_ae = main::num_cmp(1, %opts); 500 :$tmp_ae2 = main::num_cmp(1); 501 : } 502 : 503 : while ($spot <$end) { 504 : $char = substr($str,$spot,1); 505 : if ($char=~ /$specials/) { # Its a special character 506 : if ($char eq ",") { 507 : if ($level == 1) { # Level 1 comma 508 :$cur = substr($str,$hold, $spot-$hold); 509 : my($tmp_ah); 510 :$tmp_ah = $tmp_ae->evaluate($cur); 511 : if(has_errors($tmp_ah)) { 512 :$self->error("I could not parse your input correctly",[$hold,$spot]); 513 : return 0; 514 : } 515 : $self->{'normalized'} .= (defined($tmp_ah->{'preview_text_string'}) ? $tmp_ah->{'preview_text_string'} :$tmp_ah->{'student_ans'}).", "; 516 : $self->{'value'} .=$tmp_ah->{'student_ans'}.", "; 517 : $self->{'latex'} .= (defined($tmp_ah->{'preview_latex_string'}) ? $tmp_ah->{'preview_latex_string'} :$tmp_ah->{'student_ans'}).", "; 518 : $tmp_ah =$tmp_ae2->evaluate($cur); 519 :$hold = $spot+1; 520 : push @sort_list, [$cur,$tmp_ah->{'student_ans'}]; 521 : push @ans_list,$cur; 522 : } 523 : } # end of comma 524 : elsif ($char eq "[" or$char eq "(" or $char eq "{") { #opening 525 :$level++; 526 : } # end of open paren 527 : else { # must be closing paren 528 : if ($level == 1) { 529 :$self->error("Not a valid entry; unmatched $char.",[$spot]); 530 : return 0; 531 : } # end of level <= 1 532 : $level--; 533 : } # end of closing brace 534 : } 535 :$spot++; 536 : } 537 : apizer 1080 538 : gage 1064 if($level>1) { 539 :$self->error("Your expression has unmatched parens.", 540 : [$hold,$spot]); 541 : return 0; 542 : } 543 : $cur = substr($str,$hold,$spot-$hold); 544 : apizer 1080 545 : gage 1064 my($tmp_ah); 546 : $tmp_ah =$tmp_ae->evaluate($cur); 547 : apizer 1080 548 : gage 1064 if(has_errors($tmp_ah)) { 549 : $self->error("I could not parse your input correctly",[$hold, $spot]); 550 : return 0; 551 : } 552 : if(not ($cur =~ /\w/)) { # Input was empty 553 : $self->{'forsort'} = []; 554 : return 1; 555 : } 556 : 557 :$self->{'normalized'} .= defined($tmp_ah->{'preview_text_string'}) ?$tmp_ah->{'preview_text_string'} : $tmp_ah->{'student_ans'}; 558 :$self->{'value'} .= $tmp_ah->{'student_ans'}; 559 :$self->{'latex'} .= defined($tmp_ah->{'preview_latex_string'}) ?$tmp_ah->{'preview_latex_string'} : $tmp_ah->{'student_ans'}; 560 : if((3==4) &&$complex) { 561 : $tmp_ah =&{$tmp_ae2}($cur); 562 : } else { 563 :$tmp_ah = $tmp_ae2->evaluate($cur); 564 : } 565 : $hold =$spot+1; 566 : push @sort_list, [$cur,$tmp_ah->{'student_ans'}]; 567 : push @ans_list, $cur; 568 : 569 :$self->{'parsed'} = \@ans_list; 570 : $self->{'forsort'} = \@sort_list; 571 : return 1; 572 : } 573 : 574 : sub number_list_cmp { 575 : my$right_ans = shift; 576 : my %opts = @_; 577 : 578 : $opts{'mode'} = 'std' unless defined($opts{'mode'}); 579 : $opts{'tolType'} = 'relative' unless defined($opts{'tolType'}); 580 : 581 : my $ans_eval = sub { 582 : my$student = shift; 583 : apizer 1080 584 : gage 1064 my $ans_hash = new AnswerHash( 585 : 'score'=>0, 586 : 'correct_ans'=>$right_ans, 587 : 'student_ans'=>$student, 588 : 'original_student_ans' =>$student, 589 : # 'type' => undef, 590 : 'ans_message'=>'', 591 : 'preview_text_string'=>'', 592 : 'preview_latex_string'=>'', 593 : ); 594 : my $student_list = new Number_List($student); 595 : if(! $student_list->parse_number_list(%opts)) { 596 : # Error in student input 597 :$ans_hash->{'student_ans'} = "error: $student_list->{htmlerror}"; 598 :$ans_hash->{'ans_message'} = "$student_list->{error_msg}"; 599 : return$ans_hash; 600 : } 601 : 602 : $ans_hash->{'student_ans'} =$student_list->{'value'}; 603 : $ans_hash->{'preview_text_string'} =$student_list->{'normalized'}; 604 : $ans_hash->{'preview_latex_string'} =$student_list->{'latex'}; 605 : apizer 1080 606 : gage 1064 my $correct_list = new Number_List($right_ans); 607 : if(! $correct_list->parse_number_list(%opts)) { 608 : # Cannot parse instuctor's answer! 609 :$ans_hash->{'ans_message'} = "Tell your professor that there is an error in this problem."; 610 : return $ans_hash; 611 : } 612 : if (cmp_numlists($correct_list, $student_list, %opts)) { 613 :$ans_hash -> setKeys('score' => 1); 614 : } 615 : apizer 1080 616 : gage 1064 return $ans_hash; 617 : }; 618 : 619 : return$ans_eval; 620 : } 621 : 622 : sub sorting_sub { 623 : $_[0]->[1] <=>$_[1]->[1]; 624 : } 625 : 626 : sub cmp_numlists { 627 : my($in1) = shift; 628 : my($in2) = shift; 629 : my(%opts) = @_; 630 : my($strict_ordering) = 0; 631 : if (defined($opts{'ordered'}) && ($opts{'ordered'} eq 'yes')) { 632 :$strict_ordering = 1; 633 : } 634 : delete($opts{'ordered'}); 635 : apizer 1080 636 : gage 1064 my$complex=0; 637 : if(defined($opts{'complex'}) && 638 : ($opts{'complex'} =~ /(yes|ok)/i)) { 639 : $complex=1; 640 : delete($opts{'mode'}); 641 : } 642 : delete($opts{'complex'}); 643 : 644 : my(@fs1) = @{$in1->{'forsort'}}; 645 : my(@fs2) = @{$in2->{'forsort'}}; 646 : 647 : apizer 1080 648 : gage 1064 # Same number of values? 649 : if (scalar(@fs1) != scalar(@fs2)) { 650 : return 0; 651 : } 652 : 653 : my($j); 654 : if($complex) { 655 : for$j (@fs1) {$j->[1] = make_complex_number($j->[1]);} 656 : for $j (@fs2) {$j->[1] = make_complex_number($j->[1]);} 657 : } 658 : 659 : if($strict_ordering==0) { 660 : apizer 2143 @fs1 = main::PGsort(sub {$_[0]->[1] <=$_[1]->[1];}, @fs1); 661 : @fs2 = main::PGsort(sub {$_[0]->[1] <$_[1]->[1];}, @fs2); 662 : gage 1064 } 663 : apizer 1080 664 : gage 1064 for ($j=0;$j[1], %opts); 668 : } else { 669 : $ae = main::num_cmp($fs1[$j]->[0], %opts); 670 : } 671 : my$result; 672 : if($complex) { 673 :$result =$ae->evaluate($fs2[$j]->[1]); 674 : } else { 675 :$result = $ae->evaluate($fs2[$j]->[0]); 676 : } 677 : if ($result->{score} == 0) { 678 : return 0; 679 : } 680 : } 681 : return 1; 682 : } 683 : 684 : # error routine copied from AlgParser 685 : sub error { 686 : my($self, @args) = @_; 687 : # we cheat to use error from algparser 688 : my($ap) = new AlgParser(); 689 : $ap->inittokenizer($self->{'original'}); 690 : $ap->error(@args); 691 :$self->{htmlerror} = $ap->{htmlerror}; 692 :$self->{error_msg} = $ap->{error_msg}; 693 : } 694 : apizer 1080 695 : gage 1064 sub has_errors { 696 : my($ah) = shift; 697 : apizer 1080 698 : gage 1064 if($ah->{'student_ans'} =~ /error/) { 699 : return 1; 700 : } 701 : my($am) = $ah->{'ans_message'}; 702 : if($am =~ /error/) { 703 : return 2; 704 : } 705 : if($am =~ /must enter/) { 706 : return 3; 707 : } 708 : if($am =~ /does not evaluate/) { 709 : return 4; 710 : } 711 : return 0; 712 : } 713 : 714 : # Syntax is 715 : # interval_cmp("[1,2) U [3, infty)", options) 716 : # where options are key/value pairs for num_cmp. Also, we allow the option 717 : # 'ordering' which can be 'strict', which means that we do not want to test rearrangements 718 : # of the intervals. 719 : 720 : apizer 1080 721 : gage 1064 } 722 : 723 : { 724 : package Equation_eval; 725 : 726 : sub split_eqn { 727 : my $instring = shift; 728 : 729 : split /=/,$instring; 730 : } 731 : apizer 1080 732 : 733 : gage 1064 sub equation_cmp { 734 : my $right_ans = shift; 735 : my %opts = @_; 736 : my$vars = ['x','y']; 737 : 738 : apizer 1080 739 : gage 1064 $vars =$opts{'vars'} if defined($opts{'vars'}); 740 : 741 : my$ans_eval = sub { 742 : my $student = shift; 743 : apizer 1080 744 : gage 1064 my$ans_hash = new AnswerHash( 745 : 'score'=>0, 746 : 'correct_ans'=>$right_ans, 747 : 'student_ans'=>$student, 748 : 'original_student_ans' => $student, 749 : # 'type' => undef, 750 : 'ans_message'=>'', 751 : 'preview_text_string'=>'', 752 : 'preview_latex_string'=>'', 753 : ); 754 : 755 : if(! ($student =~ /\S/)) { return $ans_hash; } 756 : apizer 1080 757 : gage 1064 my @right= split_eqn($right_ans); 758 : if(scalar(@right) != 2) { 759 : $ans_hash->{'ans_message'} = "Tell your professor that there is an error in this problem."; 760 : return$ans_hash; 761 : } 762 : my @studsplit = split_eqn($student); 763 : if(scalar(@studsplit) != 2) { 764 :$ans_hash->{'ans_message'} = "You did not enter an equation (with an equals sign and two sides)."; 765 : return $ans_hash; 766 : } 767 : 768 : # Next we should do syntax checks on everyone 769 : 770 : my$ah = new AnswerHash; 771 : $ah->input($right[0]); 772 : $ah=main::check_syntax($ah); 773 : if($ah->{error_flag}) { 774 :$ans_hash->{'ans_message'} = "Tell your professor that there is an error in this problem."; 775 : return $ans_hash; 776 : } 777 : apizer 1080 778 : gage 1064$ah->input($right[1]); 779 :$ah=main::check_syntax($ah); 780 : if($ah->{error_flag}) { 781 : $ans_hash->{'ans_message'} = "Tell your professor that there is an error in this problem."; 782 : return$ans_hash; 783 : } 784 : 785 : # Correct answer checks out, now check student's syntax 786 : 787 : my @prevs = ("",""); 788 : my @prevtxt = ("",""); 789 : $ah->input($studsplit[0]); 790 : $ah=main::check_syntax($ah); 791 : if($ah->{error_flag}) { 792 :$ans_hash->{'ans_message'} = "Syntax error on the left side of your equation."; 793 : return $ans_hash; 794 : } 795 :$prevs[0] = $ah->{'preview_latex_string'}; 796 :$prevstxt[0] = $ah->{'preview_text_string'}; 797 : apizer 1080 798 : 799 : gage 1064$ah->input($studsplit[1]); 800 :$ah=main::check_syntax($ah); 801 : if($ah->{error_flag}) { 802 : $ans_hash->{'ans_message'} = "Syntax error on the right side of your equation."; 803 : return$ans_hash; 804 : } 805 : $prevs[1] =$ah->{'preview_latex_string'}; 806 : $prevstxt[1] =$ah->{'preview_text_string'}; 807 : 808 : $ans_hash->{'preview_latex_string'} = "$prevs[0] = $prevs[1]"; 809 :$ans_hash->{'preview_text_string'} = "$prevstxt[0] =$prevstxt[1]"; 810 : apizer 1080 811 : 812 : gage 1064 # Check for answer equivalent to 0=0 813 : # Could be false positive below because of parameter 814 : my $ae = main::fun_cmp("0", %opts); 815 : my$res = $ae->evaluate("$studsplit[0]-($studsplit[1])"); 816 : if($res->{'score'}==1) { 817 : # Student is 0=0, is correct answer also like this? 818 : $res =$ae->evaluate("$right[0]-($right[1])"); 819 : if($res->{'score'}==1) { 820 :$ans_hash-> setKeys('score' => $res->{'score'}); 821 : } 822 : return$ans_hash; 823 : } 824 : 825 : # Maybe answer really is 0=0, and student got it wrong, so check that 826 : $res =$ae->evaluate("$right[0]-($right[1])"); 827 : if($res->{'score'}==1) { 828 : return$ans_hash; 829 : } 830 : 831 : # Finally, use fun_cmp to check the answers 832 : apizer 1080 833 : gage 1064 $ae = main::fun_cmp("o*($right[0]-($right[1]))", vars=>$vars, params=>['o'], %opts); 834 : $res=$ae->evaluate("$studsplit[0]-($studsplit[1])"); 835 : $ans_hash-> setKeys('score' =>$res->{'score'}); 836 : apizer 1080 837 : gage 1064 return $ans_hash; 838 : }; 839 : 840 : return$ans_eval; 841 : } 842 : } 843 : 844 : =head3 interval_cmp () 845 : 846 : Compares an interval or union of intervals. Typical invocations are 847 : 848 : interval_cmp("(2, 3] U(7, 11)") 849 : 850 : The U is used for union symbol. In fact, any garbage (or nothing at all) 851 : can go between intervals. It makes sure open/closed parts of intervals 852 : are correct, unless you don't like that. To have it ignore the difference 853 : between open and closed endpoints, use 854 : 855 : interval_cmp("(2, 3] U(7, 11)", sloppy=>'yes') 856 : 857 : interval_cmp uses num_cmp on the endpoints. You can pass optional 858 : arguments for num_cmp, so to change the tolerance, you can use 859 : 860 : interval_cmp("(2, 3] U(3+4, 11)", relTol=>3) 861 : 862 : The intervals can be listed in any order, unless you want to force a 863 : particular order, which is signaled as 864 : 865 : interval_cmp("(2, 3] U(3+4, 11)", ordered=>'strict') 866 : 867 : You can specify infinity as an endpoint. It will do a case-insensitive 868 : string match looking for I, Infinity, Infty, or Inf. You can prepend a + 869 : or -, as in 870 : 871 : interval_cmp("(-inf, 3] U [e^10, infinity)") 872 : or 873 : interval_cmp("(-INF, 3] U [e^10, +I)") 874 : 875 : If the question might have an empty set as the answer, you can use 876 : the strings option to allow for it. So 877 : 878 : interval_cmp("$ans", strings=>['empty']) 879 : 880 : will not generate an error message if the student enters the string 881 : empty. Better still, it will mark a student answer of "empty" as correct 882 : iff this matches$ans. 883 : 884 : You can use interval_cmp for ordered pairs, or lists of ordered pairs. 885 : Internally, this is just a distinction of whether to put nice union symbols 886 : between intervals, or commas. To get commas, use 887 : 888 : interval_cmp("(1,2), (2,3), (4,-1)", unions=>'no') 889 : 890 : Note that interval_cmp makes no attempt at simplifying overlapping intervals. 891 : This becomes an important feature when you are really checking lists of 892 : ordered pairs. 893 : 894 : =cut 895 : 896 : sub interval_cmp { 897 : Interval_evaluator::interval_cmp(@_); 898 : } 899 : 900 : =head3 number_list_cmp () 901 : 902 : Checks an answer which is a comma-separated list of numbers. The actual 903 : numbers are fed to num_cmp, so all of the flexibilty of num_cmp carries 904 : over (values can be expressions to be evaluated). For example, 905 : 906 : number_list_cmp("1, -2") 907 : 908 : will accept "1, -2", "-2, 1", or "-1-1,sqrt(1)". 909 : 910 : number_list_cmp("1^2 + 1, 2^2 + 1, 3^2 + 1", ordered=>'strict') 911 : 912 : will accept "2, 5, 10", but not "5, 2, 10". 913 : 914 : If you want to allow complex number entries, complex=>'ok' will cause it 915 : to use cplx_cmp instead: 916 : 917 : number_list_cmp("2, -2, 2i, -2i", complex=>'ok') 918 : 919 : In cases where you set complex=>'ok', be sure the problem file loads 920 : PGcomplexmacros.pl. 921 : 922 : Optional arguements for num_cmp (resp. cplx_cmp) can be used as well, 923 : such as 924 : 925 : number_list_cmp("cos(3), sqrt(111)", relTol => 3) 926 : 927 : The strings=>['hello'] argument is treated specially. It can be used to 928 : replace the entire answer. So 929 : 930 : number_list_cmp("cos(3), sqrt(111)", strings=>['none']) 931 : 932 : will mark "none" wrong, but not generate an error. On the other hand, 933 : 934 : number_list_cmp("none", strings=>['none']) 935 : 936 : will makr "none" as correct. 937 : 938 : =cut 939 : 940 : sub number_list_cmp { 941 : Number_List::number_list_cmp(@_); 942 : } 943 : 944 : =head3 equation_cmp () 945 : 946 : Compares an equation. This really piggy-backs off of fun_cmp. It looks 947 : at LHS-RHS of the equations to see if they agree up to constant multiple. 948 : It also guards against an answer of 0=0 (which technically gives a constant 949 : multiple of any equation). It is best suited to situations such as checking 950 : the equation of a line which might be vertical and you don't want to give 951 : that away, or checking equations of ellipses where the students answer should 952 : be quadratic. 953 : 954 : Typical invocation would be: 955 : 956 : equation_com("x^2+(y-1)^2 = 11", vars=>['x','y']) 957 : 958 : =cut 959 : 960 : sub equation_cmp { 961 : Equation_eval::equation_cmp(@_); 962 : } 963 :