From: Mike G. v. a. <we...@ma...> - 2005-06-10 02:40:08
|
Log Message: ----------- Merging changes made for rel-2-1-3 back into rel-2-1-patches Tags: ---- rel-2-1-patches Modified Files: -------------- pg/lib: Value.pm pg/lib/Parser: BOP.pm Context.pm Differentiation.pm Function.pm List.pm UOP.pm pg/lib/Parser/Context: Default.pm Functions.pm pg/lib/Parser/List: Vector.pm pg/lib/Value: AnswerChecker.pm Complex.pm Formula.pm Infinity.pm Interval.pm List.pm Matrix.pm Point.pm Real.pm String.pm Union.pm Vector.pm pg/lib/WeBWorK/PG: IO.pm Translator.pm pg/macros: PGbasicmacros.pl PGchoicemacros.pl PGcommonFunctions.pl Parser.pl Value.pl dangerousMacros.pl parserImplicitPlane.pl parserParametricLine.pl Added Files: ----------- pg/macros: answerCustom.pl parserImplicitEquation.pl parserMultiPart.pl parserSolutionFor.pl Revision Data ------------- Index: Value.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Value.pm,v retrieving revision 1.30 retrieving revision 1.30.2.1 diff -Llib/Value.pm -Llib/Value.pm -u -r1.30 -r1.30.2.1 --- lib/Value.pm +++ lib/Value.pm @@ -42,6 +42,8 @@ granularity => 1000, resolution => undef, max_adapt => 1E8, + checkUndefinedPoints => 0, + max_undefined => undef, }, ); @@ -80,7 +82,7 @@ '.' => '_dot', # see _dot below 'x' => 'cross', '<=>' => 'compare', - 'cmp' => 'cmp', + 'cmp' => 'compare_string', }; $$context->{pattern}{infinite} = '[-+]?inf(?:inity)?'; @@ -134,23 +136,29 @@ # Convert non-Value objects to Values, if possible # sub makeValue { - my $x = shift; - return $x if ref($x); + my $x = shift; my %params = (showError => 0, makeFormula => 1, @_); + return $x if ref($x) || $x eq ''; return Value::Real->make($x) if matchNumber($x); if (matchInfinite($x)) { my $I = Value::Infinity->new(); $I = $I->neg if $x =~ m/^$$Value::context->{pattern}{-infinity}$/; return $I; } - if ($Parser::installed) {return $x unless $$Value::context->{strings}{$x}} - return Value::String->make($x); + return Value::String->make($x) + if (!$Parser::installed || $$Value::context->{strings}{$x}); + return $x if !$params{makeFormula}; + Value::Error("String constant '$x' is not defined in this context") + if $params{showError}; + $x = Value::Formula->new($x); + $x = $x->eval if $x->isConstant; + return $x; } # # Get a printable version of the class of an object # sub showClass { - my $value = makeValue(shift); + my $value = makeValue(shift,makeFormula=>0); return "'".$value."'" unless Value::isValue($value); my $class = class($value); return showType($value) if ($class eq 'List'); @@ -255,7 +263,7 @@ # # Convert a list of values (and open and close parens) # to a formula whose type is the list type associated with -# the parens. If the formula is constant, evaluate it. +# the parens. # sub formula { my $self = shift; my $values = shift; @@ -269,7 +277,6 @@ $formula->{tree} = $formula->{context}{parser}{List}->new($formula,[@coords],0, $formula->{context}{parens}{$paren},$coords[0]->typeRef,$open,$close); $formula->{autoFormula} = 1; # mark that this was generated automatically -# return $formula->eval if scalar(%{$formula->{variables}}) == 0; return $formula; } @@ -284,6 +291,15 @@ } # +# Easy method for setting parameters of an object +# +sub with { + my $self = shift; my %hash = @_; + foreach my $id (keys(%hash)) {$self->{$id} = $hash{$id}} + return $self; +} + +# # Return a type structure for the item # (includes name, length of vectors, and so on) # @@ -310,7 +326,7 @@ # sub value {return @{(shift)->{data}}} # the value of the object (as an array) sub data {return (shift)->{data}} # the reference to the value -sub length {return (shift)->typeRef->{length}} # the number of coordinates +sub length {return scalar(@{(shift)->{data}})} # the number of coordinates sub type {return (shift)->typeRef->{name}} # the object type sub entryType {return (shift)->typeRef->{entryType}} # the coordinate type # @@ -325,7 +341,7 @@ # sub class { my $self = shift; my $class = ref($self) || $self; - $class =~ s/Value:://; + $class =~ s/.*:://; return $class; } @@ -354,8 +370,7 @@ return 0 unless Value::isValue($other); my $sprec = $$context->{precedence}{class($self)}; my $oprec = $$context->{precedence}{class($other)}; - return (defined($oprec) && $sprec < $oprec) || - ($sprec > $oprec && $sprec >= $$context->{precedence}{special}); + return (defined($oprec) && $sprec < $oprec); } sub promote {shift} @@ -426,9 +441,9 @@ # # Compare the values as strings # -sub cmp { +sub compare_string { my ($l,$r,$flag) = @_; - if ($l->promotePrecedence($r)) {return $r->compare($l,!$flag)} + if ($l->promotePrecedence($r)) {return $r->compare_string($l,!$flag)} $l = $l->stringify; $r = $r->stringify if Value::isValue($r); if ($flag) {my $tmp = $l; $l = $r; $r = $tmp} return $l cmp $r; @@ -450,14 +465,16 @@ # sub perl { my $self = shift; my $parens = shift; my $matrix = shift; - my $class = $self->class; my $mtype = $class eq 'Matrix'; + my $class = $self->class; + my $mtype = $class eq 'Matrix'; $mtype = -1 if $mtype & !$matrix; my $perl; my @p = (); foreach my $x (@{$self->data}) { if (Value::isValue($x)) {push(@p,$x->perl(0,$mtype))} else {push(@p,$x)} } @p = ("'".$self->{open}."'",@p,"'".$self->{close}."'") if $class eq 'Interval'; if ($matrix) { - $perl = '['.join(',',@p).']'; + $perl = join(',',@p); + $perl = '['.$perl.']' if $mtype > 0; } else { $perl = $class.'('.join(',',@p).')'; $perl = '('.$perl.')' if $parens == 1; @@ -502,7 +519,7 @@ # For debugging # sub traceback { - my $frame = 2; + my $frame = shift; $frame = 2 unless defined($frame); my $trace = ''; while (my ($pkg,$file,$line,$subname) = caller($frame++)) {$trace .= " in $subname at line $line of $file\n"} Index: BOP.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Parser/BOP.pm,v retrieving revision 1.9 retrieving revision 1.9.2.1 diff -Llib/Parser/BOP.pm -Llib/Parser/BOP.pm -u -r1.9 -r1.9.2.1 --- lib/Parser/BOP.pm +++ lib/Parser/BOP.pm @@ -276,7 +276,7 @@ my $extraParens = $self->{equation}{context}->flag('showExtraParens'); my $addparens = defined($precedence) && - ((($showparens eq 'all' || $bop->{fullparens}) && $extraParens) || + ($showparens eq 'all' || (($showparens eq 'extra' || $bop->{fullparens}) && $extraParens) || $precedence > $bop->{precedence} || ($precedence == $bop->{precedence} && ($bop->{associativity} eq 'right' || $showparens eq 'same'))); my $outerRight = !$addparens && ($outerRight || $position eq 'right'); Index: List.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Parser/List.pm,v retrieving revision 1.11 retrieving revision 1.11.2.1 diff -Llib/Parser/List.pm -Llib/Parser/List.pm -u -r1.11 -r1.11.2.1 --- lib/Parser/List.pm +++ lib/Parser/List.pm @@ -216,7 +216,8 @@ $open = '\left' .$open if $open ne ''; $close = '\right'.$close if $close ne ''; foreach my $x (@{$self->{coords}}) {push(@coords,$x->TeX)} - return $open.join(',',@coords).$close; + return $open.join(',',@coords).$close unless $self->{ColumnVector}; + '\left[\begin{array}{c}'.join('\cr'."\n",@coords).'\cr\end{array}\right]'; } # Index: Context.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Parser/Context.pm,v retrieving revision 1.11 retrieving revision 1.11.2.1 diff -Llib/Parser/Context.pm -Llib/Parser/Context.pm -u -r1.11 -r1.11.2.1 --- lib/Parser/Context.pm +++ lib/Parser/Context.pm @@ -157,7 +157,7 @@ last; }; - Value::Error("Precedence type should be one of 'Standard' or 'Non-standard'"); + Value::Error("Precedence type should be one of 'Standard' or 'Non-Standard'"); } } Index: Function.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Parser/Function.pm,v retrieving revision 1.10 retrieving revision 1.10.2.1 diff -Llib/Parser/Function.pm -Llib/Parser/Function.pm -u -r1.10 -r1.10.2.1 --- lib/Parser/Function.pm +++ lib/Parser/Function.pm @@ -109,6 +109,7 @@ my $class = $fn->{class}; my $result = eval {$class->_call($name,@_)}; return $result unless $@; + Value::Error($context->{error}{message}) if $context->{error}{message}; Value::Error("Can't take $name of ".join(',',@_)); } # @@ -126,7 +127,6 @@ my $formula = Value::Formula->blank; my @args = Value::toFormula($formula,@_); $formula->{tree} = $formula->{context}{parser}{Function}->new($formula,$name,[@args]); -# return $formula->eval if scalar(%{$formula->{variables}}) == 0; return $formula; } Index: Differentiation.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Parser/Differentiation.pm,v retrieving revision 1.3 retrieving revision 1.3.2.1 diff -Llib/Parser/Differentiation.pm -Llib/Parser/Differentiation.pm -u -r1.3 -r1.3.2.1 --- lib/Parser/Differentiation.pm +++ lib/Parser/Differentiation.pm @@ -103,11 +103,11 @@ $self = $parser->{Function}->new($equation,'exp', [$parser->{BOP}->new($equation,'*',$self->{rop}->copy($equation), - $parser->{Function}->new($equation,'log',[$self->{lop}->copy($equation)],0))]); + $parser->{Function}->new($equation,'ln',[$self->{lop}->copy($equation)],0))]); return $self->D($x); } $self = $parser->{BOP}->new($equation,'*', - $parser->{Function}->new($equation,'log',[$self->{lop}->copy($equation)],0), + $parser->{Function}->new($equation,'ln',[$self->{lop}->copy($equation)],0), $parser->{BOP}->new($equation,'*', $self->copy($equation),$self->{rop}->D($x)) ); @@ -534,13 +534,19 @@ sub Parser::Function::numeric::D {Parser::Function::D_chain(@_)} -sub Parser::Function::numeric::D_log { +sub Parser::Function::numeric::D_ln { my $self = shift; my $x = shift; my $equation = $self->{equation}; my $parser = $equation->{context}{parser}; return $parser->{BOP}->new($equation,'/',$parser->{Number}->new($equation,1),$x); } +sub Parser::Function::numeric::D_log { + my $self = $_[0]; + my $base10 = $self->{equation}{context}{flags}{useBaseTenLog}; + if ($base10) {return D_log10(@_)} else {return D_ln(@_)} +} + sub Parser::Function::numeric::D_log10 { my $self = shift; my $x = shift; my $equation = $self->{equation}; @@ -555,12 +561,12 @@ } sub Parser::Function::numeric::D_exp { - my $self = shift; my $x = shift; + my $self = shift; return $self->copy(); } sub Parser::Function::numeric::D_sqrt { - my $self = shift; my $x = shift; + my $self = shift; my $equation = $self->{equation}; my $parser = $equation->{context}{parser}; return @@ -573,7 +579,13 @@ ); } -sub Parser::Function::numeric::D_abs {Parser::Function::D(@_)} +sub Parser::Function::numeric::D_abs { + my $self = shift; my $x = shift; + my $equation = $self->{equation}; + my $parser = $equation->{context}{parser}; + return $parser->{BOP}->new($equation,'/',$x,$self->copy); +} + sub Parser::Function::numeric::D_int {Parser::Function::D(@_)} sub Parser::Function::numeric::D_sgn {Parser::Function::D(@_)} @@ -593,8 +605,10 @@ } sub Parser::List::AbsoluteValue::D { - my $self = shift; - $self->Error("Can't differentiate absolute values"); + my $self = shift; my $x = $self->{coords}[0]->copy; + my $equation = $self->{equation}; + my $parser = $equation->{context}{parser}; + return $parser->{BOP}->new($equation,'/', $x, $self->copy); } Index: UOP.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Parser/UOP.pm,v retrieving revision 1.11 retrieving revision 1.11.2.1 diff -Llib/Parser/UOP.pm -Llib/Parser/UOP.pm -u -r1.11 -r1.11.2.1 --- lib/Parser/UOP.pm +++ lib/Parser/UOP.pm @@ -20,7 +20,8 @@ $UOP->_check; $UOP->{isConstant} = 1 if $op->{isConstant}; $UOP = $context->{parser}{Value}->new($equation,[$UOP->eval]) - if $op->{isConstant} && (!$UOP->isNeg || $op->isNeg) && $context->flag('reduceConstants'); + if $op->{isConstant} && (!$UOP->isNeg || $op->isNeg) && + ($context->flag('reduceConstants') || $op->{isInfinity}); return $UOP; } Index: Vector.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Parser/List/Vector.pm,v retrieving revision 1.3 retrieving revision 1.3.2.1 diff -Llib/Parser/List/Vector.pm -Llib/Parser/List/Vector.pm -u -r1.3 -r1.3.2.1 --- lib/Parser/List/Vector.pm +++ lib/Parser/List/Vector.pm @@ -7,10 +7,20 @@ @ISA = qw(Parser::List); # -# The basic List class does nearly everything. We only need this class -# for its name. +# The basic List class does nearly everything. # +# +# Check that the coordinates are numbers (avoid <i+j+k>) +# +sub _check { + my $self = shift; + foreach my $x (@{$self->{coords}}) { + $self->{equation}->Error("Coordinates of Vector must be Numbers") + unless $x->isNumber; + } +} + my $ijk_string = ['i','j','k','0']; my $ijk_TeX = ['\boldsymbol{i}','\boldsymbol{j}','\boldsymbol{k}','\boldsymbol(0)']; Index: Real.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Value/Real.pm,v retrieving revision 1.10 retrieving revision 1.10.2.1 diff -Llib/Value/Real.pm -Llib/Value/Real.pm -u -r1.10 -r1.10.2.1 --- lib/Value/Real.pm +++ lib/Value/Real.pm @@ -11,41 +11,44 @@ @ISA = qw(Value); use overload - '+' => \&add, - '-' => \&sub, - '*' => \&mult, - '/' => \&div, - '**' => \&power, + '+' => sub {shift->add(@_)}, + '-' => sub {shift->sub(@_)}, + '*' => sub {shift->mult(@_)}, + '/' => sub {shift->div(@_)}, + '**' => sub {shift->power(@_)}, '.' => \&Value::_dot, - 'x' => \&Value::cross, - '<=>' => \&compare, - 'cmp' => \&Value::cmp, - 'neg' => sub {$_[0]->neg}, - 'abs' => sub {$_[0]->abs}, - 'sqrt'=> sub {$_[0]->sqrt}, - 'exp' => sub {$_[0]->exp}, - 'log' => sub {$_[0]->log}, - 'sin' => sub {$_[0]->sin}, - 'cos' => sub {$_[0]->cos}, - 'atan2' => \&atan2, - 'nomethod' => \&Value::nomethod, - '""' => \&Value::stringify; + 'x' => sub {shift->cross(@_)}, + '<=>' => sub {shift->compare(@_)}, + 'cmp' => sub {shift->compare_string(@_)}, + 'neg' => sub {shift->neg}, + 'abs' => sub {shift->abs}, + 'sqrt'=> sub {shift->sqrt}, + 'exp' => sub {shift->exp}, + 'log' => sub {shift->log}, + 'sin' => sub {shift->sin}, + 'cos' => sub {shift->cos}, + 'atan2' => sub {shift->atan2(@_)}, + 'nomethod' => sub {shift->nomethod(@_)}, + '""' => sub {shift->stringify(@_)}; # # Check that the input is a real number or a formula -# Make a formula if either part is a formula +# or a string that evaluates to a number # sub new { my $self = shift; my $class = ref($self) || $self; my $x = shift; $x = [$x,@_] if scalar(@_) > 0; - $x = $x->data if ref($x) eq $pkg; + return $x if ref($x) eq $pkg; $x = [$x] unless ref($x) eq 'ARRAY'; - Value::Error("Can't convert ARRAY of length ".scalar(@{$x})." to a Real Number") + Value::Error("Can't convert ARRAY of length ".scalar(@{$x})." to ".Value::showClass($class)) unless (scalar(@{$x}) == 1); - Value::Error("Real Number can't be ".Value::showClass($x->[0])) - unless (Value::isRealNumber($x->[0])); - return $self->formula($x->[0]) if Value::isFormula($x->[0]); - bless {data => $x}, $class; + if (Value::isRealNumber($x->[0])) { + return $self->formula($x->[0]) if Value::isFormula($x->[0]); + return (bless {data => $x}, $class); + } + $x = Value::makeValue($x->[0]); + return $x if Value::isRealNumber($x); + Value::Error("Can't convert ".Value::showClass($x)." to ".Value::showClass($class)); } # @@ -69,6 +72,7 @@ # Return the real number type # sub typeRef {return $Value::Type{number}} +sub length {1} # # return the real number Index: Complex.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Value/Complex.pm,v retrieving revision 1.11 retrieving revision 1.11.2.1 diff -Llib/Value/Complex.pm -Llib/Value/Complex.pm -u -r1.11 -r1.11.2.1 --- lib/Value/Complex.pm +++ lib/Value/Complex.pm @@ -8,32 +8,33 @@ @ISA = qw(Value); use overload - '+' => \&add, - '-' => \&sub, - '*' => \&mult, - '/' => \&div, - '**' => \&power, + '+' => sub {shift->add(@_)}, + '-' => sub {shift->sub(@_)}, + '*' => sub {shift->mult(@_)}, + '/' => sub {shift->div(@_)}, + '**' => sub {shift->power(@_)}, '.' => \&Value::_dot, - 'x' => \&Value::cross, - '<=>' => \&compare, - 'cmp' => \&Value::cmp, - '~' => sub {$_[0]->conj}, - 'neg' => sub {$_[0]->neg}, - 'abs' => sub {$_[0]->norm}, - 'sqrt'=> sub {$_[0]->sqrt}, - 'exp' => sub {$_[0]->exp}, - 'log' => sub {$_[0]->log}, - 'sin' => sub {$_[0]->sin}, - 'cos' => sub {$_[0]->cos}, - 'atan2' => \&atan2, - 'nomethod' => \&Value::nomethod, - '""' => \&Value::stringify; + 'x' => sub {shift->cross(@_)}, + '<=>' => sub {shift->compare(@_)}, + 'cmp' => sub {shift->compare_string(@_)}, + '~' => sub {shift->conj}, + 'neg' => sub {shift->neg}, + 'abs' => sub {shift->norm}, + 'sqrt'=> sub {shift->sqrt}, + 'exp' => sub {shift->exp}, + 'log' => sub {shift->log}, + 'sin' => sub {shift->sin}, + 'cos' => sub {shift->cos}, + 'atan2' => sub {shift->atan2(@_)}, + 'nomethod' => sub {shift->nomethod(@_)}, + '""' => sub {shift->stringify(@_)}; # # Check that the inputs are: # one or two real numbers, or # an array ref of one or two reals, or # a Value::Complex object +# a formula returning a real or complex # Make a formula if either part is a formula # sub new { @@ -44,6 +45,7 @@ Value::Error("Can't convert ARRAY of length ".scalar(@{$x})." to a Complex Number") unless (scalar(@{$x}) == 2); $x->[0] = Value::makeValue($x->[0]); $x->[1] = Value::makeValue($x->[1]); + return $x->[0] if Value::isComplex($x->[0]) && scalar(@_) == 0; Value::Error("Real part can't be ".Value::showClass($x->[0])) unless (Value::isRealNumber($x->[0])); Value::Error("Imaginary part can't be ".Value::showClass($x->[1])) @@ -71,6 +73,7 @@ # Return the complex type # sub typeRef {return $Value::Type{complex}} +sub length {2} sub isZero {shift eq "0"} sub isOne {shift eq "1"} @@ -156,7 +159,7 @@ sub compare { my ($l,$r,$flag) = @_; - if ($l->promotePrecedence($r)) {return $r->power($l,!$flag)} + if ($l->promotePrecedence($r)) {return $r->compare($l,!$flag)} $r = promote($r); if ($flag) {my $tmp = $l; $l = $r; $r = $tmp} my ($a,$b) = (@{$l->data}); Index: List.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Value/List.pm,v retrieving revision 1.13 retrieving revision 1.13.2.1 diff -Llib/Value/List.pm -Llib/Value/List.pm -u -r1.13 -r1.13.2.1 --- lib/Value/List.pm +++ lib/Value/List.pm @@ -10,13 +10,13 @@ @ISA = qw(Value); use overload - '+' => \&add, + '+' => sub {shift->add(@_)}, '.' => \&Value::_dot, - 'x' => \&Value::cross, - '<=>' => \&compare, - 'cmp' => \&Value::cmp, - 'nomethod' => \&Value::nomethod, - '""' => \&stringify; + 'x' => sub {shift->cross(@_)}, + '<=>' => sub {shift->compare(@_)}, + 'cmp' => sub {shift->compare_string(@_)}, + 'nomethod' => sub {shift->nomethod(@_)}, + '""' => sub {shift->stringify(@_)}; # # Make a List out of a list of entries or a @@ -43,7 +43,6 @@ # # Return the proper data # -sub length {return scalar(@{shift->{data}})} sub typeRef { my $self = shift; return Value::Type($self->class, $self->length, Value::Type($self->{type},1)); Index: String.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Value/String.pm,v retrieving revision 1.3 retrieving revision 1.3.2.1 diff -Llib/Value/String.pm -Llib/Value/String.pm -u -r1.3 -r1.3.2.1 --- lib/Value/String.pm +++ lib/Value/String.pm @@ -9,10 +9,10 @@ use overload '.' => \&Value::_dot, - '<=>' => \&compare, - 'cmp' => \&compare, - 'nomethod' => \&Value::nomethod, - '""' => \&Value::stringify; + '<=>' => sub {shift->compare(@_)}, + 'cmp' => sub {shift->compare(@_)}, + 'nomethod' => sub {shift->nomethod(@_)}, + '""' => sub {shift->stringify(@_)}; # # Create a string object @@ -21,7 +21,7 @@ my $self = shift; my $class = ref($self) || $self; my $x = join('',@_); if ($Parser::installed) { - Value::Error("Unrecognized string '$x'") + Value::Error("String constant '$x' is not defined in this context") unless $$Value::context->{strings}{$x}; } bless {data => [$x]}, $class; @@ -44,7 +44,7 @@ # sub promote { my $x = shift; $x = [$x,@_] if scalar(@_) > 0; - $x = Value::makeValue($x); $x = join('',@{$x}) if ref($x) eq 'ARRAY'; + $x = Value::makeValue($x,showError=>1); $x = join('',@{$x}) if ref($x) eq 'ARRAY'; $x = $pkg->make($x) unless Value::isValue($x); return $x; } Index: Union.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Value/Union.pm,v retrieving revision 1.11 retrieving revision 1.11.2.1 diff -Llib/Value/Union.pm -Llib/Value/Union.pm -u -r1.11 -r1.11.2.1 --- lib/Value/Union.pm +++ lib/Value/Union.pm @@ -8,13 +8,13 @@ @ISA = qw(Value); use overload - '+' => \&add, + '+' => sub {shift->add(@_)}, '.' => \&Value::_dot, - 'x' => \&Value::cross, - '<=>' => \&compare, - 'cmp' => \&Value::cmp, - 'nomethod' => \&Value::nomethod, - '""' => \&Value::stringify; + 'x' => sub {shift->cross(@_)}, + '<=>' => sub {shift->compare(@_)}, + 'cmp' => sub {shift->compare_string(@_)}, + 'nomethod' => sub {shift->nomethod(@_)}, + '""' => sub {shift->stringify(@_)}; # # Convert a value to a union of intervals. The value must be @@ -51,7 +51,6 @@ # # Return the appropriate data. # -sub length {return scalar(@{shift->{data}})} sub typeRef { my $self = shift; return Value::Type($self->class, $self->length, $self->data->[0]->typeRef); Index: Infinity.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Value/Infinity.pm,v retrieving revision 1.6 retrieving revision 1.6.2.1 diff -Llib/Value/Infinity.pm -Llib/Value/Infinity.pm -u -r1.6 -r1.6.2.1 --- lib/Value/Infinity.pm +++ lib/Value/Infinity.pm @@ -9,11 +9,11 @@ use overload '.' => \&Value::_dot, - '<=>' => \&compare, - 'cmp' => \&Value::cmp, - 'neg' => \&neg, - 'nomethod' => \&Value::nomethod, - '""' => \&Value::stringify; + '<=>' => sub {shift->compare(@_)}, + 'cmp' => sub {shift->compare_string(@_)}, + 'neg' => sub {shift->neg(@_)}, + 'nomethod' => sub {shift->nomethod(@_)}, + '""' => sub {shift->stringify(@_)}; # # Create an infinity object Index: Vector.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Value/Vector.pm,v retrieving revision 1.11 retrieving revision 1.11.2.1 diff -Llib/Value/Vector.pm -Llib/Value/Vector.pm -u -r1.11 -r1.11.2.1 --- lib/Value/Vector.pm +++ lib/Value/Vector.pm @@ -10,29 +10,32 @@ @ISA = qw(Value); use overload - '+' => \&add, - '-' => \&sub, - '*' => \&mult, - '/' => \&div, - '**' => \&power, + '+' => sub {shift->add(@_)}, + '-' => sub {shift->sub(@_)}, + '*' => sub {shift->mult(@_)}, + '/' => sub {shift->div(@_)}, + '**' => sub {shift->power(@_)}, '.' => \&Value::_dot, - 'x' => \&cross, - '<=>' => \&compare, - 'cmp' => \&Value::cmp, - 'neg' => sub {$_[0]->neg}, - 'abs' => sub {$_[0]->abs}, - 'nomethod' => \&Value::nomethod, - '""' => \&stringify; + 'x' => sub {shift->cross(@_)}, + '<=>' => sub {shift->compare(@_)}, + 'cmp' => sub {shift->compare_string(@_)}, + 'neg' => sub {shift->neg}, + 'abs' => sub {shift->abs}, + 'nomethod' => sub {shift->nomethod(@_)}, + '""' => sub {shift->stringify(@_)}; # # Convert a value to a Vector. The value can be # a list of numbers, or an reference to an array of numbers # a point or vector object (demote a vector) # a matrix if it is n x 1 or 1 x n +# a string that parses to a vector # sub new { my $self = shift; my $class = ref($self) || $self; my $p = shift; $p = [$p,@_] if (scalar(@_) > 0); + $p = Value::makeValue($p) if (defined($p) && !ref($p)); + return $p if (Value::isFormula($p) && $p->type eq Value::class($self)); my $pclass = Value::class($p); my $isFormula = 0; my @d; @d = $p->dimensions if $pclass eq 'Matrix'; if ($pclass =~ m/Point|Vector/) {$p = $p->data} @@ -43,22 +46,26 @@ $p = [$p] if (defined($p) && ref($p) ne 'ARRAY'); Value::Error("Vectors must have at least one coordinate") unless defined($p) && scalar(@{$p}) > 0; foreach my $x (@{$p}) { + $x = Value::makeValue($x); $isFormula = 1 if Value::isFormula($x); Value::Error("Coordinate of Vector can't be ".Value::showClass($x)) unless Value::isNumber($x); - $x = Value::Real->make($x) unless ref($x); } } - return $self->formula($p) if $isFormula; - bless {data => $p}, $class; + if ($isFormula) { + my $v = $self->formula($p); + if (ref($self) && $self->{ColumnVector}) { + $v->{tree}{ColumnVector} = 1; + $v->{tree}{open} = $v->{tree}{close} = undef; + } + return $v; + } + my $v = bless {data => $p}, $class; + $v->{ColumnVector} = 1 if ref($self) && $self->{ColumnVector}; + return $v; } # -# The number of coordinates -# -sub length {return scalar(@{shift->{data}})} - -# # Try to promote arbitary data to a vector # sub promote { @@ -138,7 +145,7 @@ sub cross { my ($l,$r,$flag) = @_; - if ($l->promotePrecedence($r)) {return $r->dot($l,!$flag)} + if ($l->promotePrecedence($r)) {return $r->cross($l,!$flag)} ($l,$r) = (promote($l)->data,promote($r)->data); Value::Error("Vector must be in 3-space for cross product") unless scalar(@{$l}) == 3 && scalar(@{$r}) == 3; @@ -241,8 +248,8 @@ sub stringify { my $self = shift; - return $self->TeX(undef,$self->{open},$self->{close}) if $$Value::context->flag('StringifyAsTeX'); - return $self->string(undef,$self->{open},$self->{close}) + return $self->TeX if $$Value::context->flag('StringifyAsTeX'); + return $self->string(undef,$self->{open},$self->{close}); }; sub string { @@ -250,7 +257,7 @@ return $self->ijk($ijk_string) if ($self->{ijk} || $equation->{ijk} || $$Value::context->flag("ijk")); my $def = ($equation->{context} || $$Value::context)->lists->get('Vector'); - my $open = shift || $def->{open}; my $close = shift || $def->{close}; + my $open = shift || $def->{open}; my $close = shift || $def->{close}; my @coords = (); foreach my $x (@{$self->data}) { if (Value::isValue($x)) {push(@coords,$x->string($equation))} else {push(@coords,$x)} @@ -262,12 +269,17 @@ my $self = shift; my $equation = shift; return $self->ijk if ($self->{ijk} || $equation->{ijk} || $$Value::context->flag("ijk")); my $def = ($equation->{context} || $$Value::context)->lists->get('Vector'); - my $open = shift || $def->{open}; my $close = shift || $def->{close}; + my $open = shift || $self->{open} || $def->{open}; + my $close = shift || $self->{close} || $def->{close}; my @coords = (); foreach my $x (@{$self->data}) { if (Value::isValue($x)) {push(@coords,$x->TeX($equation))} else {push(@coords,$x)} } - return '\left'.$open.join(',',@coords).'\right'.$close; + return '\left'.$open.join(',',@coords).'\right'.$close unless $self->{ColumnVector}; + $def = ($equation->{context} || $$Value::context)->lists->get('Matrix'); + $open = shift || $self->{open} || $def->{open}; + $close = shift || $self->{close} || $def->{close}; + return '\left'.$open.'\begin{array}{c}'.join('\\\\',@coords).'\\\\\end{array}\right'.$close; } sub ijk { Index: AnswerChecker.pm =================================================================== RCS file: /webwork/cvs/system/pg/lib/Value/AnswerChecker.pm,v retrieving revision 1.38 retrieving revision 1.38.2.1 diff -Llib/Value/AnswerChecker.pm -Llib/Value/AnswerChecker.pm -u -r1.38 -r1.38.2.1 --- lib/Value/AnswerChecker.pm +++ lib/Value/AnswerChecker.pm @@ -26,18 +26,23 @@ sub cmp { my $self = shift; my $ans = new AnswerEvaluator; + my $correct = protectHTML($self->{correct_ans}); + $correct = $self->correct_ans unless defined($correct); $ans->ans_hash( type => "Value (".$self->class.")", - correct_ans => protectHTML($self->string), + correct_ans => $correct, correct_value => $self, - $self->cmp_defaults, + $self->cmp_defaults(@_), @_ ); $ans->install_evaluator(sub {$ans = shift; $ans->{correct_value}->cmp_parse($ans)}); + $ans->install_pre_filter('erase') if $self->{ans_name}; # don't do blank check if answer_array $self->{context} = $$Value::context unless defined($self->{context}); return $ans; } +sub correct_ans {protectHTML(shift->string)} + # # Parse the student answer and compute its value, # produce the preview strings, and then compare the @@ -81,10 +86,13 @@ $ans->{preview_latex_string} = $ans->{student_formula}->TeX; $ans->{preview_text_string} = protectHTML($ans->{student_formula}->string); $ans->{student_ans} = $ans->{preview_text_string}; - $self->cmp_equal($ans); - $self->cmp_postprocess($ans) if !$ans->{error_message}; + if ($self->cmp_collect($ans)) { + $self->cmp_equal($ans); + $self->cmp_postprocess($ans) if !$ans->{error_message}; + } } else { $self->cmp_error($ans); + $self->cmp_collect($ans); } contextSet($context,%{$flags}); # restore context values Parser::Context->current(undef,$current); # put back the old context @@ -92,6 +100,37 @@ } # +# Check if the object has an answer array and collect the results +# Build the combined student answer and set the preview values +# +sub cmp_collect { + my $self = shift; my $ans = shift; + return 1 unless $self->{ans_name}; + $ans->{preview_latex_string} = $ans->{preview_text_string} = ""; + my $OK = $self->ans_collect($ans); + $ans->{student_ans} = $self->format_matrix($ans->{student_formula},@{$self->{format_options}},tth_delims=>1); + return 0 unless $OK; + my $array = $ans->{student_formula}; + if ($self->{ColumnVector}) { + my @V = (); foreach my $x (@{$array}) {push(@V,$x->[0])} + $array = [@V]; + } elsif (scalar(@{$array}) == 1) {$array = $array->[0]} + my $type = $self; + $type = "Value::".$self->{tree}->type if $self->class eq 'Formula'; + $ans->{student_formula} = eval {$type->new($array)->with(ColumnVector=>$self->{ColumnVector})}; + if (!defined($ans->{student_formula}) || $$Value::context->{error}{flag}) + {Parser::reportEvalError($@); return 0} + $ans->{student_value} = $ans->{student_formula}; + $ans->{preview_text_string} = $ans->{student_ans}; + $ans->{preview_latex_string} = $ans->{student_formula}->TeX; + if (Value::isFormula($ans->{student_formula}) && $ans->{student_formula}->isConstant) { + $ans->{student_value} = Parser::Evaluate($ans->{student_formula}); + return 0 unless $ans->{student_value}; + } + return 1; +} + +# # Check if the parsed student answer equals the professor's answer # sub cmp_equal { @@ -99,19 +138,41 @@ my $correct = $ans->{correct_value}; my $student = $ans->{student_value}; if ($correct->typeMatch($student,$ans)) { - my $equal = eval {$correct == $student}; + my $equal = $correct->cmp_compare($student,$ans); if (defined($equal) || !$ans->{showEqualErrors}) {$ans->score(1) if $equal; return} $self->cmp_error($ans); } else { return if $ans->{ignoreStrings} && (!Value::isValue($student) || $student->type eq 'String'); $ans->{ans_message} = $ans->{error_message} = - "Your answer isn't ".lc($ans->{cmp_class}). - " (it looks like ".lc($student->showClass).")" + "Your answer isn't ".lc($ans->{cmp_class}).'<BR>'. + "(it looks like ".lc($student->showClass).")" if !$ans->{isPreview} && $ans->{showTypeWarnings} && !$ans->{error_message}; } } # +# Perform the comparison, either using the checker supplied +# by the answer evaluator, or the overloaded == operator. +# + +our $CMP_ERROR = 2; # a fatal error was detected + +sub cmp_compare { + my $self = shift; my $other = shift; my $ans = shift; + return eval {$self == $other} unless ref($ans->{checker}) eq 'CODE'; + my $equal = eval {&{$ans->{checker}}($self,$other,$ans)}; + if (!defined($equal) && $@ ne '' && !$$Value::context->{error}{flag}) { + $$Value::context->setError("<I>An error occurred while checking your answer:</I>\n". + '<DIV STYLE="margin-left:1em">'.$@.'</DIV>',''); + $$Value::context->{error}{flag} = $CMP_ERROR; + warn "Please inform your instructor that an error occurred while checking your answer"; + } + return $equal; +} + +sub cmp_list_compare {Value::List::cmp_list_compare(@_)} + +# # Check if types are compatible for equality check # sub typeMatch { @@ -137,11 +198,11 @@ # sub cmp_error { my $self = shift; my $ans = shift; - my $context = $$Value::context; - my $message = $context->{error}{message}; - if ($context->{error}{pos}) { - my $string = $context->{error}{string}; - my ($s,$e) = @{$context->{error}{pos}}; + my $error = $$Value::context->{error}; + my $message = $error->{message}; + if ($error->{pos}) { + my $string = $error->{string}; + my ($s,$e) = @{$error->{pos}}; $message =~ s/; see.*//; # remove the position from the message $ans->{student_ans} = protectHTML(substr($string,0,$s)) . @@ -169,6 +230,239 @@ sub cmp_postprocess {} # +# create answer rules of various types +# +sub ans_rule {shift; pgCall('ans_rule',@_)} +sub named_ans_rule {shift; pgCall('NAMED_ANS_RULE',@_)} +sub named_ans_rule_extension {shift; pgCall('NAMED_ANS_RULE_EXTENSION',@_)} +sub ans_array {shift->ans_rule(@_)}; +sub named_ans_array {shift->named_ans_rule(@_)}; +sub named_ans_array_extension {shift->named_ans_rule_extension(@_)}; + +sub pgCall {my $call = shift; &{WeBWorK::PG::Translator::PG_restricted_eval('\&'.$call)}(@_)} +sub pgRef {WeBWorK::PG::Translator::PG_restricted_eval('\&'.shift)} + +our $answerPrefix = "MaTrIx"; + +# +# Lay out a matrix of answer rules +# +sub ans_matrix { + my $self = shift; + my ($extend,$name,$rows,$cols,$size,$open,$close,$sep) = @_; + my $named_extension = pgRef('NAMED_ANS_RULE_EXTENSION'); + my $new_name = pgRef('RECORD_FORM_LABEL'); + my $HTML = ""; my $ename = $name; + if ($name eq '') { + my $n = pgCall('inc_ans_rule_count'); + $name = pgCall('NEW_ANS_NAME',$n); + $ename = $answerPrefix.$n; + } + $self->{ans_name} = $ename; + $self->{ans_rows} = $rows; + $self->{ans_cols} = $cols; + my @array = (); + foreach my $i (0..$rows-1) { + my @row = (); + foreach my $j (0..$cols-1) { + if ($i == 0 && $j == 0) { + if ($extend) {push(@row,&$named_extension(&$new_name($name),$size))} + else {push(@row,pgCall('NAMED_ANS_RULE',$name,$size))} + } else { + push(@row,&$named_extension(&$new_name(ANS_NAME($ename,$i,$j)),$size)); + } + } + push(@array,[@row]); + } + $self->format_matrix([@array],open=>$open,close=>$close,sep=>$sep); +} + +sub ANS_NAME { + my ($name,$i,$j) = @_; + $name.'_'.$i.'_'.$j; +} + + +# +# Lay out an arbitrary matrix +# +sub format_matrix { + my $self = shift; + my $displayMode = $self->getPG('$displayMode'); + return $self->format_matrix_tex(@_) if ($displayMode eq 'TeX'); + return $self->format_matrix_HTML(@_); +} + +sub format_matrix_tex { + my $self = shift; my $array = shift; + my %options = (open=>'.',close=>'.',sep=>'',@_); + $self->{format_options} = [%options] unless $self->{format_options}; + my ($open,$close,$sep) = ($options{open},$options{close},$options{sep}); + my ($rows,$cols) = (scalar(@{$array}),scalar(@{$array->[0]})); + my $tex = ""; + $open = '\\'.$open if $open =~ m/[{}]/; $close = '\\'.$close if $close =~ m/[{}]/; + $tex .= '\(\left'.$open; + $tex .= '\setlength{\arraycolsep}{2pt}', $sep = '\,'.$sep if $sep; + $tex .= '\begin{array}{'.('c'x$cols).'}'; + foreach my $i (0..$rows-1) {$tex .= join($sep.'&',@{$array->[$i]}).'\cr'."\n"} + $tex .= '\end{array}\right'.$close.'\)'; + return $tex; +} + +sub format_matrix_HTML { + my $self = shift; my $array = shift; + my %options = (open=>'',close=>'',sep=>'',tth_delims=>0,@_); + $self->{format_options} = [%options] unless $self->{format_options}; + my ($open,$close,$sep) = ($options{open},$options{close},$options{sep}); + my ($rows,$cols) = (scalar(@{$array}),scalar(@{$array->[0]})); + my $HTML = ""; + if ($sep) {$sep = '</TD><TD STYLE="padding: 0px 1px">'.$sep.'</TD><TD>'} + else {$sep = '</TD><TD WIDTH="8px"></TD><TD>'} + foreach my $i (0..$rows-1) { + $HTML .= '<TR><TD HEIGHT="6px"></TD></TR>' if $i; + $HTML .= '<TR ALIGN="MIDDLE"><TD>'.join($sep,@{$array->[$i]}).'</TD></TR>'."\n"; + } + $open = $self->format_delimiter($open,$rows,$options{tth_delims}); + $close = $self->format_delimiter($close,$rows,$options{tth_delims}); + if ($open ne '' || $close ne '') { + $HTML = '<TR ALIGN="MIDDLE">' + . '<TD>'.$open.'</TD>' + . '<TD WIDTH="2"></TD>' + . '<TD><TABLE BORDER="0" CELLSPACING="0" CELLPADDING="0" CLASS="ArrayLayout">' + . $HTML + . '</TABLE></TD>' + . '<TD WIDTH="4"></TD>' + . '<TD>'.$close.'</TD>' + . '</TR>'."\n"; + } + return '<TABLE BORDER="0" CELLSPACING="0" CELLPADDING="0" CLASS="ArrayLayout"' + . ' STYLE="display:inline;vertical-align:-'.(1.1*$rows-.6).'em">' + . $HTML + . '</TABLE>'; +} + +sub VERBATIM { + my $string = shift; + my $displayMode = Value->getPG('$displayMode'); + $string = '\end{verbatim}'.$string.'\begin{verbatim}' if $displayMode eq 'TeX'; + return $string; +} + +# +# Create a tall delimiter to match the line height +# +sub format_delimiter { + my $self = shift; my $delim = shift; my $rows = shift; my $tth = shift; + return '' if $delim eq '' || $delim eq '.'; + my $displayMode = $self->getPG('$displayMode'); + return $self->format_delimiter_tth($delim,$rows,$tth) + if $tth || $displayMode eq 'HTML_tth' || $displayMode !~ m/^HTML_/; + my $rule = '\vrule width 0pt height '.(.8*$rows).'em depth 0pt'; + $rule = '\rule 0pt '.(.8*$rows).'em 0pt' if $displayMode eq 'HTML_jsMath'; + $delim = '\\'.$delim if $delim eq '{' || $delim eq '}'; + return '\(\left'.$delim.$rule.'\right.\)'; +} + +# +# Data for tth delimiters [top,mid,bot,rep] +# +my %tth_delim = ( + '[' => ['','','',''], + ']' => ['','','',''], + '(' => ['','','',''], + ')' => ['','','',''], + '{' => ['','','',''], + '}' => ['','','',''], + '|' => ['|','','|','|'], + '<' => ['<'], + '>' => ['>'], + '\lgroup' => ['','','',''], + '\rgroup' => ['','','',''], +); + +# +# Make delimiters as stacks of characters +# +sub format_delimiter_tth { + my $self = shift; + my $delim = shift; my $rows = shift; my $tth = shift; + return '' if $delim eq '' || !defined($tth_delim{$delim}); + my $c = $delim; $delim = $tth_delim{$delim}; + $c = $delim->[0] if scalar(@{$delim}) == 1; + my $size = ($tth? "": "font-size:175%; "); + return '<SPAN STYLE="'.$size.'margin:0px 2px">'.$c.'</SPAN>' + if $rows == 1 || scalar(@{$delim}) == 1; + my $HTML = ""; + if ($delim->[1] eq '') { + $HTML = join('<BR>',$delim->[0],($delim->[3])x(2*($rows-1)),$delim->[2]); + } else { + $HTML = join('<BR>',$delim->[0],($delim->[3])x($rows-1), + $delim->[1],($delim->[3])x($rows-1), + $delim->[2]); + } + return '<DIV STYLE="line-height:90%; margin: 0px 2px">'.$HTML.'</DIV>'; +} + + +# +# Look up the values of the answer array entries, and check them +# for syntax and other errors. Build the student answer +# based on these, and keep track of error messages. +# + +my @ans_defaults = (showCoodinateHints => 0, checker => sub {0}); + +sub ans_collect { + my $self = shift; my $ans = shift; + my $inputs = $self->getPG('$inputs_ref'); + my $blank = ($self->getPG('$displayMode') eq 'TeX') ? '\_\_' : '__'; + my ($rows,$cols) = ($self->{ans_rows},$self->{ans_cols}); + my @array = (); my $data = [$self->value]; my $errors = []; my $OK = 1; + if ($self->{ColumnVector}) {foreach my $x (@{$data}) {$x = [$x]}} + $data = [$data] unless ref($data->[0]) eq 'ARRAY'; + foreach my $i (0..$rows-1) { + my @row = (); + foreach my $j (0..$cols-1) { + if ($i || $j) { + my $entry = $inputs->{ANS_NAME($self->{ans_name},$i,$j)}; + my $result = $data->[$i][$j]->cmp(@ans_cmp_defaults)->evaluate($entry); + $OK &= entryCheck($result,$blank); + push(@row,$result->{student_formula}); + entryMessage($result->{ans_message},$errors,$i,$j,$rows); + } else { + $ans->{student_formula} = $ans->{student_value} = undef unless $ans->{student_ans} =~ m/\S/; + $OK &= entryCheck($ans,$blank); + push(@row,$ans->{student_formula}); + entryMessage($ans->{ans_message},$errors,$i,$j,$rows); + } + } + push(@array,[@row]); + } + $ans->{student_formula} = [@array]; + $ans->{ans_message} = $ans->{error_message} = join("<BR>",@{$errors}); + return $OK && scalar(@{$errors}) == 0; +} + +sub entryMessage { + my $message = shift; return unless $message; + my ($errors,$i,$j,$rows) = @_; $i++; $j++; + if ($rows == 1) {$message = "Coordinate $j: $message"} + else {$message = "Entry ($i,$j): $message"} + push(@{$errors},$message); +} + +sub entryCheck { + my $ans = shift; my $blank = shift; + return 1 if defined($ans->{student_value}); + if (!defined($ans->{student_formula})) { + $ans->{student_formula} = $ans->{student_ans}; + $ans->{student_formula} = $blank unless $ans->{student_formula}; + } + return 0 +} + + +# # Get and Set values in context # sub contextSet { @@ -216,7 +510,7 @@ package Value::Real; sub cmp_defaults {( - shift->SUPER::cmp_defaults, + shift->SUPER::cmp_defaults(@_), ignoreInfinity => 1, )} @@ -247,7 +541,7 @@ package Value::String; sub cmp_defaults {( - Value::Real->cmp_defaults, + Value::Real->cmp_defaults(@_), typeMatch => 'Value::Real', )} @@ -271,7 +565,7 @@ package Value::Point; sub cmp_defaults {( - shift->SUPER::cmp_defaults, + shift->SUPER::cmp_defaults(@_), showDimensionHints => 1, showCoordinateHints => 1, )} @@ -287,26 +581,46 @@ sub cmp_postprocess { my $self = shift; my $ans = shift; return unless $ans->{score} == 0 && !$ans->{isPreview}; - if ($ans->{showDimensionHints} && - $self->length != $ans->{student_value}->length) { - $self->cmp_Error($ans,"The dimension of your result is incorrect"); return; + my $student = $ans->{student_value}; + return if $ans->{ignoreStrings} && (!Value::isValue($student) || $student->type eq 'String'); + if ($ans->{showDimensionHints} && $self->length != $student->length) { + $self->cmp_Error($ans,"The number of coordinates is incorrect"); return; } if ($ans->{showCoordinateHints}) { my @errors; foreach my $i (1..$self->length) { push(@errors,"The ".$self->NameForNumber($i)." coordinate is incorrect") - if ($self->{data}[$i-1] != $ans->{student_value}{data}[$i-1]); + if ($self->{data}[$i-1] != $student->{data}[$i-1]); } $self->cmp_Error($ans,@errors); return; } } +sub correct_ans { + my $self = shift; + return $self->SUPER::correct_ans unless $self->{ans_name}; + Value::VERBATIM($self->format_matrix([[@{$self->{data}}]],@{$self->{format_options}},tth_delims=>1)); +} + +sub ANS_MATRIX { + my $self = shift; + my $extend = shift; my $name = shift; + my $size = shift || 5; + my $def = ($self->{context} || $$Value::context)->lists->get('Point'); + my $open = $self->{open} || $def->{open}; my $close = $self->{close} || $def->{close}; + $self->ans_matrix($extend,$name,1,$self->length,$size,$open,$close,','); +} + +sub ans_array {my $self = shift; $self->ANS_MATRIX(0,'',@_)} +sub named_ans_array {my $self = shift; $self->ANS_MATRIX(0,@_)} +sub named_ans_array_extension {my $self = shift; $self->ANS_MATRIX(1,@_)} + ############################################################# package Value::Vector; sub cmp_defaults {( - shift->SUPER::cmp_defaults, + shift->SUPER::cmp_defaults(@_), showDimensionHints => 1, showCoordinateHints => 1, promotePoints => 0, @@ -329,24 +643,51 @@ sub cmp_postprocess { my $self = shift; my $ans = shift; return unless $ans->{score} == 0; + my $student = $ans->{student_value}; + return if $ans->{ignoreStrings} && (!Value::isValue($student) || $student->type eq 'String'); if (!$ans->{isPreview} && $ans->{showDimensionHints} && - $self->length != $ans->{student_value}->length) { - $self->cmp_Error($ans,"The dimension of your result is incorrect"); return; + $self->length != $student->length) { + $self->cmp_Error($ans,"The number of coordinates is incorrect"); return; } if ($ans->{parallel} && - $self->isParallel($ans->{student_value},$ans->{sameDirection})) { + $self->isParallel($student,$ans->{sameDirection})) { $ans->score(1); return; } if (!$ans->{isPreview} && $ans->{showCoordinateHints} && !$ans->{parallel}) { my @errors; foreach my $i (1..$self->length) { push(@errors,"The ".$self->NameForNumber($i)." coordinate is incorrect") - if ($self->{data}[$i-1] != $ans->{student_value}{data}[$i-1]); + if ($self->{data}[$i-1] != $student->{data}[$i-1]); } $self->cmp_Error($ans,@errors); return; } } +sub correct_ans { + my $self = shift; + return $self->SUPER::correct_ans unless $self->{ans_name}; + return Value::VERBATIM($self->format_matrix([[$self->value]],@{$self->{format_options}},tth_delims=>1)) + unless $self->{ColumnVector}; + my @array = (); foreach my $x ($self->value) {push(@array,[$x])} + return Value::VERBATIM($self->format_matrix([@array],@{$self->{format_options}},tth_delims=>1)); +} + +sub ANS_MATRIX { + my $self = shift; + my $extend = shift; my $name = shift; + my $size = shift || 5; my ($def,$open,$close); + $def = ($self->{context} || $$Value::context)->lists->get('Matrix'); + $open = $self->{open} || $def->{open}; $close = $self->{close} || $def->{close}; + return $self->ans_matrix($extend,$name,$self->length,1,$size,$open,$close) + if ($self->{ColumnVector}); + $def = ($self->{context} || $$Value::context)->lists->get('Vector'); + $open = $self->{open} || $def->{open}; $close = $self->{close} || $def->{close}; + $self->ans_matrix($extend,$name,1,$self->length,$size,$open,$close,','); +} + +sub ans_array {my $self = shift; $self->ANS_MATRIX(0,'',@_)} +sub named_ans_array {my $self = shift; $self->ANS_MATRIX(0,@_)} +sub named_ans_array_extension {my $self = shift; $self->ANS_MATRIX(1,@_)} ############################################################# @@ -354,7 +695,7 @@ package Value::Matrix; sub cmp_defaults {( - shift->SUPER::cmp_defaults, + shift->SUPER::cmp_defaults(@_), showDimensionHints => 1, showEqualErrors => 0, )} @@ -371,7 +712,9 @@ my $self = shift; my $ans = shift; return unless $ans->{score} == 0 && !$ans->{isPreview} && $ans->{showDimensionHints}; - my @d1 = $self->dimensions; my @d2 = $ans->{student_value}->dimensions; + my $student = $ans->{student_value}; + return if $ans->{ignoreStrings} && (!Value::isValue($student) || $student->type eq 'String'); + my @d1 = $self->dimensions; my @d2 = $student->dimensions; if (scalar(@d1) != scalar(@d2)) { $self->cmp_Error($ans,"Matrix dimension is not correct"); return; @@ -385,12 +728,36 @@ } } +sub correct_ans { + my $self = shift; + return $self->SUPER::correct_ans unless $self->{ans_name}; + my @array = $self->value; @array = ([@array]) if $self->isRow; + Value::VERBATIM($self->format_matrix([$self->value],@{$self->{format_options}},tth_delims=>1)); +} + +sub ANS_MATRIX { + my $self = shift; + my $extend = shift; my $name = shift; + my $size = shift || 5; + my $def = ($self->{context} || $$Value::context)->lists->get('Matrix'); + my $open = $self->{open} || $def->{open}; my $close = $self->{close} || $def->{close}; + my @d = $self->dimensions; + Value::Error("Can't create ans_array for ".scalar(@d)."-dimensional matrix") + if (scalar(@d) > 2); + @d = (1,@d) if (scalar(@d) == 1); + $self->ans_matrix($extend,$name,@d,$size,$open,$close,''); +} + +sub ans_array {my $self = shift; $self->ANS_MATRIX(0,'',@_)} +sub named_ans_array {my $self = shift; $self->ANS_MATRIX(0,@_)} +sub named_ans_array_extension {my $self = shift; $self->ANS_MATRIX(1,@_)} + ############################################################# package Value::Interval; sub cmp_defaults {( - shift->SUPER::cmp_defaults, + shift->SUPER::cmp_defaults(@_), showEndpointHints => 1, showEndTypeHints => 1, )} @@ -412,6 +779,7 @@ my $self = shift; my $ans = shift; return unless $ans->{score} == 0 && !$ans->{isPreview}; my $other = $ans->{student_value}; + return if $ans->{ignoreStrings} && (!Value::isValue($other) || $other->type eq 'String'); return unless $other->class eq 'Interval'; my @errors; if ($ans->{showEndpointHints}) { @@ -461,16 +829,21 @@ sub cmp_defaults { my $self = shift; + my %options = (@_); + my $element = Value::makeValue($self->{data}[0]); + $element = Value::Formula->new($element) unless Value::isValue($element); return ( - Value::Real->cmp_defaults, + Value::Real->cmp_defaults(@_), showHints => undef, showLengthHints => undef, showParenHints => undef, partialCredit => undef, ordered => 0, + showEqualErrors => $options{ordered}, entry_type => undef, list_type => undef, - typeMatch => Value::makeValue($self->{data}[0]), + typeMatch => $element, + extra => $element, requireParenMatch => 1, removeParens => 1, ); @@ -489,7 +862,8 @@ my $cmp = $self->SUPER::cmp(@_); if ($cmp->{rh_ans}{removeParens}) { $self->{open} = $self->{close} = ''; - $cmp->ans_hash(correct_ans => $self->stringify); + $cmp->ans_hash(correct_ans => $self->stringify) + unless defined($self->{correct_ans}); } return $cmp; } @@ -501,23 +875,21 @@ # # get the paramaters # - my $showTypeWarnings = $ans->{showTypeWarnings}; - my $showHints = getOption($ans,'showHints'); - my $showLengthHints = getOption($ans,'showLengthHints'); - my $showParenHints = getOption($ans,'showLengthHints'); - my $partialCredit = getOption($ans,'partialCredit'); - my $ordered = $ans->{ordered}; + my $showHints = getOption($ans,'showHints'); + my $showLengthHints = getOption($ans,'showLengthHints'); + my $showParenHints = getOption($ans,'showLengthHints'); + my $partialCredit = getOption($ans,'partialCredit'); my $requireParenMatch = $ans->{requireParenMatch}; - my $typeMatch = $ans->{typeMatch}; - my $value = $ans->{entry_type}; - my $ltype = $ans->{list_type} || lc($self->type); + my $typeMatch = $ans->{typeMatch}; + my $value = $ans->{entry_type}; + my $ltype = $ans->{list_type} || lc($self->type); $value = (Value::isValue($typeMatch)? lc($typeMatch->cmp_class): 'value') unless defined($value); $value =~ s/(real|complex) //; $ans->{cmp_class} = $value; $value =~ s/^an? //; $value = 'formula' if $value =~ m/formula/; $ltype =~ s/^an? //; - $showTypeWarnings = $showHints = $showLengthHints = 0 if $ans->{isPreview}; + $showHints = $showLengthHints = 0 if $ans->{isPreview}; # # Get the lists of correct and student answers @@ -555,34 +927,92 @@ } return; } - # - # Check for empty lists - # - if (scalar(@correct) == 0 && scalar(@student) == 0) {$ans->score(1); return} # - # Initialize the score + # Determine the maximum score # my $M = scalar(@correct); my $m = scalar(@student); my $maxscore = ($m > $M)? $m : $M; + + # + # Compare the two lists + # (Handle errors in user-supplied functions) + # + my ($score,@errors); + if (ref($ans->{list_checker}) eq 'CODE') { + eval {($score,@errors) = &{$ans->{list_checker}}([@correct],[@student],$ans,$value)}; + if (!defined($score)) { + die $@ if $@ ne '' && $self->{context}{error}{flag} == 0; + $self->cmp_error($ans) if $self->{context}{error}{flag}; + } + } else { + ($score,@errors) = $self->cmp_list_compare([@correct],[@student],$ans,$value); + } + return unless defined($score); + + # + # Give hints about extra or missing answers + # + if ($showLengthHints) { + $value =~ s/ or /s or /; # fix "interval or union" + push(@errors,"There should be more ${value}s in your $ltype") + if ($score < $maxscore && $score == $m); + push(@errors,"There should be fewer ${value}s in your $ltype") + if ($score < $maxscore && $score == $M && !$showHints); + } + + # + # Finalize the score + # + $score = 0 if ($score != $maxscore && !$partialCredit); + $ans->score($score/$maxscore); + push(@errors,"Score = $ans->{score}") if $ans->{debug}; + my $error = join("\n",@errors); $error =~ s!</DIV>\n!</DIV>!g; + $ans->{error_message} = $ans->{ans_message} = $error; +} + +# +# Compare the contents of the list to see of they are equal +# +sub cmp_list_compare { + my $self = shift; + my $correct = shift; my $student = shift; my $ans = shift; my $value = shift; + my @correct = @{$correct}; my @student = @{$student}; my $m = scalar(@student); + my $ordered = $ans->{ordered}; + my $showTypeWarnings = $ans->{showTypeWarnings} && !$ans->{isPreview}; + my $typeMatch = $ans->{typeMatch}; + my $extra = $ans->{extra}; + my $showHints = getOption($ans,'showHints') && !$ans->{isPreview}; + my $error = $$Value::context->{error}; my $score = 0; my @errors; my $i = 0; # + # Check for empty lists + # + if (scalar(@correct) == 0) {$ans->score($m == 0); return} + + # # Loop through student answers looking for correct ones # ENTRY: foreach my $entry (@student) { - $i++; + $i++; $$Value::context->clearError; $entry = Value::makeValue($entry); $entry = Value::Formula->new($entry) if !Value::isValue($entry); if ($ordered) { - if (eval {shift(@correct) == $entry}) {$score++; next ENTRY} + if (scalar(@correct)) { + if (shift(@correct)->cmp_compare($entry,$ans)) {$score++; next ENTRY} + } else { + $extra->cmp_compare($entry,$ans); # do syntax check + } + if ($error->{flag} == $CMP_ERROR) {$self->cmp_error($ans); return} } else { foreach my $k (0..$#correct) { - if (eval {$correct[$k] == $entry}) { + if ($correct[$k]->cmp_compare($entry,$ans)) { splice(@correct,$k,1); $score++; next ENTRY; } + if ($error->{flag} == $CMP_ERROR) {$self->cmp_error($ans); return} } } # @@ -590,13 +1020,17 @@ # my $nth = ''; my $answer = 'answer'; my $class = $ans->{list_type} || $self->cmp_class; - if (scalar(@student) > 1) { + if ($m > 1) { $nth = ' '.$self->NameForNumber($i); $class = $ans->{cmp_class}; $answer = 'value'; } - if ($showTypeWarnings && !$typeMatch->typeMatch($entry,$ans) && - !($ans->{ignoreStrings} && $entry->class eq 'String')) { + if ($error->{flag} && $ans->{showEqualErrors}) { + my $message = $error->{message}; $message =~ s/\s+$//; + push(@errors,"<SMALL>There is a problem with your$nth $value:</SMALL>", + '<DIV STYLE="margin-left:1em">'.$message.'</DIV>'); + } elsif ($showTypeWarnings && !$typeMatch->typeMatch($entry,$ans) && + !($ans->{ignoreStrings} && $entry->class eq 'String')) { push(@errors,"Your$nth $answer isn't ".lc($class). " (it looks like ".lc($entry->showClass).")"); } elsif ($showHints && $m > 1) { @@ -605,23 +1039,9 @@ } # - # Give hints about extra or missing answsers + # Return the score and errors # - if ($showLengthHints) { - $value =~ s/ or /s or /; # fix "interval or union" - push(@errors,"There should be more ${value}s in your $ltype") - if ($score == $m && scalar(@correct) > 0); - push(@errors,"There should be fewer ${value}s in your $ltype") - if ($score < $maxscore && $score == $M && !$showHints); - } - - # - # Finalize the score - # - $score = 0 if ($score != $maxscore && !$partialCredit); - $ans->score($score/$maxscore); - push(@errors,"Score = $ans->{score}") if $ans->{debug}; - $ans->{error_message} = $ans->{ans_message} = join("\n",@errors); + return ($score,@errors); } # @@ -666,19 +1086,24 @@ return ( Value::Union::cmp_defaults($self,@_), typeMatch => Value::Formula->new("(1,2]"), + showDomainErrors => 1, ) if $self->type eq 'Union'; my $type = $self->type; $type = ($self->isComplex)? 'Complex': 'Real' if $type eq 'Number'; $type = 'Value::'.$type.'::'; - return (&{$type.'cmp_defaults'}($self,@_), upToConstant => 0) - if defined(%$type) && $self->type ne 'List'; + return ( + &{$type.'cmp_defaults'}($self,@_), + upToConstant => 0, + showDomainErrors => 1, + ) if defined(%$type) && $self->type ne 'List'; return ( Value::List::cmp_defaults($self,@_), removeParens => $self->{autoFormula}, typeMatch => Value::Formula->new(($self->createRandomPoints(1))[1]->[0]{data}[0]), + showDomainErrors => 1, ); } @@ -703,7 +1128,8 @@ my $cmp = $self->SUPER::cmp(@_); if ($cmp->{rh_ans}{removeParens} && $self->type eq 'List') { $self->{tree}{open} = $self->{tree}{close} = ''; - $cmp->ans_hash(correct_ans => $self->stringify); + $cmp->ans_hash(correct_ans => $self->stringify) + unless defined($self->{correct_ans}); } if ($cmp->{rh_ans}{eval} && $self->isConstant) { $cmp->ans_hash(correct_value => $self->eval); @@ -716,7 +1142,11 @@ $context->{_variables}->{pattern} = $context->{_variables}->{namePattern} = 'C0|' . $context->{_variables}->{pattern}; $context->update; $context->variables->add('C0' => 'Parameter'); - $cmp->ans_hash(correct_value => Value::Formula->new('C0')+$self); + my $f = Value::Formula->new('C0')+$self; + for ('limits','test_points','test_values','num_points','granularity','resolution', + 'checkUndefinedPoints','max_undefined') + {$f->{$_} = $self->{$_} if defined($self->{$_})} + $cmp->ans_hash(correct_value => $f); Parser::Context->current(undef,$current); } return $cmp; @@ -745,14 +1175,106 @@ sub cmp_postprocess { my $self = shift; my $ans = shift; return unless $ans->{score} == 0 && !$ans->{isPreview}; - return if $ans->{ans_message} || !$ans->{showDimensionHints}; + return if $ans->{ans_message}; + if ($self->{domainMismatch} && $ans->{showDomainErrors}) { + $self->cmp_Error($ans,"The domain of your function doesn't match that of the correct answer"); + return; + } + return if !$ans->{showDimensionHints}; my $other = $ans->{student_value}; + return if $ans->{ignoreStrings} && (!Value::isValue($other) || $other->type eq 'String'); return unless $other->type =~ m/^(Point|Vector|Matrix)$/; return unless $self->type =~ m/^(Point|Vector|Matrix)$/; return if P... [truncated message content] |