[system] / trunk / pg / macros / extraAnswerEvaluators.pl Repository:
ViewVC logotype

View of /trunk/pg/macros/extraAnswerEvaluators.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3463 - (download) (as text) (annotate)
Wed Aug 10 23:05:41 2005 UTC (7 years, 9 months ago) by jj
File size: 24491 byte(s)
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