[system] / trunk / pg / lib / Value / AnswerChecker.pm Repository:
ViewVC logotype

Annotation of /trunk/pg/lib/Value/AnswerChecker.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 4143 - (view) (download) (as text)

1 : dpvc 2593 #############################################################
2 :     #
3 :     # Implements the ->cmp method for Value objects. This produces
4 :     # an answer checker appropriate for the type of object.
5 :     # Additional options can be passed to the checker to
6 :     # modify its action.
7 :     #
8 :     # The individual Value packages are modified below to add the
9 :     # needed methods.
10 :     #
11 :    
12 :     #############################################################
13 :    
14 :     package Value;
15 :    
16 :     #
17 : dpvc 3589 # Context can add default values to the answer checkers by class;
18 :     #
19 :     $Value::defaultContext->{cmpDefaults} = {};
20 :    
21 :    
22 :     #
23 : dpvc 3629 # Default flags for the answer checkers
24 : dpvc 2593 #
25 : dpvc 2609 sub cmp_defaults {(
26 : dpvc 2593 showTypeWarnings => 1,
27 : dpvc 2627 showEqualErrors => 1,
28 :     ignoreStrings => 1,
29 : dpvc 3497 studentsMustReduceUnions => 1,
30 :     showUnionReduceWarnings => 1,
31 : dpvc 2621 )}
32 : dpvc 2593
33 : dpvc 3629 #
34 : dpvc 3703 # Special Context flags to be set for the student answer
35 :     #
36 :     sub cmp_contextFlags {
37 :     my $self = shift; my $ans = shift;
38 :     return (
39 :     StringifyAsTeX => 0, # reset this, just in case.
40 :     no_parameters => 1, # don't let students enter parameters
41 :     showExtraParens => 1, # make student answer painfully unambiguous
42 :     reduceConstants => 0, # don't combine student constants
43 :     reduceConstantFunctions => 0, # don't reduce constant functions
44 :     ($ans->{studentsMustReduceUnions} ?
45 :     (reduceUnions => 0, reduceSets => 0,
46 :     reduceUnionsForComparison => $ans->{showUnionReduceWarnings},
47 :     reduceSetsForComparison => $ans->{showUnionReduceWarnings}) :
48 :     (reduceUnions => 1, reduceSets => 1,
49 :     reduceUnionsForComparison => 1, reduceSetsForComparison => 1)),
50 :     ($ans->{requireParenMatch}? (): ignoreEndpointTypes => 1), # for Intervals
51 :     );
52 :     }
53 :    
54 :    
55 :     #
56 : dpvc 3629 # Create an answer checker for the given type of object
57 :     #
58 : dpvc 2593 sub cmp {
59 :     my $self = shift;
60 :     my $ans = new AnswerEvaluator;
61 : dpvc 3269 my $correct = protectHTML($self->{correct_ans});
62 :     $correct = $self->correct_ans unless defined($correct);
63 : dpvc 3589 $self->{context} = $$Value::context unless defined($self->{context});
64 : dpvc 2593 $ans->ans_hash(
65 :     type => "Value (".$self->class.")",
66 : dpvc 3269 correct_ans => $correct,
67 : dpvc 2593 correct_value => $self,
68 : dpvc 3206 $self->cmp_defaults(@_),
69 : dpvc 3589 %{$self->{context}{cmpDefaults}{$self->class} || {}}, # context-specified defaults
70 : dpvc 2593 @_
71 :     );
72 : dpvc 3713 $ans->{debug} = $ans->{rh_ans}{debug};
73 : dpvc 2648 $ans->install_evaluator(sub {$ans = shift; $ans->{correct_value}->cmp_parse($ans)});
74 : dpvc 3269 $ans->install_pre_filter('erase') if $self->{ans_name}; # don't do blank check if answer_array
75 : dpvc 3715 $self->cmp_diagnostics($ans);
76 : dpvc 2593 return $ans;
77 :     }
78 :    
79 : dpvc 3269 sub correct_ans {protectHTML(shift->string)}
80 : dpvc 3715 sub cmp_diagnostics {}
81 : dpvc 3269
82 : dpvc 2593 #
83 :     # Parse the student answer and compute its value,
84 :     # produce the preview strings, and then compare the
85 :     # student and professor's answers for equality.
86 :     #
87 : dpvc 2648 sub cmp_parse {
88 : dpvc 2593 my $self = shift; my $ans = shift;
89 : dpvc 2599 #
90 : dpvc 2648 # Do some setup
91 : dpvc 2599 #
92 : dpvc 2688 my $current = $$Value::context; # save it for later
93 : dpvc 2692 my $context = $ans->{correct_value}{context} || $current;
94 : dpvc 2688 Parser::Context->current(undef,$context); # change to correct answser's context
95 : dpvc 3703 my $flags = contextSet($context,$self->cmp_contextFlags($ans)); # save old context flags
96 : dpvc 3493 my $inputs = $self->getPG('$inputs_ref',{action=>""});
97 :     $ans->{isPreview} = $inputs->{previewAnswers} || ($inputs->{action} =~ m/^Preview/);
98 : dpvc 2648 $ans->{cmp_class} = $self->cmp_class($ans) unless $ans->{cmp_class};
99 : dpvc 2916 $ans->{error_message} = $ans->{ans_message} = ''; # clear any old messages
100 :     $ans->{preview_latex_string} = $ans->{preview_text_string} = '';
101 : dpvc 2648
102 : dpvc 2599 #
103 :     # Parse and evaluate the student answer
104 :     #
105 : dpvc 2593 $ans->score(0); # assume failure
106 : dpvc 2621 $ans->{student_value} = $ans->{student_formula} = Parser::Formula($ans->{student_ans});
107 :     $ans->{student_value} = Parser::Evaluate($ans->{student_formula})
108 : dpvc 2624 if defined($ans->{student_formula}) && $ans->{student_formula}->isConstant;
109 : dpvc 2648
110 : dpvc 2599 #
111 :     # If it parsed OK, save the output forms and check if it is correct
112 :     # otherwise report an error
113 :     #
114 : dpvc 2593 if (defined $ans->{student_value}) {
115 :     $ans->{student_value} = Value::Formula->new($ans->{student_value})
116 :     unless Value::isValue($ans->{student_value});
117 :     $ans->{preview_latex_string} = $ans->{student_formula}->TeX;
118 : dpvc 2648 $ans->{preview_text_string} = protectHTML($ans->{student_formula}->string);
119 : dpvc 3703 #
120 :     # Get the string for the student answer
121 :     #
122 :     for ($ans->{formatStudentAnswer} || $context->flag('formatStudentAnswer')) {
123 :     /evaluated/i and do {$ans->{student_ans} = protectHTML($ans->{student_value}->string); last};
124 :     /parsed/i and do {$ans->{student_ans} = $ans->{preview_text_string}; last};
125 :     /reduced/i and do {
126 :     my $oldFlags = contextSet($context,reduceConstants=>1,reduceConstantFunctions=>0);
127 :     $ans->{student_ans} = protectHTML($ans->{student_formula}->substitute()->string);
128 :     contextSet($context,%{$oldFags}); last;
129 :     };
130 :     warn "Unkown student answer format |$ans->{formatStudentAnswer}|";
131 :     }
132 : dpvc 3269 if ($self->cmp_collect($ans)) {
133 :     $self->cmp_equal($ans);
134 : dpvc 4093 $self->cmp_postprocess($ans) if !$ans->{error_message} && !$ans->{typeError};
135 : dpvc 3715 $self->cmp_diagnostics($ans);
136 : dpvc 3269 }
137 : dpvc 2593 } else {
138 : dpvc 3629 $self->cmp_collect($ans);
139 : dpvc 2648 $self->cmp_error($ans);
140 : dpvc 2593 }
141 : dpvc 2791 contextSet($context,%{$flags}); # restore context values
142 : dpvc 2688 Parser::Context->current(undef,$current); # put back the old context
143 : dpvc 2593 return $ans;
144 :     }
145 :    
146 :     #
147 : dpvc 3269 # Check if the object has an answer array and collect the results
148 :     # Build the combined student answer and set the preview values
149 :     #
150 :     sub cmp_collect {
151 :     my $self = shift; my $ans = shift;
152 :     return 1 unless $self->{ans_name};
153 :     $ans->{preview_latex_string} = $ans->{preview_text_string} = "";
154 :     my $OK = $self->ans_collect($ans);
155 :     $ans->{student_ans} = $self->format_matrix($ans->{student_formula},@{$self->{format_options}},tth_delims=>1);
156 :     return 0 unless $OK;
157 :     my $array = $ans->{student_formula};
158 :     if ($self->{ColumnVector}) {
159 :     my @V = (); foreach my $x (@{$array}) {push(@V,$x->[0])}
160 :     $array = [@V];
161 :     } elsif (scalar(@{$array}) == 1) {$array = $array->[0]}
162 :     my $type = $self;
163 :     $type = "Value::".$self->{tree}->type if $self->class eq 'Formula';
164 :     $ans->{student_formula} = eval {$type->new($array)->with(ColumnVector=>$self->{ColumnVector})};
165 :     if (!defined($ans->{student_formula}) || $$Value::context->{error}{flag})
166 : dpvc 3629 {Parser::reportEvalError($@); $self->cmp_error($ans); return 0}
167 : dpvc 3269 $ans->{student_value} = $ans->{student_formula};
168 :     $ans->{preview_text_string} = $ans->{student_ans};
169 :     $ans->{preview_latex_string} = $ans->{student_formula}->TeX;
170 :     if (Value::isFormula($ans->{student_formula}) && $ans->{student_formula}->isConstant) {
171 :     $ans->{student_value} = Parser::Evaluate($ans->{student_formula});
172 :     return 0 unless $ans->{student_value};
173 :     }
174 :     return 1;
175 :     }
176 :    
177 :     #
178 : dpvc 2593 # Check if the parsed student answer equals the professor's answer
179 :     #
180 :     sub cmp_equal {
181 :     my $self = shift; my $ans = shift;
182 : dpvc 2627 my $correct = $ans->{correct_value};
183 :     my $student = $ans->{student_value};
184 :     if ($correct->typeMatch($student,$ans)) {
185 : dpvc 3206 my $equal = $correct->cmp_compare($student,$ans);
186 : dpvc 2594 if (defined($equal) || !$ans->{showEqualErrors}) {$ans->score(1) if $equal; return}
187 : dpvc 2648 $self->cmp_error($ans);
188 : dpvc 2593 } else {
189 : dpvc 2627 return if $ans->{ignoreStrings} && (!Value::isValue($student) || $student->type eq 'String');
190 : dpvc 4093 $ans->{typeError} = 1;
191 : dpvc 2593 $ans->{ans_message} = $ans->{error_message} =
192 : dpvc 3336 "Your answer isn't ".lc($ans->{cmp_class})."\n".
193 : dpvc 3269 "(it looks like ".lc($student->showClass).")"
194 : dpvc 2593 if !$ans->{isPreview} && $ans->{showTypeWarnings} && !$ans->{error_message};
195 :     }
196 :     }
197 :    
198 :     #
199 : dpvc 3206 # Perform the comparison, either using the checker supplied
200 :     # by the answer evaluator, or the overloaded == operator.
201 :     #
202 :    
203 : dpvc 3504 our $CMP_ERROR = 2; # a fatal error was detected
204 :     our $CMP_WARNING = 3; # a warning was produced
205 : dpvc 3206
206 :     sub cmp_compare {
207 : dpvc 3504 my $self = shift; my $other = shift; my $ans = shift; my $nth = shift || '';
208 : dpvc 3754 return eval {$self == $other} unless ref($ans->{checker}) eq 'CODE';
209 :     my $equal = eval {&{$ans->{checker}}($self,$other,$ans,$nth,@_)};
210 :     if (!defined($equal) && $@ ne '' && (!$$Value::context->{error}{flag} || $ans->{showAllErrors})) {
211 :     $$Value::context->setError(["<I>An error occurred while checking your$nth answer:</I>\n".
212 :     '<DIV STYLE="margin-left:1em">%s</DIV>',$@],'',undef,undef,$CMP_ERROR);
213 :     warn "Please inform your instructor that an error occurred while checking your answer";
214 : dpvc 3206 }
215 :     return $equal;
216 :     }
217 :    
218 :     sub cmp_list_compare {Value::List::cmp_list_compare(@_)}
219 :    
220 :     #
221 : dpvc 2593 # Check if types are compatible for equality check
222 :     #
223 :     sub typeMatch {
224 : dpvc 2600 my $self = shift; my $other = shift;
225 :     return 1 unless ref($other);
226 : dpvc 2621 $self->type eq $other->type && $other->class ne 'Formula';
227 : dpvc 2593 }
228 :    
229 :     #
230 : dpvc 2605 # Class name for cmp error messages
231 :     #
232 : dpvc 2609 sub cmp_class {
233 :     my $self = shift; my $ans = shift;
234 : dpvc 2624 my $class = $self->showClass; $class =~ s/Real //;
235 :     return $class if $class =~ m/Formula/;
236 : dpvc 3516 return "an Interval, Set or Union" if $self->isSetOfReals;
237 : dpvc 2609 return $class;
238 :     }
239 : dpvc 2605
240 :     #
241 : dpvc 2593 # Student answer evaluation failed.
242 :     # Report the error, with formatting, if possible.
243 :     #
244 :     sub cmp_error {
245 :     my $self = shift; my $ans = shift;
246 : dpvc 3206 my $error = $$Value::context->{error};
247 :     my $message = $error->{message};
248 :     if ($error->{pos}) {
249 :     my $string = $error->{string};
250 :     my ($s,$e) = @{$error->{pos}};
251 : dpvc 2593 $message =~ s/; see.*//; # remove the position from the message
252 :     $ans->{student_ans} =
253 :     protectHTML(substr($string,0,$s)) .
254 :     '<SPAN CLASS="parsehilight">' .
255 :     protectHTML(substr($string,$s,$e-$s)) .
256 :     '</SPAN>' .
257 :     protectHTML(substr($string,$e));
258 :     }
259 : dpvc 2601 $self->cmp_Error($ans,$message);
260 :     }
261 :    
262 :     #
263 :     # Set the error message
264 :     #
265 :     sub cmp_Error {
266 :     my $self = shift; my $ans = shift;
267 :     return unless scalar(@_) > 0;
268 : dpvc 2599 $ans->score(0);
269 : dpvc 2601 $ans->{ans_message} = $ans->{error_message} = join("\n",@_);
270 : dpvc 2593 }
271 :    
272 :     #
273 : dpvc 2601 # filled in by sub-classes
274 :     #
275 :     sub cmp_postprocess {}
276 :    
277 :     #
278 : dpvc 3504 # Check for unreduced reduced Unions and Sets
279 : dpvc 3497 #
280 :     sub cmp_checkUnionReduce {
281 : dpvc 3504 my $self = shift; my $student = shift; my $ans = shift; my $nth = shift || '';
282 : dpvc 3497 return unless $ans->{studentsMustReduceUnions} &&
283 :     $ans->{showUnionReduceWarnings} &&
284 : dpvc 3504 !$ans->{isPreview} && !Value::isFormula($student);
285 : dpvc 3497 if ($student->type eq 'Union' && $student->length >= 2) {
286 :     my $reduced = $student->reduce;
287 : dpvc 3630 return "Your$nth union can be written without overlaps"
288 : dpvc 3497 unless $reduced->type eq 'Union' && $reduced->length == $student->length;
289 : dpvc 3505 my @R = $reduced->sort->value;
290 :     my @S = $student->sort->value;
291 : dpvc 3497 foreach my $i (0..$#R) {
292 : dpvc 3630 return "Your$nth union can be written without overlaps"
293 : dpvc 3507 unless $R[$i] == $S[$i] && $R[$i]->length == $S[$i]->length;
294 : dpvc 3497 }
295 : dpvc 3507 } elsif ($student->type eq 'Set' && $student->length >= 2) {
296 : dpvc 3516 return "Your$nth set should have no repeated elements"
297 : dpvc 3507 unless $student->reduce->length == $student->length;
298 : dpvc 3497 }
299 :     return;
300 :     }
301 :    
302 :     #
303 : dpvc 3269 # create answer rules of various types
304 :     #
305 :     sub ans_rule {shift; pgCall('ans_rule',@_)}
306 :     sub named_ans_rule {shift; pgCall('NAMED_ANS_RULE',@_)}
307 :     sub named_ans_rule_extension {shift; pgCall('NAMED_ANS_RULE_EXTENSION',@_)}
308 :     sub ans_array {shift->ans_rule(@_)};
309 :     sub named_ans_array {shift->named_ans_rule(@_)};
310 :     sub named_ans_array_extension {shift->named_ans_rule_extension(@_)};
311 :    
312 :     sub pgCall {my $call = shift; &{WeBWorK::PG::Translator::PG_restricted_eval('\&'.$call)}(@_)}
313 :     sub pgRef {WeBWorK::PG::Translator::PG_restricted_eval('\&'.shift)}
314 :    
315 :     our $answerPrefix = "MaTrIx";
316 :    
317 :     #
318 :     # Lay out a matrix of answer rules
319 :     #
320 :     sub ans_matrix {
321 :     my $self = shift;
322 :     my ($extend,$name,$rows,$cols,$size,$open,$close,$sep) = @_;
323 :     my $named_extension = pgRef('NAMED_ANS_RULE_EXTENSION');
324 :     my $new_name = pgRef('RECORD_FORM_LABEL');
325 :     my $HTML = ""; my $ename = $name;
326 :     if ($name eq '') {
327 :     my $n = pgCall('inc_ans_rule_count');
328 :     $name = pgCall('NEW_ANS_NAME',$n);
329 :     $ename = $answerPrefix.$n;
330 :     }
331 :     $self->{ans_name} = $ename;
332 :     $self->{ans_rows} = $rows;
333 :     $self->{ans_cols} = $cols;
334 :     my @array = ();
335 :     foreach my $i (0..$rows-1) {
336 :     my @row = ();
337 :     foreach my $j (0..$cols-1) {
338 :     if ($i == 0 && $j == 0) {
339 :     if ($extend) {push(@row,&$named_extension(&$new_name($name),$size))}
340 :     else {push(@row,pgCall('NAMED_ANS_RULE',$name,$size))}
341 :     } else {
342 :     push(@row,&$named_extension(&$new_name(ANS_NAME($ename,$i,$j)),$size));
343 :     }
344 :     }
345 :     push(@array,[@row]);
346 :     }
347 :     $self->format_matrix([@array],open=>$open,close=>$close,sep=>$sep);
348 :     }
349 :    
350 :     sub ANS_NAME {
351 :     my ($name,$i,$j) = @_;
352 :     $name.'_'.$i.'_'.$j;
353 :     }
354 :    
355 :    
356 :     #
357 :     # Lay out an arbitrary matrix
358 :     #
359 :     sub format_matrix {
360 :     my $self = shift;
361 :     my $displayMode = $self->getPG('$displayMode');
362 :     return $self->format_matrix_tex(@_) if ($displayMode eq 'TeX');
363 :     return $self->format_matrix_HTML(@_);
364 :     }
365 :    
366 :     sub format_matrix_tex {
367 :     my $self = shift; my $array = shift;
368 : dpvc 3273 my %options = (open=>'.',close=>'.',sep=>'',@_);
369 : dpvc 3269 $self->{format_options} = [%options] unless $self->{format_options};
370 :     my ($open,$close,$sep) = ($options{open},$options{close},$options{sep});
371 :     my ($rows,$cols) = (scalar(@{$array}),scalar(@{$array->[0]}));
372 :     my $tex = "";
373 : dpvc 3273 $open = '\\'.$open if $open =~ m/[{}]/; $close = '\\'.$close if $close =~ m/[{}]/;
374 :     $tex .= '\(\left'.$open;
375 :     $tex .= '\setlength{\arraycolsep}{2pt}', $sep = '\,'.$sep if $sep;
376 :     $tex .= '\begin{array}{'.('c'x$cols).'}';
377 :     foreach my $i (0..$rows-1) {$tex .= join($sep.'&',@{$array->[$i]}).'\cr'."\n"}
378 :     $tex .= '\end{array}\right'.$close.'\)';
379 : dpvc 3269 return $tex;
380 :     }
381 :    
382 :     sub format_matrix_HTML {
383 :     my $self = shift; my $array = shift;
384 :     my %options = (open=>'',close=>'',sep=>'',tth_delims=>0,@_);
385 :     $self->{format_options} = [%options] unless $self->{format_options};
386 :     my ($open,$close,$sep) = ($options{open},$options{close},$options{sep});
387 :     my ($rows,$cols) = (scalar(@{$array}),scalar(@{$array->[0]}));
388 :     my $HTML = "";
389 :     if ($sep) {$sep = '</TD><TD STYLE="padding: 0px 1px">'.$sep.'</TD><TD>'}
390 :     else {$sep = '</TD><TD WIDTH="8px"></TD><TD>'}
391 :     foreach my $i (0..$rows-1) {
392 :     $HTML .= '<TR><TD HEIGHT="6px"></TD></TR>' if $i;
393 : dpvc 3631 $HTML .= '<TR ALIGN="MIDDLE"><TD>'.join($sep,EVALUATE(@{$array->[$i]})).'</TD></TR>'."\n";
394 : dpvc 3269 }
395 :     $open = $self->format_delimiter($open,$rows,$options{tth_delims});
396 :     $close = $self->format_delimiter($close,$rows,$options{tth_delims});
397 :     if ($open ne '' || $close ne '') {
398 :     $HTML = '<TR ALIGN="MIDDLE">'
399 :     . '<TD>'.$open.'</TD>'
400 :     . '<TD WIDTH="2"></TD>'
401 :     . '<TD><TABLE BORDER="0" CELLSPACING="0" CELLPADDING="0" CLASS="ArrayLayout">'
402 :     . $HTML
403 :     . '</TABLE></TD>'
404 :     . '<TD WIDTH="4"></TD>'
405 :     . '<TD>'.$close.'</TD>'
406 :     . '</TR>'."\n";
407 :     }
408 :     return '<TABLE BORDER="0" CELLSPACING="0" CELLPADDING="0" CLASS="ArrayLayout"'
409 :     . ' STYLE="display:inline;vertical-align:-'.(1.1*$rows-.6).'em">'
410 :     . $HTML
411 :     . '</TABLE>';
412 :     }
413 :    
414 : dpvc 3631 sub EVALUATE {map {(Value::isFormula($_) && $_->isConstant? $_->eval: $_)} @_}
415 :    
416 : dpvc 3273 sub VERBATIM {
417 :     my $string = shift;
418 :     my $displayMode = Value->getPG('$displayMode');
419 :     $string = '\end{verbatim}'.$string.'\begin{verbatim}' if $displayMode eq 'TeX';
420 :     return $string;
421 :     }
422 :    
423 : dpvc 3269 #
424 :     # Create a tall delimiter to match the line height
425 :     #
426 :     sub format_delimiter {
427 :     my $self = shift; my $delim = shift; my $rows = shift; my $tth = shift;
428 :     return '' if $delim eq '' || $delim eq '.';
429 :     my $displayMode = $self->getPG('$displayMode');
430 :     return $self->format_delimiter_tth($delim,$rows,$tth)
431 :     if $tth || $displayMode eq 'HTML_tth' || $displayMode !~ m/^HTML_/;
432 :     my $rule = '\vrule width 0pt height '.(.8*$rows).'em depth 0pt';
433 :     $rule = '\rule 0pt '.(.8*$rows).'em 0pt' if $displayMode eq 'HTML_jsMath';
434 :     $delim = '\\'.$delim if $delim eq '{' || $delim eq '}';
435 :     return '\(\left'.$delim.$rule.'\right.\)';
436 :     }
437 :    
438 :     #
439 :     # Data for tth delimiters [top,mid,bot,rep]
440 :     #
441 :     my %tth_delim = (
442 :     '[' => ['&#xF8EE;','','&#xF8F0;','&#xF8EF;'],
443 :     ']' => ['&#xF8F9;','','&#xF8FB;','&#xF8FA;'],
444 :     '(' => ['&#xF8EB;','','&#xF8ED;','&#xF8EC;'],
445 :     ')' => ['&#xF8F6;','','&#xF8F8;','&#xF8F7;'],
446 :     '{' => ['&#xF8F1;','&#xF8F2;','&#xF8F3;','&#xF8F4;'],
447 :     '}' => ['&#xF8FC;','&#xF8FD;','&#xF8FE;','&#xF8F4;'],
448 :     '|' => ['|','','|','|'],
449 :     '<' => ['&lt;'],
450 :     '>' => ['&gt;'],
451 :     '\lgroup' => ['&#xF8F1;','','&#xF8F3;','&#xF8F4;'],
452 :     '\rgroup' => ['&#xF8FC;','','&#xF8FE;','&#xF8F4;'],
453 :     );
454 :    
455 :     #
456 :     # Make delimiters as stacks of characters
457 :     #
458 :     sub format_delimiter_tth {
459 :     my $self = shift;
460 :     my $delim = shift; my $rows = shift; my $tth = shift;
461 :     return '' if $delim eq '' || !defined($tth_delim{$delim});
462 :     my $c = $delim; $delim = $tth_delim{$delim};
463 :     $c = $delim->[0] if scalar(@{$delim}) == 1;
464 :     my $size = ($tth? "": "font-size:175%; ");
465 :     return '<SPAN STYLE="'.$size.'margin:0px 2px">'.$c.'</SPAN>'
466 :     if $rows == 1 || scalar(@{$delim}) == 1;
467 :     my $HTML = "";
468 :     if ($delim->[1] eq '') {
469 :     $HTML = join('<BR>',$delim->[0],($delim->[3])x(2*($rows-1)),$delim->[2]);
470 :     } else {
471 :     $HTML = join('<BR>',$delim->[0],($delim->[3])x($rows-1),
472 :     $delim->[1],($delim->[3])x($rows-1),
473 :     $delim->[2]);
474 :     }
475 :     return '<DIV STYLE="line-height:90%; margin: 0px 2px">'.$HTML.'</DIV>';
476 :     }
477 :    
478 :    
479 :     #
480 :     # Look up the values of the answer array entries, and check them
481 :     # for syntax and other errors. Build the student answer
482 :     # based on these, and keep track of error messages.
483 :     #
484 :    
485 : dpvc 3629 my @ans_cmp_defaults = (showCoodinateHints => 0, checker => sub {0});
486 : dpvc 3269
487 :     sub ans_collect {
488 :     my $self = shift; my $ans = shift;
489 :     my $inputs = $self->getPG('$inputs_ref');
490 :     my $blank = ($self->getPG('$displayMode') eq 'TeX') ? '\_\_' : '__';
491 :     my ($rows,$cols) = ($self->{ans_rows},$self->{ans_cols});
492 :     my @array = (); my $data = [$self->value]; my $errors = []; my $OK = 1;
493 :     if ($self->{ColumnVector}) {foreach my $x (@{$data}) {$x = [$x]}}
494 :     $data = [$data] unless ref($data->[0]) eq 'ARRAY';
495 :     foreach my $i (0..$rows-1) {
496 : dpvc 3629 my @row = (); my $entry;
497 : dpvc 3269 foreach my $j (0..$cols-1) {
498 :     if ($i || $j) {
499 : dpvc 3629 $entry = $inputs->{ANS_NAME($self->{ans_name},$i,$j)};
500 : dpvc 3269 } else {
501 : dpvc 3629 $entry = $ans->{original_student_ans};
502 :     $ans->{student_formula} = $ans->{student_value} = undef unless $entry =~ m/\S/;
503 : dpvc 3269 }
504 : dpvc 3629 my $result = $data->[$i][$j]->cmp(@ans_cmp_defaults)->evaluate($entry);
505 :     $OK &= entryCheck($result,$blank);
506 :     push(@row,$result->{student_formula});
507 :     entryMessage($result->{ans_message},$errors,$i,$j,$rows,$cols);
508 : dpvc 3269 }
509 :     push(@array,[@row]);
510 :     }
511 :     $ans->{student_formula} = [@array];
512 : dpvc 3629 $ans->{ans_message} = $ans->{error_message} = "";
513 :     if (scalar(@{$errors})) {
514 :     $ans->{ans_message} = $ans->{error_message} =
515 :     '<TABLE BORDER="0" CELLSPACING="0" CELLPADDING="0" CLASS="ArrayLayout">'.
516 :     join('<TR><TD HEIGHT="4"></TD></TR>',@{$errors}).
517 :     '</TABLE>';
518 :     $OK = 0;
519 :     }
520 :     return $OK;
521 : dpvc 3269 }
522 :    
523 :     sub entryMessage {
524 :     my $message = shift; return unless $message;
525 : dpvc 3629 my ($errors,$i,$j,$rows,$cols) = @_; $i++; $j++;
526 :     my $title;
527 :     if ($rows == 1) {$title = "In entry $j"}
528 :     elsif ($cols == 1) {$title = "In entry $i"}
529 :     else {$title = "In entry ($i,$j)"}
530 : dpvc 3633 push(@{$errors},"<TR VALIGN=\"TOP\"><TD NOWRAP STYLE=\"text-align:right; border:0px\"><I>$title</I>:&nbsp;</TD>".
531 : dpvc 3629 "<TD STYLE=\"text-align:left; border:0px\">$message</TD></TR>");
532 : dpvc 3269 }
533 :    
534 :     sub entryCheck {
535 :     my $ans = shift; my $blank = shift;
536 :     return 1 if defined($ans->{student_value});
537 :     if (!defined($ans->{student_formula})) {
538 :     $ans->{student_formula} = $ans->{student_ans};
539 :     $ans->{student_formula} = $blank unless $ans->{student_formula};
540 :     }
541 :     return 0
542 :     }
543 :    
544 :    
545 :     #
546 : dpvc 2791 # Get and Set values in context
547 :     #
548 :     sub contextSet {
549 :     my $context = shift; my %set = (@_);
550 :     my $flags = $context->{flags}; my $get = {};
551 :     foreach my $id (keys %set) {$get->{$id} = $flags->{$id}; $flags->{$id} = $set{$id}}
552 :     return $get;
553 :     }
554 :    
555 :     #
556 : dpvc 2593 # Quote HTML characters
557 :     #
558 :     sub protectHTML {
559 :     my $string = shift;
560 : dpvc 3488 return unless defined($string);
561 : dpvc 2661 return $string if eval ('$main::displayMode') eq 'TeX';
562 : dpvc 2593 $string =~ s/&/\&amp;/g;
563 :     $string =~ s/</\&lt;/g;
564 :     $string =~ s/>/\&gt;/g;
565 :     $string;
566 :     }
567 :    
568 : dpvc 2599 #
569 : dpvc 2601 # names for numbers
570 :     #
571 :     sub NameForNumber {
572 :     my $self = shift; my $n = shift;
573 :     my $name = ('zeroth','first','second','third','fourth','fifth',
574 :     'sixth','seventh','eighth','ninth','tenth')[$n];
575 :     $name = "$n-th" if ($n > 10);
576 :     return $name;
577 :     }
578 :    
579 :     #
580 : dpvc 2599 # Get a value from the safe compartment
581 :     #
582 :     sub getPG {
583 :     my $self = shift;
584 : dpvc 2664 # (WeBWorK::PG::Translator::PG_restricted_eval(shift))[0];
585 :     eval ('package main; '.shift); # faster
586 : dpvc 2599 }
587 :    
588 : dpvc 2593 #############################################################
589 :     #############################################################
590 :    
591 : dpvc 2596 package Value::Real;
592 :    
593 : dpvc 2609 sub cmp_defaults {(
594 : dpvc 3206 shift->SUPER::cmp_defaults(@_),
595 : dpvc 2605 ignoreInfinity => 1,
596 : dpvc 2621 )}
597 : dpvc 2597
598 : dpvc 2596 sub typeMatch {
599 :     my $self = shift; my $other = shift; my $ans = shift;
600 : dpvc 2600 return 1 unless ref($other);
601 : dpvc 2648 return 0 if Value::isFormula($other);
602 : dpvc 2605 return 1 if $other->type eq 'Infinity' && $ans->{ignoreInfinity};
603 : dpvc 2596 $self->type eq $other->type;
604 :     }
605 :    
606 :     #############################################################
607 :    
608 : dpvc 2605 package Value::Infinity;
609 :    
610 : dpvc 2609 sub cmp_class {'a Number'};
611 : dpvc 2605
612 :     sub typeMatch {
613 :     my $self = shift; my $other = shift; my $ans = shift;
614 :     return 1 unless ref($other);
615 : dpvc 2648 return 0 if Value::isFormula($other);
616 : dpvc 2605 return 1 if $other->type eq 'Number';
617 :     $self->type eq $other->type;
618 :     }
619 :    
620 :     #############################################################
621 :    
622 : dpvc 2609 package Value::String;
623 :    
624 :     sub cmp_defaults {(
625 : dpvc 3206 Value::Real->cmp_defaults(@_),
626 : dpvc 2621 typeMatch => 'Value::Real',
627 :     )}
628 : dpvc 2609
629 :     sub cmp_class {
630 : dpvc 2621 my $self = shift; my $ans = shift; my $typeMatch = $ans->{typeMatch};
631 : dpvc 2612 return 'a Word' if !Value::isValue($typeMatch) || $typeMatch->class eq 'String';
632 : dpvc 2609 return $typeMatch->cmp_class;
633 :     };
634 :    
635 :     sub typeMatch {
636 :     my $self = shift; my $other = shift; my $ans = shift;
637 : dpvc 3678 # return 0 if ref($other) && Value::isFormula($other);
638 : dpvc 2612 my $typeMatch = $ans->{typeMatch};
639 : dpvc 3872 return &$typeMatch($other,$ans) if ref($typeMatch) eq 'CODE';
640 : dpvc 2621 return 1 if !Value::isValue($typeMatch) || $typeMatch->class eq 'String' ||
641 :     $self->type eq $other->type;
642 : dpvc 2612 return $typeMatch->typeMatch($other,$ans);
643 : dpvc 2609 }
644 :    
645 : dpvc 3652 #
646 :     # Remove the blank-check prefilter when the string is empty,
647 :     # and add a filter that removes leading and trailing whitespace.
648 :     #
649 :     sub cmp {
650 :     my $self = shift;
651 :     my $cmp = $self->SUPER::cmp(@_);
652 :     if ($self->value =~ m/^\s*$/) {
653 :     $cmp->install_pre_filter('erase');
654 :     $cmp->install_pre_filter(sub {
655 :     my $ans = shift;
656 :     $ans->{student_ans} =~ s/^\s+//g;
657 :     $ans->{student_ans} =~ s/\s+$//g;
658 :     return $ans;
659 :     });
660 :     }
661 :     return $cmp;
662 :     }
663 :    
664 : dpvc 2609 #############################################################
665 :    
666 : dpvc 2593 package Value::Point;
667 :    
668 : dpvc 2609 sub cmp_defaults {(
669 : dpvc 3206 shift->SUPER::cmp_defaults(@_),
670 : dpvc 2601 showDimensionHints => 1,
671 :     showCoordinateHints => 1,
672 : dpvc 2621 )}
673 : dpvc 2593
674 :     sub typeMatch {
675 :     my $self = shift; my $other = shift; my $ans = shift;
676 : dpvc 2621 return ref($other) && $other->type eq 'Point' && $other->class ne 'Formula';
677 : dpvc 2601 }
678 :    
679 :     #
680 :     # Check for dimension mismatch and incorrect coordinates
681 :     #
682 :     sub cmp_postprocess {
683 :     my $self = shift; my $ans = shift;
684 :     return unless $ans->{score} == 0 && !$ans->{isPreview};
685 : dpvc 3205 my $student = $ans->{student_value};
686 :     return if $ans->{ignoreStrings} && (!Value::isValue($student) || $student->type eq 'String');
687 :     if ($ans->{showDimensionHints} && $self->length != $student->length) {
688 : dpvc 3269 $self->cmp_Error($ans,"The number of coordinates is incorrect"); return;
689 : dpvc 2593 }
690 : dpvc 2601 if ($ans->{showCoordinateHints}) {
691 :     my @errors;
692 :     foreach my $i (1..$self->length) {
693 :     push(@errors,"The ".$self->NameForNumber($i)." coordinate is incorrect")
694 : dpvc 3205 if ($self->{data}[$i-1] != $student->{data}[$i-1]);
695 : dpvc 2601 }
696 :     $self->cmp_Error($ans,@errors); return;
697 :     }
698 : dpvc 2593 }
699 :    
700 : dpvc 3269 sub correct_ans {
701 :     my $self = shift;
702 :     return $self->SUPER::correct_ans unless $self->{ans_name};
703 : dpvc 3273 Value::VERBATIM($self->format_matrix([[@{$self->{data}}]],@{$self->{format_options}},tth_delims=>1));
704 : dpvc 3269 }
705 :    
706 :     sub ANS_MATRIX {
707 :     my $self = shift;
708 :     my $extend = shift; my $name = shift;
709 :     my $size = shift || 5;
710 :     my $def = ($self->{context} || $$Value::context)->lists->get('Point');
711 :     my $open = $self->{open} || $def->{open}; my $close = $self->{close} || $def->{close};
712 :     $self->ans_matrix($extend,$name,1,$self->length,$size,$open,$close,',');
713 :     }
714 :    
715 :     sub ans_array {my $self = shift; $self->ANS_MATRIX(0,'',@_)}
716 :     sub named_ans_array {my $self = shift; $self->ANS_MATRIX(0,@_)}
717 :     sub named_ans_array_extension {my $self = shift; $self->ANS_MATRIX(1,@_)}
718 :    
719 : dpvc 2593 #############################################################
720 :    
721 :     package Value::Vector;
722 :    
723 : dpvc 2609 sub cmp_defaults {(
724 : dpvc 3206 shift->SUPER::cmp_defaults(@_),
725 : dpvc 2601 showDimensionHints => 1,
726 :     showCoordinateHints => 1,
727 : dpvc 2594 promotePoints => 0,
728 : dpvc 2597 parallel => 0,
729 :     sameDirection => 0,
730 : dpvc 2621 )}
731 : dpvc 2593
732 :     sub typeMatch {
733 :     my $self = shift; my $other = shift; my $ans = shift;
734 : dpvc 2621 return 0 unless ref($other) && $other->class ne 'Formula';
735 : dpvc 2627 return $other->type eq 'Vector' ||
736 :     ($ans->{promotePoints} && $other->type eq 'Point');
737 : dpvc 2593 }
738 :    
739 : dpvc 2597 #
740 : dpvc 2601 # check for dimension mismatch
741 :     # for parallel vectors, and
742 :     # for incorrect coordinates
743 : dpvc 2597 #
744 :     sub cmp_postprocess {
745 :     my $self = shift; my $ans = shift;
746 : dpvc 4093 return unless $ans->{score} == 0 && !$ans->{isPreview};
747 : dpvc 3205 my $student = $ans->{student_value};
748 :     return if $ans->{ignoreStrings} && (!Value::isValue($student) || $student->type eq 'String');
749 : dpvc 4093 if ($ans->{showDimensionHints} && $self->length != $student->length) {
750 : dpvc 3269 $self->cmp_Error($ans,"The number of coordinates is incorrect"); return;
751 : dpvc 2601 }
752 : dpvc 4143 if ($ans->{parallel} && $student->class ne 'Formula' && $student->class ne 'String' &&
753 : dpvc 3205 $self->isParallel($student,$ans->{sameDirection})) {
754 : dpvc 2900 $ans->score(1); return;
755 :     }
756 : dpvc 4093 if ($ans->{showCoordinateHints} && !$ans->{parallel}) {
757 : dpvc 2601 my @errors;
758 :     foreach my $i (1..$self->length) {
759 :     push(@errors,"The ".$self->NameForNumber($i)." coordinate is incorrect")
760 : dpvc 3205 if ($self->{data}[$i-1] != $student->{data}[$i-1]);
761 : dpvc 2601 }
762 :     $self->cmp_Error($ans,@errors); return;
763 :     }
764 : dpvc 2597 }
765 :    
766 : dpvc 3269 sub correct_ans {
767 :     my $self = shift;
768 :     return $self->SUPER::correct_ans unless $self->{ans_name};
769 : dpvc 3273 return Value::VERBATIM($self->format_matrix([[$self->value]],@{$self->{format_options}},tth_delims=>1))
770 : dpvc 3269 unless $self->{ColumnVector};
771 :     my @array = (); foreach my $x ($self->value) {push(@array,[$x])}
772 : dpvc 3273 return Value::VERBATIM($self->format_matrix([@array],@{$self->{format_options}},tth_delims=>1));
773 : dpvc 3269 }
774 : dpvc 2597
775 : dpvc 3269 sub ANS_MATRIX {
776 :     my $self = shift;
777 :     my $extend = shift; my $name = shift;
778 :     my $size = shift || 5; my ($def,$open,$close);
779 :     $def = ($self->{context} || $$Value::context)->lists->get('Matrix');
780 :     $open = $self->{open} || $def->{open}; $close = $self->{close} || $def->{close};
781 :     return $self->ans_matrix($extend,$name,$self->length,1,$size,$open,$close)
782 :     if ($self->{ColumnVector});
783 :     $def = ($self->{context} || $$Value::context)->lists->get('Vector');
784 :     $open = $self->{open} || $def->{open}; $close = $self->{close} || $def->{close};
785 :     $self->ans_matrix($extend,$name,1,$self->length,$size,$open,$close,',');
786 :     }
787 : dpvc 2597
788 : dpvc 3269 sub ans_array {my $self = shift; $self->ANS_MATRIX(0,'',@_)}
789 :     sub named_ans_array {my $self = shift; $self->ANS_MATRIX(0,@_)}
790 :     sub named_ans_array_extension {my $self = shift; $self->ANS_MATRIX(1,@_)}
791 :    
792 :    
793 : dpvc 2593 #############################################################
794 :    
795 :     package Value::Matrix;
796 :    
797 : dpvc 2609 sub cmp_defaults {(
798 : dpvc 3206 shift->SUPER::cmp_defaults(@_),
799 : dpvc 2601 showDimensionHints => 1,
800 :     showEqualErrors => 0,
801 : dpvc 2621 )}
802 : dpvc 2593
803 :     sub typeMatch {
804 :     my $self = shift; my $other = shift; my $ans = shift;
805 : dpvc 2621 return 0 unless ref($other) && $other->class ne 'Formula';
806 : dpvc 2627 return $other->type eq 'Matrix' ||
807 :     ($other->type =~ m/^(Point|list)$/ &&
808 :     $other->{open}.$other->{close} eq $self->{open}.$self->{close});
809 : dpvc 2601 }
810 :    
811 :     sub cmp_postprocess {
812 :     my $self = shift; my $ans = shift;
813 :     return unless $ans->{score} == 0 &&
814 :     !$ans->{isPreview} && $ans->{showDimensionHints};
815 : dpvc 3205 my $student = $ans->{student_value};
816 :     return if $ans->{ignoreStrings} && (!Value::isValue($student) || $student->type eq 'String');
817 :     my @d1 = $self->dimensions; my @d2 = $student->dimensions;
818 : dpvc 2593 if (scalar(@d1) != scalar(@d2)) {
819 : dpvc 2601 $self->cmp_Error($ans,"Matrix dimension is not correct");
820 :     return;
821 : dpvc 2593 } else {
822 :     foreach my $i (0..scalar(@d1)-1) {
823 :     if ($d1[$i] != $d2[$i]) {
824 : dpvc 2601 $self->cmp_Error($ans,"Matrix dimension is not correct");
825 :     return;
826 : dpvc 2593 }
827 :     }
828 :     }
829 :     }
830 :    
831 : dpvc 3269 sub correct_ans {
832 :     my $self = shift;
833 :     return $self->SUPER::correct_ans unless $self->{ans_name};
834 :     my @array = $self->value; @array = ([@array]) if $self->isRow;
835 : dpvc 3273 Value::VERBATIM($self->format_matrix([$self->value],@{$self->{format_options}},tth_delims=>1));
836 : dpvc 3269 }
837 :    
838 :     sub ANS_MATRIX {
839 :     my $self = shift;
840 :     my $extend = shift; my $name = shift;
841 :     my $size = shift || 5;
842 :     my $def = ($self->{context} || $$Value::context)->lists->get('Matrix');
843 :     my $open = $self->{open} || $def->{open}; my $close = $self->{close} || $def->{close};
844 :     my @d = $self->dimensions;
845 : dpvc 3370 Value::Error("Can't create ans_array for %d-dimensional matrix",scalar(@d))
846 : dpvc 3269 if (scalar(@d) > 2);
847 :     @d = (1,@d) if (scalar(@d) == 1);
848 :     $self->ans_matrix($extend,$name,@d,$size,$open,$close,'');
849 :     }
850 :    
851 :     sub ans_array {my $self = shift; $self->ANS_MATRIX(0,'',@_)}
852 :     sub named_ans_array {my $self = shift; $self->ANS_MATRIX(0,@_)}
853 :     sub named_ans_array_extension {my $self = shift; $self->ANS_MATRIX(1,@_)}
854 :    
855 : dpvc 2593 #############################################################
856 :    
857 :     package Value::Interval;
858 :    
859 : dpvc 2609 sub cmp_defaults {(
860 : dpvc 3206 shift->SUPER::cmp_defaults(@_),
861 : dpvc 2601 showEndpointHints => 1,
862 :     showEndTypeHints => 1,
863 : dpvc 3460 requireParenMatch => 1,
864 : dpvc 2621 )}
865 : dpvc 2594
866 : dpvc 2593 sub typeMatch {
867 :     my $self = shift; my $other = shift;
868 : dpvc 3516 return 0 if !Value::isValue($other) || $other->isFormula;
869 :     return $other->canBeInUnion;
870 : dpvc 2593 }
871 :    
872 : dpvc 2601 #
873 : dpvc 3504 # Check for unreduced sets and unions
874 : dpvc 3497 #
875 : dpvc 3504 sub cmp_compare {
876 :     my $self = shift; my $student = shift; my $ans = shift;
877 :     my $error = $self->cmp_checkUnionReduce($student,$ans,@_);
878 : dpvc 3511 if ($error) {$$Value::context->setError($error,'',undef,undef,$CMP_WARNING); return}
879 : dpvc 3504 $self->SUPER::cmp_compare($student,$ans,@_);
880 : dpvc 3497 }
881 :    
882 :     #
883 : dpvc 2601 # Check for wrong enpoints and wrong type of endpoints
884 :     #
885 :     sub cmp_postprocess {
886 :     my $self = shift; my $ans = shift;
887 :     return unless $ans->{score} == 0 && !$ans->{isPreview};
888 :     my $other = $ans->{student_value};
889 : dpvc 3205 return if $ans->{ignoreStrings} && (!Value::isValue($other) || $other->type eq 'String');
890 : dpvc 2604 return unless $other->class eq 'Interval';
891 : dpvc 2601 my @errors;
892 :     if ($ans->{showEndpointHints}) {
893 :     push(@errors,"Your left endpoint is incorrect")
894 :     if ($self->{data}[0] != $other->{data}[0]);
895 :     push(@errors,"Your right endpoint is incorrect")
896 :     if ($self->{data}[1] != $other->{data}[1]);
897 :     }
898 : dpvc 3460 if (scalar(@errors) == 0 && $ans->{showEndTypeHints} && $ans->{requireParenMatch}) {
899 : dpvc 2601 push(@errors,"The type of interval is incorrect")
900 :     if ($self->{open}.$self->{close} ne $other->{open}.$other->{close});
901 :     }
902 :     $self->cmp_Error($ans,@errors);
903 :     }
904 :    
905 : dpvc 2593 #############################################################
906 :    
907 : dpvc 3469 package Value::Set;
908 :    
909 :     sub typeMatch {
910 :     my $self = shift; my $other = shift;
911 : dpvc 3516 return 0 if !Value::isValue($other) || $other->isFormula;
912 :     return $other->canBeInUnion;
913 : dpvc 3469 }
914 :    
915 :     #
916 :     # Use the List checker for sets, in order to get
917 :     # partial credit. Set the various types for error
918 :     # messages.
919 :     #
920 :     sub cmp_defaults {(
921 :     Value::List::cmp_defaults(@_),
922 :     typeMatch => 'Value::Real',
923 :     list_type => 'a set',
924 :     entry_type => 'a number',
925 :     removeParens => 0,
926 :     showParenHints => 1,
927 :     )}
928 :    
929 :     #
930 :     # Use the list checker if the student answer is a set
931 :     # otherwise use the standard compare (to get better
932 : dpvc 3504 # error messages).
933 : dpvc 3469 #
934 :     sub cmp_equal {
935 :     my ($self,$ans) = @_;
936 : dpvc 3497 return Value::List::cmp_equal(@_) if $ans->{student_value}->type eq 'Set';
937 :     $self->SUPER::cmp_equal($ans);
938 : dpvc 3469 }
939 :    
940 : dpvc 3504 #
941 :     # Check for unreduced sets and unions
942 :     #
943 :     sub cmp_compare {
944 :     my $self = shift; my $student = shift; my $ans = shift;
945 :     my $error = $self->cmp_checkUnionReduce($student,$ans,@_);
946 : dpvc 3511 if ($error) {$$Value::context->setError($error,'',undef,undef,$CMP_WARNING); return}
947 : dpvc 3504 $self->SUPER::cmp_compare($student,$ans,@_);
948 :     }
949 :    
950 : dpvc 3469 #############################################################
951 :    
952 : dpvc 2593 package Value::Union;
953 :    
954 :     sub typeMatch {
955 :     my $self = shift; my $other = shift;
956 : dpvc 2621 return 0 unless ref($other) && $other->class ne 'Formula';
957 : dpvc 2597 return $other->length == 2 &&
958 :     ($other->{open} eq '(' || $other->{open} eq '[') &&
959 :     ($other->{close} eq ')' || $other->{close} eq ']')
960 :     if $other->type =~ m/^(Point|List)$/;
961 : dpvc 3516 $other->isSetOfReals;
962 : dpvc 2593 }
963 :    
964 : dpvc 2617 #
965 :     # Use the List checker for unions, in order to get
966 :     # partial credit. Set the various types for error
967 :     # messages.
968 :     #
969 :     sub cmp_defaults {(
970 : dpvc 2621 Value::List::cmp_defaults(@_),
971 :     typeMatch => 'Value::Interval',
972 : dpvc 3469 list_type => 'an interval, set or union',
973 :     short_type => 'a union',
974 :     entry_type => 'an interval or set',
975 : dpvc 2617 )}
976 :    
977 : dpvc 3511 sub cmp_equal {
978 :     my $self = shift; my $ans = shift;
979 :     my $error = $self->cmp_checkUnionReduce($ans->{student_value},$ans);
980 :     if ($error) {$self->cmp_Error($ans,$error); return}
981 :     Value::List::cmp_equal($self,$ans);
982 :     }
983 : dpvc 3504
984 : dpvc 3497 #
985 :     # Check for unreduced sets and unions
986 :     #
987 : dpvc 3504 sub cmp_compare {
988 :     my $self = shift; my $student = shift; my $ans = shift;
989 :     my $error = $self->cmp_checkUnionReduce($student,$ans,@_);
990 : dpvc 3511 if ($error) {$$Value::context->setError($error,'',undef,undef,$CMP_WARNING); return}
991 : dpvc 3504 $self->SUPER::cmp_compare($student,$ans,@_);
992 : dpvc 3497 }
993 : dpvc 2617
994 : dpvc 2593 #############################################################
995 :    
996 : dpvc 2599 package Value::List;
997 :    
998 : dpvc 2621 sub cmp_defaults {
999 :     my $self = shift;
1000 : dpvc 3206 my %options = (@_);
1001 : dpvc 3207 my $element = Value::makeValue($self->{data}[0]);
1002 :     $element = Value::Formula->new($element) unless Value::isValue($element);
1003 : dpvc 2621 return (
1004 : dpvc 3206 Value::Real->cmp_defaults(@_),
1005 : dpvc 2621 showHints => undef,
1006 :     showLengthHints => undef,
1007 : dpvc 2661 showParenHints => undef,
1008 : dpvc 2757 partialCredit => undef,
1009 : dpvc 2621 ordered => 0,
1010 :     entry_type => undef,
1011 : dpvc 2629 list_type => undef,
1012 : dpvc 3207 typeMatch => $element,
1013 : dpvc 3872 firstElement => $element,
1014 : dpvc 3678 extra => undef,
1015 : dpvc 2661 requireParenMatch => 1,
1016 :     removeParens => 1,
1017 : dpvc 3869 implicitList => 1,
1018 : dpvc 3487 );
1019 : dpvc 2621 }
1020 : dpvc 2599
1021 : dpvc 2621 #
1022 :     # Match anything but formulas
1023 :     #
1024 :     sub typeMatch {return !ref($other) || $other->class ne 'Formula'}
1025 : dpvc 2599
1026 : dpvc 2604 #
1027 :     # Handle removal of outermost parens in correct answer.
1028 :     #
1029 :     sub cmp {
1030 :     my $self = shift;
1031 : dpvc 3696 my %params = @_;
1032 : dpvc 2604 my $cmp = $self->SUPER::cmp(@_);
1033 : dpvc 2661 if ($cmp->{rh_ans}{removeParens}) {
1034 : dpvc 2604 $self->{open} = $self->{close} = '';
1035 : dpvc 3172 $cmp->ans_hash(correct_ans => $self->stringify)
1036 : dpvc 3696 unless defined($self->{correct_ans}) || defined($params{correct_ans});
1037 : dpvc 2604 }
1038 :     return $cmp;
1039 :     }
1040 :    
1041 : dpvc 2599 sub cmp_equal {
1042 :     my $self = shift; my $ans = shift;
1043 : dpvc 2621 $ans->{showPartialCorrectAnswers} = $self->getPG('$showPartialCorrectAnswers');
1044 :    
1045 :     #
1046 :     # get the paramaters
1047 :     #
1048 : dpvc 3206 my $showHints = getOption($ans,'showHints');
1049 :     my $showLengthHints = getOption($ans,'showLengthHints');
1050 : dpvc 3469 my $showParenHints = getOption($ans,'showParenHints');
1051 : dpvc 3206 my $partialCredit = getOption($ans,'partialCredit');
1052 : dpvc 2661 my $requireParenMatch = $ans->{requireParenMatch};
1053 : dpvc 3869 my $implicitList = $ans->{implicitList};
1054 : dpvc 3206 my $typeMatch = $ans->{typeMatch};
1055 :     my $value = $ans->{entry_type};
1056 :     my $ltype = $ans->{list_type} || lc($self->type);
1057 : dpvc 3469 my $stype = $ans->{short_type} || $ltype;
1058 : dpvc 2621
1059 : dpvc 3678 $value = (Value::isValue($typeMatch)? lc($typeMatch->cmp_class): 'a value')
1060 : dpvc 2621 unless defined($value);
1061 : dpvc 2624 $value =~ s/(real|complex) //; $ans->{cmp_class} = $value;
1062 :     $value =~ s/^an? //; $value = 'formula' if $value =~ m/formula/;
1063 : dpvc 3469 $ltype =~ s/^an? //; $stype =~ s/^an? //;
1064 : dpvc 3206 $showHints = $showLengthHints = 0 if $ans->{isPreview};
1065 : dpvc 2599
1066 : dpvc 2621 #
1067 :     # Get the lists of correct and student answers
1068 : dpvc 2624 # (split formulas that return lists or unions)
1069 : dpvc 2621 #
1070 : dpvc 2661 my @correct = (); my ($cOpen,$cClose);
1071 :     if ($self->class ne 'Formula') {
1072 :     @correct = $self->value;
1073 :     $cOpen = $ans->{correct_value}{open}; $cClose = $ans->{correct_value}{close};
1074 :     } else {
1075 :     @correct = Value::List->splitFormula($self,$ans);
1076 :     $cOpen = $self->{tree}{open}; $cClose = $self->{tree}{close};
1077 :     }
1078 :     my $student = $ans->{student_value}; my @student = ($student);
1079 :     my ($sOpen,$sClose) = ('','');
1080 : dpvc 2648 if (Value::isFormula($student) && $student->type eq $self->type) {
1081 : dpvc 3869 if ($implicitList && $student->{tree}{open} ne '') {
1082 :     @student = ($student);
1083 :     } else {
1084 :     @student = Value::List->splitFormula($student,$ans);
1085 :     $sOpen = $student->{tree}{open}; $sClose = $student->{tree}{close};
1086 :     }
1087 : dpvc 2661 } elsif ($student->class ne 'Formula' && $student->class eq $self->type) {
1088 : dpvc 3869 if ($implicitList && $student->{open} ne '') {
1089 :     @student = ($student);
1090 :     } else {
1091 :     @student = @{$student->{data}};
1092 :     $sOpen = $student->{open}; $sClose = $student->{close};
1093 :     }
1094 : dpvc 2621 }
1095 : dpvc 2624 return if $ans->{split_error};
1096 : dpvc 2661 #
1097 :     # Check for parenthesis match
1098 :     #
1099 :     if ($requireParenMatch && ($sOpen ne $cOpen || $sClose ne $cClose)) {
1100 :     if ($showParenHints && !($ans->{ignoreStrings} && $student->type eq 'String')) {
1101 :     my $message = "The parentheses for your $ltype ";
1102 :     if (($cOpen || $cClose) && ($sOpen || $sClose))
1103 :     {$message .= "are of the wrong type"}
1104 :     elsif ($sOpen || $sClose) {$message .= "should be removed"}
1105 : dpvc 3469 else {$message .= "seem to be missing"}
1106 : dpvc 2664 $self->cmp_Error($ans,$message) unless $ans->{isPreview};
1107 : dpvc 2661 }
1108 :     return;
1109 :     }
1110 : dpvc 2599
1111 : dpvc 2621 #
1112 : dpvc 3206 # Determine the maximum score
1113 : dpvc 2621 #
1114 : dpvc 2624 my $M = scalar(@correct);
1115 : dpvc 2599 my $m = scalar(@student);
1116 : dpvc 2624 my $maxscore = ($m > $M)? $m : $M;
1117 : dpvc 3206
1118 :     #
1119 :     # Compare the two lists
1120 :     # (Handle errors in user-supplied functions)
1121 :     #
1122 :     my ($score,@errors);
1123 :     if (ref($ans->{list_checker}) eq 'CODE') {
1124 :     eval {($score,@errors) = &{$ans->{list_checker}}([@correct],[@student],$ans,$value)};
1125 :     if (!defined($score)) {
1126 :     die $@ if $@ ne '' && $self->{context}{error}{flag} == 0;
1127 :     $self->cmp_error($ans) if $self->{context}{error}{flag};
1128 :     }
1129 :     } else {
1130 :     ($score,@errors) = $self->cmp_list_compare([@correct],[@student],$ans,$value);
1131 :     }
1132 :     return unless defined($score);
1133 :    
1134 :     #
1135 :     # Give hints about extra or missing answers
1136 :     #
1137 :     if ($showLengthHints) {
1138 : dpvc 3504 $value =~ s/( or|,) /s$1 /g; # fix "interval or union"
1139 : dpvc 3469 push(@errors,"There should be more ${value}s in your $stype")
1140 : dpvc 3206 if ($score < $maxscore && $score == $m);
1141 : dpvc 3469 push(@errors,"There should be fewer ${value}s in your $stype")
1142 : dpvc 3206 if ($score < $maxscore && $score == $M && !$showHints);
1143 :     }
1144 :    
1145 :     #
1146 : dpvc 3497 # If all the entries are in error, don't give individual messages
1147 :     #
1148 :     if ($score == 0) {
1149 :     my $i = 0;
1150 :     while ($i <= $#errors) {
1151 :     if ($errors[$i++] =~ m/^Your .* is incorrect$/)
1152 :     {splice(@errors,--$i,1)}
1153 :     }
1154 :     }
1155 :    
1156 :     #
1157 : dpvc 3206 # Finalize the score
1158 :     #
1159 :     $score = 0 if ($score != $maxscore && !$partialCredit);
1160 :     $ans->score($score/$maxscore);
1161 :     push(@errors,"Score = $ans->{score}") if $ans->{debug};
1162 : dpvc 3207 my $error = join("\n",@errors); $error =~ s!</DIV>\n!</DIV>!g;
1163 :     $ans->{error_message} = $ans->{ans_message} = $error;
1164 : dpvc 3206 }
1165 :    
1166 :     #
1167 :     # Compare the contents of the list to see of they are equal
1168 :     #
1169 :     sub cmp_list_compare {
1170 :     my $self = shift;
1171 :     my $correct = shift; my $student = shift; my $ans = shift; my $value = shift;
1172 :     my @correct = @{$correct}; my @student = @{$student}; my $m = scalar(@student);
1173 :     my $ordered = $ans->{ordered};
1174 :     my $showTypeWarnings = $ans->{showTypeWarnings} && !$ans->{isPreview};
1175 :     my $typeMatch = $ans->{typeMatch};
1176 : dpvc 3872 my $extra = $ans->{extra} ||
1177 :     (Value::isValue($typeMatch) ? $typeMatch: $ans->{firstElement}) ||
1178 :     "Value::List";
1179 : dpvc 3206 my $showHints = getOption($ans,'showHints') && !$ans->{isPreview};
1180 :     my $error = $$Value::context->{error};
1181 : dpvc 2599 my $score = 0; my @errors; my $i = 0;
1182 :    
1183 : dpvc 2621 #
1184 : dpvc 3206 # Check for empty lists
1185 :     #
1186 :     if (scalar(@correct) == 0) {$ans->score($m == 0); return}
1187 :    
1188 :     #
1189 : dpvc 2621 # Loop through student answers looking for correct ones
1190 :     #
1191 : dpvc 2599 ENTRY: foreach my $entry (@student) {
1192 : dpvc 3206 $i++; $$Value::context->clearError;
1193 : dpvc 2605 $entry = Value::makeValue($entry);
1194 : dpvc 2600 $entry = Value::Formula->new($entry) if !Value::isValue($entry);
1195 : dpvc 3504
1196 :     #
1197 :     # Some words differ if ther eis only one entry in the student's list
1198 :     #
1199 :     my $nth = ''; my $answer = 'answer';
1200 : dpvc 3678 my $class = $ans->{list_type} || $ans->{cmp_class};
1201 : dpvc 3504 if ($m > 1) {
1202 :     $nth = ' '.$self->NameForNumber($i);
1203 :     $class = $ans->{cmp_class};
1204 :     $answer = 'value';
1205 :     }
1206 :    
1207 :     #
1208 :     # See if the entry matches the correct answer
1209 :     # and perform syntax checking if not
1210 :     #
1211 : dpvc 2599 if ($ordered) {
1212 : dpvc 3207 if (scalar(@correct)) {
1213 : dpvc 3504 if (shift(@correct)->cmp_compare($entry,$ans,$nth,$value)) {$score++; next ENTRY}
1214 : dpvc 3207 } else {
1215 : dpvc 3872 # do syntax check
1216 :     if (ref($extra) eq 'CODE') {&$extra($entry,$ans,$nth,$value)}
1217 :     else {$extra->cmp_compare($entry,$ans,$nth,$value)}
1218 : dpvc 3207 }
1219 : dpvc 3206 if ($error->{flag} == $CMP_ERROR) {$self->cmp_error($ans); return}
1220 : dpvc 2599 } else {
1221 :     foreach my $k (0..$#correct) {
1222 : dpvc 3504 if ($correct[$k]->cmp_compare($entry,$ans,$nth,$value)) {
1223 : dpvc 2599 splice(@correct,$k,1);
1224 :     $score++; next ENTRY;
1225 :     }
1226 : dpvc 3206 if ($error->{flag} == $CMP_ERROR) {$self->cmp_error($ans); return}
1227 : dpvc 2599 }
1228 : dpvc 3504 $$Value::context->clearError;
1229 : dpvc 3872 # do syntax check
1230 :     if (ref($extra) eq 'CODE') {&$extra($entry,$ans,$nth,$value)}
1231 :     else {$extra->cmp_compare($entry,$ans,$nth,$value)}
1232 : dpvc 2599 }
1233 : dpvc 2621 #
1234 :     # Give messages about incorrect answers
1235 :     #
1236 : dpvc 3872 my $match = (ref($typeMatch) eq 'CODE')? &$typeMatch($entry,$ans) :
1237 :     $typeMatch->typeMatch($entry,$ans);
1238 :     if ($showTypeWarnings && !$match &&
1239 :     !($ans->{ignoreStrings} && $entry->class eq 'String')) {
1240 : dpvc 2624 push(@errors,"Your$nth $answer isn't ".lc($class).
1241 :     " (it looks like ".lc($entry->showClass).")");
1242 : dpvc 3504 } elsif ($error->{flag} && $ans->{showEqualErrors}) {
1243 :     my $message = $error->{message}; $message =~ s/\s+$//;
1244 :     if ($m > 1 && $error->{flag} != $CMP_WARNING) {
1245 :     push(@errors,"<SMALL>There is a problem with your$nth $value:</SMALL>",
1246 :     '<DIV STYLE="margin-left:1em">'.$message.'</DIV>');
1247 :     } else {push(@errors,$message)}
1248 : dpvc 2621 } elsif ($showHints && $m > 1) {
1249 :     push(@errors,"Your$nth $value is incorrect");
1250 : dpvc 2599 }
1251 :     }
1252 :    
1253 : dpvc 2621 #
1254 : dpvc 3206 # Return the score and errors
1255 : dpvc 2621 #
1256 : dpvc 3206 return ($score,@errors);
1257 : dpvc 2599 }
1258 :    
1259 :     #
1260 : dpvc 2624 # Split a formula that is a list or union into a
1261 :     # list of formulas (or Value objects).
1262 :     #
1263 :     sub splitFormula {
1264 :     my $self = shift; my $formula = shift; my $ans = shift;
1265 :     my @formula; my @entries;
1266 : dpvc 3469 if ($formula->type eq 'Union') {@entries = $formula->{tree}->makeUnion}
1267 :     else {@entries = @{$formula->{tree}{coords}}}
1268 : dpvc 2624 foreach my $entry (@entries) {
1269 :     my $v = Parser::Formula($entry);
1270 :     $v = Parser::Evaluate($v) if (defined($v) && $v->isConstant);
1271 :     push(@formula,$v);
1272 :     #
1273 :     # There shouldn't be an error evaluating the formula,
1274 :     # but you never know...
1275 :     #
1276 : dpvc 2648 if (!defined($v)) {$ans->{split_error} = 1; $self->cmp_error; return}
1277 : dpvc 2624 }
1278 :     return @formula;
1279 :     }
1280 :    
1281 :     #
1282 : dpvc 2621 # Return the value if it is defined, otherwise use a default
1283 : dpvc 2599 #
1284 :     sub getOption {
1285 : dpvc 2621 my $ans = shift; my $name = shift;
1286 :     my $value = $ans->{$name};
1287 : dpvc 2599 return $value if defined($value);
1288 : dpvc 2621 return $ans->{showPartialCorrectAnswers};
1289 : dpvc 2599 }
1290 :    
1291 :     #############################################################
1292 :    
1293 : dpvc 2593 package Value::Formula;
1294 :    
1295 : dpvc 2624 sub cmp_defaults {
1296 :     my $self = shift;
1297 : dpvc 2626
1298 : dpvc 2624 return (
1299 :     Value::Union::cmp_defaults($self,@_),
1300 :     typeMatch => Value::Formula->new("(1,2]"),
1301 : dpvc 3257 showDomainErrors => 1,
1302 : dpvc 2624 ) if $self->type eq 'Union';
1303 : dpvc 2622
1304 : dpvc 2667 my $type = $self->type;
1305 :     $type = ($self->isComplex)? 'Complex': 'Real' if $type eq 'Number';
1306 :     $type = 'Value::'.$type.'::';
1307 : dpvc 2624
1308 : dpvc 3257 return (
1309 :     &{$type.'cmp_defaults'}($self,@_),
1310 :     upToConstant => 0,
1311 :     showDomainErrors => 1,
1312 :     ) if defined(%$type) && $self->type ne 'List';
1313 : dpvc 2667
1314 : dpvc 3678 my $element;
1315 :     if ($self->{tree}->class eq 'List') {$element = Value::Formula->new($self->{tree}{coords}[0])}
1316 :     else {$element = Value::Formula->new(($self->createRandomPoints(1))[1]->[0]{data}[0])}
1317 : dpvc 2624 return (
1318 :     Value::List::cmp_defaults($self,@_),
1319 : dpvc 2661 removeParens => $self->{autoFormula},
1320 : dpvc 3678 typeMatch => $element,
1321 : dpvc 3257 showDomainErrors => 1,
1322 : dpvc 2624 );
1323 :     }
1324 :    
1325 :     #
1326 :     # Get the types from the values of the formulas
1327 :     # and compare those.
1328 :     #
1329 :     sub typeMatch {
1330 :     my $self = shift; my $other = shift; my $ans = shift;
1331 :     return 1 if $self->type eq $other->type;
1332 :     my $typeMatch = ($self->createRandomPoints(1))[1]->[0];
1333 : dpvc 2648 $other = eval {($other->createRandomPoints(1))[1]->[0]} if Value::isFormula($other);
1334 : dpvc 2624 return 1 unless defined($other); # can't really tell, so don't report type mismatch
1335 :     $typeMatch->typeMatch($other,$ans);
1336 :     }
1337 :    
1338 : dpvc 2629 #
1339 :     # Handle removal of outermost parens in a list.
1340 : dpvc 3715 # Evaluate answer, if the eval option is used.
1341 :     # Handle the UpToConstant option.
1342 : dpvc 2629 #
1343 :     sub cmp {
1344 :     my $self = shift;
1345 :     my $cmp = $self->SUPER::cmp(@_);
1346 : dpvc 2661 if ($cmp->{rh_ans}{removeParens} && $self->type eq 'List') {
1347 : dpvc 2629 $self->{tree}{open} = $self->{tree}{close} = '';
1348 : dpvc 3172 $cmp->ans_hash(correct_ans => $self->stringify)
1349 :     unless defined($self->{correct_ans});
1350 : dpvc 2629 }
1351 : dpvc 2799 if ($cmp->{rh_ans}{eval} && $self->isConstant) {
1352 :     $cmp->ans_hash(correct_value => $self->eval);
1353 :     return $cmp;
1354 :     }
1355 : dpvc 2688 if ($cmp->{rh_ans}{upToConstant}) {
1356 :     my $current = Parser::Context->current();
1357 :     my $context = $self->{context} = $self->{context}->copy;
1358 :     Parser::Context->current(undef,$context);
1359 : dpvc 3868 $context->variables->add('C0' => 'Parameter');
1360 : dpvc 3217 my $f = Value::Formula->new('C0')+$self;
1361 : dpvc 3257 for ('limits','test_points','test_values','num_points','granularity','resolution',
1362 :     'checkUndefinedPoints','max_undefined')
1363 : dpvc 3217 {$f->{$_} = $self->{$_} if defined($self->{$_})}
1364 :     $cmp->ans_hash(correct_value => $f);
1365 : dpvc 2688 Parser::Context->current(undef,$current);
1366 :     }
1367 : dpvc 2629 return $cmp;
1368 :     }
1369 :    
1370 : dpvc 2622 sub cmp_equal {
1371 : dpvc 2624 my $self = shift; my $ans = shift;
1372 :     #
1373 :     # Get the problem's seed
1374 :     #
1375 : dpvc 2622 $self->{context}->flags->set(
1376 :     random_seed => $self->getPG('$PG_original_problemSeed')
1377 :     );
1378 : dpvc 2624
1379 :     #
1380 :     # Use the list checker if the formula is a list or union
1381 :     # Otherwise use the normal checker
1382 :     #
1383 : dpvc 3497 if ($self->type =~ m/^(List|Union|Set)$/) {
1384 : dpvc 2624 Value::List::cmp_equal($self,$ans);
1385 :     } else {
1386 :     $self->SUPER::cmp_equal($ans);
1387 :     }
1388 : dpvc 2622 }
1389 :    
1390 : dpvc 2667 sub cmp_postprocess {
1391 :     my $self = shift; my $ans = shift;
1392 :     return unless $ans->{score} == 0 && !$ans->{isPreview};
1393 : dpvc 3257 return if $ans->{ans_message};
1394 :     if ($self->{domainMismatch} && $ans->{showDomainErrors}) {
1395 :     $self->cmp_Error($ans,"The domain of your function doesn't match that of the correct answer");
1396 :     return;
1397 :     }
1398 :     return if !$ans->{showDimensionHints};
1399 : dpvc 2667 my $other = $ans->{student_value};
1400 : dpvc 3205 return if $ans->{ignoreStrings} && (!Value::isValue($other) || $other->type eq 'String');
1401 : dpvc 2667 return unless $other->type =~ m/^(Point|Vector|Matrix)$/;
1402 :     return unless $self->type =~ m/^(Point|Vector|Matrix)$/;
1403 :     return if Parser::Item::typeMatch($self->typeRef,$other->typeRef);
1404 : dpvc 2671 $self->cmp_Error($ans,"The dimension of your result is incorrect");
1405 : dpvc 2593 }
1406 :    
1407 : dpvc 3269 #
1408 : dpvc 3715 # Diagnostics for Formulas
1409 :     #
1410 :     sub cmp_diagnostics {
1411 :     my $self = shift; my $ans = shift;
1412 :     my $isEvaluator = (ref($ans) =~ /Evaluator/)? 1: 0;
1413 :     my $hash = $isEvaluator? $ans->rh_ans : $ans;
1414 :     my $diagnostics = $self->{context}->diagnostics->merge("formulas",$self,$hash);
1415 :     my $formulas = $diagnostics->{formulas};
1416 :     return unless $formulas->{show};
1417 :    
1418 :     my $output = "";
1419 :     if ($isEvaluator) {
1420 :     #
1421 :     # The tests to be performed with the answer checker is created
1422 :     #
1423 :     $self->getPG('loadMacros("PGgraphmacros.pl")');
1424 :     my ($inputs) = $self->getPG('$inputs_ref');
1425 :     my $process = $inputs->{checkAnswers} || $inputs->{previewAnswers} || $inputs->{submitAnswers};
1426 :     if ($formulas->{checkNumericStability} && !$process) {
1427 :     ### still needs to be written
1428 :     }
1429 :     } else {
1430 :     #
1431 :     # The checks to be performed when an answer is submitted
1432 :     #
1433 :     my $student = $ans->{student_formula};
1434 : dpvc 4143 #
1435 :     # Get the test points
1436 :     #
1437 :     my @names = $self->{context}->variables->names;
1438 :     my $vx = (keys(%{$self->{variables}}))[0];
1439 :     my $vi = 0; while ($names[$vi] ne $vx) {$vi++}
1440 :     my $points = [map {$_->[$vi]} @{$self->{test_points}}];
1441 : dpvc 3715
1442 :     #
1443 :     # The graphs of the functions and errors
1444 :     #
1445 :     if ($formulas->{showGraphs}) {
1446 :     my @G = ();
1447 :     if ($formulas->{combineGraphs}) {
1448 :     push(@G,$self->cmp_graph($diagnostics,[$student,$self],
1449 :     title=>'Student Answer (red)<BR>Correct Answer (green)<BR>',
1450 :     points=>$points,showDomain=>1));
1451 :     } else {
1452 :     push(@G,$self->cmp_graph($diagnostics,$self,title=>'Correct Answer'));
1453 :     push(@G,$self->cmp_graph($diagnostics,$student,title=>'Student Answer'));
1454 :     }
1455 :     my $cutoff = Value::Formula->new($self->getFlag('tolerance'));
1456 :     if ($formulas->{graphAbsoluteErrors}) {
1457 :     push(@G,$self->cmp_graph($diagnostics,[abs($self-$student),$cutoff],
1458 :     clip=>$formulas->{clipAbsoluteError},
1459 :     title=>'Absolute Error',points=>$points));
1460 :     }
1461 :     if ($formulas->{graphRelativeErrors}) {
1462 :     push(@G,$self->cmp_graph($diagnostics,[abs(($self-$student)/$self),$cutoff],
1463 :     clip=>$formulas->{clipRelativeError},
1464 :     title=>'Relative Error',points=>$points));
1465 :     }
1466 :     $output .= '<TABLE BORDER="0" CELLSPACING="0" CELLPADDING="0">'
1467 :     . '<TR VALIGN="TOP">'.join('<TD WIDTH="20"></TD>',@G).'</TR></TABLE>';
1468 :     }
1469 :    
1470 :     #
1471 :     # The test points and values
1472 :     #
1473 :     my @rows = (); my $colsep = '</TD><TD WIDTH="20"></TD><TD ALIGN="RIGHT">';
1474 :     my @P = (map {(scalar(@{$_}) == 1)? $_->[0]: Value::Point->make(@{$_})} @{$self->{test_points}});
1475 :     my @i = sort {$P[$a] <=> $P[$b]} (0..$#P);
1476 : dpvc 4143 foreach $p (@P) {if (Value::isValue($p) && $p->length > 2) {$p = $p->string; $p =~ s|,|,<br />|g}}
1477 :     my $zeroLevelTol = $self->getFlag('zeroLevelTol');
1478 :     $self->{context}{flags}{zeroLevelTol} = 0; # always show full resolution in the tables below
1479 :     my $names = join(',',@names); $names = '('.$names.')' if scalar(@names) > 1;
1480 : dpvc 3715 if ($formulas->{showTestPoints}) {
1481 :     $student->createPointValues($self->{test_points},0,1,1) unless $student->{test_values};
1482 : dpvc 4143 my @p = ("$names:", (map {$P[$i[$_]]} (0..$#P)));
1483 : dpvc 3715 push(@rows,'<TR><TD ALIGN="RIGHT">'.join($colsep,@p).'</TD></TR>');
1484 :     push(@rows,'<TR><TD ALIGN="RIGHT">'.join($colsep,("<HR>")x scalar(@p)).'</TD></TR>');
1485 :     push(@rows,'<TR><TD ALIGN="RIGHT">'
1486 : dpvc 4143 .join($colsep,"Correct Answer:",
1487 :     map {Value::isNumber($self->{test_values}[$i[$_]])? $self->{test_values}[$i[$_]]: "undefined"} (0..$#P))
1488 : dpvc 3715 .'</TD></TR>');
1489 :     my $test = $student->{test_values};
1490 :     push(@rows,'<TR><TD ALIGN="RIGHT">'
1491 : dpvc 4143 .join($colsep,"Student Answer:",
1492 :     map {Value::isNumber($test->[$i[$_]])? $test->[$i[$_]]: "undefined"} (0..$#P))
1493 : dpvc 3715 .'</TD></TR>');
1494 :     }
1495 :     #
1496 :     # The absolute errors (colored by whether they are ok or too big)
1497 :     #
1498 :     if ($formulas->{showAbsoluteErrors}) {
1499 :     my @p = ("Absolute Error:");
1500 :     my $tolerance = $self->getFlag('tolerance');
1501 :     my $tolType = $self->getFlag('tolType'); my $error;
1502 :     foreach my $j (0..$#P) {
1503 :     if (Value::isNumber($student->{test_values}[$i[$j]])) {
1504 :     $error = abs($self->{test_values}[$i[$j]]-$student->{test_values}[$i[$j]]);
1505 : dpvc 4143 $error = '<SPAN STYLE="color:#'.($error->value<$tolerance ? '00AA00': 'AA0000').'">'.$error.'</SPAN>'
1506 : dpvc 3715 if $tolType eq 'absolute';
1507 :     } else {$error = "---"}
1508 :     push(@p,$error);
1509 :     }
1510 :     push(@rows,'<TR><TD ALIGN="RIGHT">'.join($colsep,@p).'</TD></TR>');
1511 :     }
1512 :     #
1513 : dpvc 4143 # The relative errors (colored by whether they are OK or too big)
1514 : dpvc 3715 #
1515 :     if ($formulas->{showRelativeErrors}) {
1516 :     my @p = ("Relative Error:");
1517 : dpvc 4143 my $tolerance = $self->getFlag('tolerance'); my $tol;
1518 : dpvc 3715 my $tolType = $self->getFlag('tolType'); my $error;
1519 : dpvc 4143 my $zeroLevel = $self->getFlag('zeroLevel');
1520 : dpvc 3715 foreach my $j (0..$#P) {
1521 :     if (Value::isNumber($student->{test_values}[$i[$j]])) {
1522 : dpvc 4143 my $c = $self->{test_values}[$i[$j]]; my $s = $student->{test_values}[$i[$j]];
1523 :     if (abs($c->value) < $zeroLevel || abs($s->value) < $zeroLevel)
1524 :     {$error = abs($c-$s); $tol = $zeroLevelTol} else
1525 :     {$error = abs(($c-$s)/($c||1E-10)); $tol = $tolerance}
1526 :     $error = '<SPAN STYLE="color:#'.($error < $tol ? '00AA00': 'AA0000').'">'.$error.'</SPAN>'
1527 : dpvc 3715 if $tolType eq 'relative';
1528 :     } else {$error = "---"}
1529 :     push(@p,$error);
1530 :     }
1531 :     push(@rows,'<TR><TD ALIGN="RIGHT">'.join($colsep,@p).'</TD></TR>');
1532 :     }
1533 : dpvc 4143 $self->{context}{flags}{zeroLevelTol} = $zeroLevelTol;
1534 : dpvc 3715 #
1535 :     # Put the data into a table
1536 :     #
1537 :     if (scalar(@rows)) {
1538 :     $output .= '<p><HR><p><TABLE BORDER="0" CELLSPACING="0" CELLPADDING="0">'
1539 :     . join('<TR><TD HEIGHT="3"></TD>',@rows)
1540 :     . '</TABLE>';
1541 :     }
1542 :     }
1543 :     #
1544 :     # Put all the diagnostic output into a frame
1545 :     #
1546 :     return unless $output;
1547 :     $output
1548 :     = '<TABLE BORDER="1" CELLSPACING="2" CELLPADDING="20" BGCOLOR="#F0F0F0">'
1549 :     . '<TR><TD ALIGN="LEFT"><B>Diagnostics for '.$self->string .':</B>'
1550 :     . '<P><CENTER>' . $output . '</CENTER></TD></TR></TABLE><P>';
1551 :     warn $output;
1552 :     }
1553 :    
1554 :     #
1555 :     # Draw a graph from a given Formula object
1556 :     #
1557 :     sub cmp_graph {
1558 :     my $self = shift; my $diagnostics = shift;
1559 :     my $F1 = shift; my $F2; ($F1,$F2) = @{$F1} if (ref($F1) eq 'ARRAY');
1560 :     #
1561 :     # Get the various options
1562 :     #
1563 :     my %options = (title=>'',points=>[],@_);
1564 :     my $graphs = $diagnostics->{graphs};
1565 :     my $limits = $graphs->{limits}; $limits = $self->getFlag('limits',[-2,2]) unless $limits;
1566 : dpvc 4143 $limits = $limits->[0] while ref($limits) eq 'ARRAY' && ref($limits->[0]) eq 'ARRAY';
1567 : dpvc 3715 my $size = $graphs->{size}; $size = [$size,$size] unless ref($size) eq 'ARRAY';
1568 :     my $steps = $graphs->{divisions};
1569 :     my $points = $options{points}; my $clip = $options{clip};
1570 :     my ($my,$My) = (0,0); my ($mx,$Mx) = @{$limits};
1571 :     my $dx = ($Mx-$mx)/$steps; my $f; my $y;
1572 :    
1573 :     #
1574 :     # Find the max and min values of the function
1575 :     #
1576 :     foreach $f ($F1,$F2) {
1577 :     next unless defined($f);
1578 :     unless (scalar(keys(%{$f->{variables}})) < 2) {
1579 :     warn "Only formulas with one variable can be graphed";
1580 :     return "";
1581 :     }
1582 :     if ($f->isConstant) {
1583 :     $y = $f->eval;
1584 :     $my = $y if $y < $my; $My = $y if $y > $My;
1585 :     } else {
1586 :     my $F = $f->perlFunction;
1587 :     foreach my $i (0..$steps-1) {
1588 :     $y = eval {&{$F}($mx+$i*$dx)}; next unless defined($y) && Value::isNumber($y);
1589 :     $my = $y if $y < $my; $My = $y if $y > $My;
1590 :     }
1591 :     }
1592 :     }
1593 :     $My = 1 if abs($My - $my) < 1E-5;
1594 :     $my *= 1.1; $My *= 1.1;
1595 :     if ($clip) {
1596 :     $my = -$clip if $my < -$clip;
1597 :     $My = $clip if $My > $clip;
1598 :     }
1599 :     $my = -$My/10 if $my > -$My/10; $My = -$my/10 if $My < -$my/10;
1600 :     my $a = Value::Real->new(($My-$my)/($Mx-$mx));
1601 :    
1602 :     #
1603 :     # Create the graph itself, with suitable title
1604 :     #
1605 :     my $grf = $self->getPG('$_grf_ = {n => 0}');
1606 :     $grf->{Goptions} = [
1607 :     $mx,$my,$Mx,$My,
1608 :     axes => $graphs->{axes},
1609 :     grid => $graphs->{grid},
1610 :     size => $size,
1611 :     ];
1612 :     $grf->{G} = $self->getPG('init_graph(@{$_grf_->{Goptions}})');
1613 :     $grf->{G}->imageName($grf->{G}->imageName.'-'.time()); # avoid browser cache
1614 :     $self->cmp_graph_function($grf,$F2,"green",$steps,$points) if defined($F2);
1615 :     $self->cmp_graph_function($grf,$F1,"red",$steps,$points);
1616 :     my $image = $self->getPG('alias(insertGraph($_grf_->{G}))');
1617 :     $image = '<IMG SRC="'.$image.'" WIDTH="'.$size->[0].'" HEIGHT="'.$size->[1].'" BORDER="0" STYLE="margin-bottom:5px">';
1618 :     my $title = $options{title}; $title .= '<DIV STYLE="margin-top:5px"></DIV>' if $title;
1619 :     $title .= "<SMALL>Domain: [$mx,$Mx]</SMALL><BR>" if $options{showDomain};
1620 :     $title .= "<SMALL>Range: [$my,$My]<BR>Aspect ratio: $a:1</SMALL>";
1621 :     return '<TD ALIGN="CENTER" VALIGN="TOP" NOWRAP>'.$image.'<BR>'.$title.'</TD>';
1622 :     }
1623 :    
1624 :     #
1625 :     # Add a function to a graph object, and plot the points
1626 :     # that are used to test the function
1627 :     #
1628 :     sub cmp_graph_function {
1629 :     my $self = shift; my $grf = shift; my $F = shift;
1630 :     my $color = shift; my $steps = shift; my $points = shift;
1631 :     $grf->{n}++; my $Fn = "F".$grf->{n}; $grf->{$Fn} = $F; my $f;
1632 :     if ($F->isConstant) {
1633 :     my $y = $F->eval;
1634 :     $f = $self->getPG('new Fun(sub {'.$y.'},$_grf_->{G})');
1635 :     } else {
1636 :     my $X = (keys %{$F->{variables}})[0];
1637 :     $f = $self->getPG('new Fun(sub {Parser::Evaluate($_grf_->{'.$Fn.'},'.$X.'=>shift)},$_grf_->{G})');
1638 :     foreach my $x (@{$points}) {
1639 :     my $y = Parser::Evaluate($F,($X)=>$x); next unless defined($y) && Value::isNumber($y);
1640 :     $grf->{x} = $x; $grf->{y} = $y;
1641 :     my $C = $self->getPG('new Circle($_grf_->{x},$_grf_->{y},4,"'.$color.'","'.$color.'")');
1642 :     $grf->{G}->stamps($C);
1643 :     }
1644 :     }
1645 :     $f->color($color); $f->weight(2); $f->steps($steps);
1646 :     }
1647 :    
1648 :     #
1649 : dpvc 3269 # If an answer array was used, get the data from the
1650 :     # Matrix, Vector or Point, and format the array of
1651 :     # data using the original parameter
1652 :     #
1653 :     sub correct_ans {
1654 :     my $self = shift;
1655 :     return $self->SUPER::correct_ans unless $self->{ans_name};
1656 :     my @array = ();
1657 :     if ($self->{tree}->type eq 'Matrix') {
1658 :     foreach my $row (@{$self->{tree}{coords}}) {
1659 :     my @row = ();
1660 :     foreach my $x (@{$row->coords}) {push(@row,$x->string)}
1661 :     push(@array,[@row]);
1662 :     }
1663 :     } else {
1664 :     foreach my $x (@{$self->{tree}{coords}}) {push(@array,$x->string)}
1665 :     if ($self->{tree}{ColumnVector}) {foreach my $x (@array) {$x = [$x]}}
1666 :     else {@array = [@array]}
1667 :     }
1668 : dpvc 3273 Value::VERBATIM($self->format_matrix([@array],@{$self->{format_options}},tth_delims=>1));
1669 : dpvc 3269 }
1670 :    
1671 :     #
1672 :     # Get the size of the array and create the appropriate answer array
1673 :     #
1674 :     sub ANS_MATRIX {
1675 :     my $self = shift;
1676 :     my $extend = shift; my $name = shift;
1677 :     my $size = shift || 5; my $type = $self->type;
1678 :     my $cols = $self->length; my $rows = 1; my $sep = ',';
1679 :     if ($type eq 'Matrix') {
1680 :     $sep = ''; $rows = $cols; $cols = $self->{tree}->typeRef->{entryType}{length};
1681 :     }
1682 :     if ($self->{tree}{ColumnVector}) {
1683 :     $sep = ""; $type = "Matrix";
1684 :     my $tmp = $rows; $rows = $cols; $cols = $tmp;
1685 :     $self->{ColumnVector} = 1;
1686 :     }
1687 :     my $def = ($self->{context} || $$Value::context)->lists->get($type);
1688 :     my $open = $self->{open} || $self->{tree}{open} || $def->{open};
1689 :     my $close = $self->{close} || $self->{tree}{close} || $def->{close};
1690 :     $self->ans_matrix($extend,$name,$rows,$cols,$size,$open,$close,$sep);
1691 :     }
1692 :    
1693 :     sub ans_array {
1694 :     my $self = shift;
1695 :     return $self->SUPER::ans_array(@_) unless $self->array_OK;
1696 :     $self->ANS_MATRIX(0,'',@_);
1697 :     }
1698 :     sub named_ans_array {
1699 :     my $self = shift;
1700 :     return $self->SUPER::named_ans_array(@_) unless $self->array_OK;
1701 :     $self->ANS_MATRIX(0,@_);
1702 :     }
1703 :     sub named_ans_array_extension {
1704 :     my $self = shift;
1705 :     return $self->SUPER::named_ans_array_extension(@_) unless $self->array_OK;
1706 :     $self->ANS_MATRIX(1,@_);
1707 :     }
1708 :    
1709 :     sub array_OK {
1710 :     my $self = shift; my $tree = $self->{tree};
1711 :     return $tree->type =~ m/^(Point|Vector|Matrix)$/ && $tree->class eq 'List';
1712 :     }
1713 :    
1714 :     #
1715 :     # Get an array of values from a Matrix, Vector or Point
1716 :     #
1717 :     sub value {
1718 :     my $self = shift;
1719 :     my @array = ();
1720 :     if ($self->{tree}->type eq 'Matrix') {
1721 :     foreach my $row (@{$self->{tree}->coords}) {
1722 :     my @row = ();
1723 :     foreach my $x (@{$row->coords}) {push(@row,Value::Formula->new($x))}
1724 :     push(@array,[@row]);
1725 :     }
1726 :     } else {
1727 :     foreach my $x (@{$self->{tree}->coords}) {
1728 :     push(@array,Value::Formula->new($x));
1729 :     }
1730 :     }
1731 :     return @array;
1732 :     }
1733 :    
1734 : dpvc 2593 #############################################################
1735 :    
1736 :     1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9