| … | |
… | |
| 12 | ############################################################# |
12 | ############################################################# |
| 13 | |
13 | |
| 14 | package Value; |
14 | package Value; |
| 15 | |
15 | |
| 16 | # |
16 | # |
|
|
17 | # Context can add default values to the answer checkers by class; |
|
|
18 | # |
|
|
19 | $Value::defaultContext->{cmpDefaults} = {}; |
|
|
20 | |
|
|
21 | |
|
|
22 | # |
|
|
23 | # Default flags for the answer checkers |
|
|
24 | # |
|
|
25 | sub cmp_defaults {( |
|
|
26 | showTypeWarnings => 1, |
|
|
27 | showEqualErrors => 1, |
|
|
28 | ignoreStrings => 1, |
|
|
29 | studentsMustReduceUnions => 1, |
|
|
30 | showUnionReduceWarnings => 1, |
|
|
31 | )} |
|
|
32 | |
|
|
33 | # |
|
|
34 | # 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 | # |
| 17 | # Create an answer checker for the given type of object |
56 | # Create an answer checker for the given type of object |
| 18 | # |
57 | # |
| 19 | |
|
|
| 20 | our $cmp_defaults = { |
|
|
| 21 | showTypeWarnings => 1, |
|
|
| 22 | showEqualErrors => 1, |
|
|
| 23 | }; |
|
|
| 24 | |
|
|
| 25 | sub cmp { |
58 | sub cmp { |
| 26 | my $self = shift; |
59 | my $self = shift; |
| 27 | my $ans = new AnswerEvaluator; |
60 | my $ans = new AnswerEvaluator; |
| 28 | my $defaults = ref($self)."::cmp_defaults"; |
61 | my $correct = protectHTML($self->{correct_ans}); |
|
|
62 | $correct = $self->correct_ans unless defined($correct); |
|
|
63 | $self->{context} = $$Value::context unless defined($self->{context}); |
| 29 | $ans->ans_hash( |
64 | $ans->ans_hash( |
| 30 | type => "Value (".$self->class.")", |
65 | type => "Value (".$self->class.")", |
| 31 | correct_ans => $self->string, |
66 | correct_ans => $correct, |
| 32 | correct_value => $self, |
67 | correct_value => $self, |
| 33 | %{$$defaults || $cmp_defaults}, |
68 | $self->cmp_defaults(@_), |
|
|
69 | %{$self->{context}{cmpDefaults}{$self->class} || {}}, # context-specified defaults |
| 34 | @_ |
70 | @_ |
| 35 | ); |
71 | ); |
| 36 | $ans->install_evaluator( |
72 | $ans->{debug} = $ans->{rh_ans}{debug}; |
| 37 | sub { |
73 | $ans->install_evaluator(sub {$ans = shift; $ans->{correct_value}->cmp_parse($ans)}); |
| 38 | my $ans = shift; |
74 | $ans->install_pre_filter('erase') if $self->{ans_name}; # don't do blank check if answer_array |
| 39 | # can't seem to get $inputs_ref any other way |
75 | $self->cmp_diagnostics($ans); |
| 40 | $ans->{isPreview} = $self->getPG('$inputs_ref->{previewAnswers}'); |
|
|
| 41 | my $self = $ans->{correct_value}; |
|
|
| 42 | my $method = $ans->{cmp_check} || 'cmp_check'; |
|
|
| 43 | $self->$method($ans); |
|
|
| 44 | } |
|
|
| 45 | ); |
|
|
| 46 | return $ans; |
76 | return $ans; |
| 47 | } |
77 | } |
|
|
78 | |
|
|
79 | sub correct_ans {protectHTML(shift->string)} |
|
|
80 | sub cmp_diagnostics {} |
| 48 | |
81 | |
| 49 | # |
82 | # |
| 50 | # Parse the student answer and compute its value, |
83 | # Parse the student answer and compute its value, |
| 51 | # produce the preview strings, and then compare the |
84 | # produce the preview strings, and then compare the |
| 52 | # student and professor's answers for equality. |
85 | # student and professor's answers for equality. |
| 53 | # |
86 | # |
| 54 | sub cmp_check { |
87 | sub cmp_parse { |
| 55 | my $self = shift; my $ans = shift; |
88 | my $self = shift; my $ans = shift; |
| 56 | # |
89 | # |
| 57 | # Methods to call |
90 | # Do some setup |
| 58 | # |
91 | # |
| 59 | my $cmp_equal = $ans->{cmp_equal} || 'cmp_equal'; |
92 | my $current = $$Value::context; # save it for later |
| 60 | my $cmp_error = $ans->{cmp_error} || 'cmp_error'; |
93 | my $context = $ans->{correct_value}{context} || $current; |
| 61 | my $cmp_postprocess = $ans->{cmp_postprocess}; |
94 | Parser::Context->current(undef,$context); # change to correct answser's context |
|
|
95 | my $flags = contextSet($context,$self->cmp_contextFlags($ans)); # save old context flags |
|
|
96 | my $inputs = $self->getPG('$inputs_ref',{action=>""}); |
|
|
97 | $ans->{isPreview} = $inputs->{previewAnswers} || ($inputs->{action} =~ m/^Preview/); |
|
|
98 | $ans->{cmp_class} = $self->cmp_class($ans) unless $ans->{cmp_class}; |
|
|
99 | $ans->{error_message} = $ans->{ans_message} = ''; # clear any old messages |
|
|
100 | $ans->{preview_latex_string} = $ans->{preview_text_string} = ''; |
|
|
101 | |
| 62 | # |
102 | # |
| 63 | # Parse and evaluate the student answer |
103 | # Parse and evaluate the student answer |
| 64 | # |
104 | # |
| 65 | $ans->score(0); # assume failure |
105 | $ans->score(0); # assume failure |
| 66 | my $vars = $$Value::context->{variables}; |
|
|
| 67 | $$Value::context->{variables} = {}; # pretend there are no variables |
|
|
| 68 | $ans->{student_formula} = Parser::Formula($ans->{student_ans}); |
106 | $ans->{student_value} = $ans->{student_formula} = Parser::Formula($ans->{student_ans}); |
| 69 | $ans->{student_value} = Parser::Evaluate($ans->{student_formula}); |
107 | $ans->{student_value} = Parser::Evaluate($ans->{student_formula}) |
| 70 | $$Value::context->{variables} = $vars; |
108 | if defined($ans->{student_formula}) && $ans->{student_formula}->isConstant; |
|
|
109 | |
| 71 | # |
110 | # |
| 72 | # If it parsed OK, save the output forms and check if it is correct |
111 | # If it parsed OK, save the output forms and check if it is correct |
| 73 | # otherwise report an error |
112 | # otherwise report an error |
| 74 | # |
113 | # |
| 75 | if (defined $ans->{student_value}) { |
114 | if (defined $ans->{student_value}) { |
| 76 | $ans->{student_value} = Value::Formula->new($ans->{student_value}) |
115 | $ans->{student_value} = Value::Formula->new($ans->{student_value}) |
| 77 | unless Value::isValue($ans->{student_value}); |
116 | unless Value::isValue($ans->{student_value}); |
| 78 | $ans->{preview_latex_string} = $ans->{student_formula}->TeX; |
117 | $ans->{preview_latex_string} = $ans->{student_formula}->TeX; |
| 79 | $ans->{preview_text_string} = $ans->{student_formula}->string; |
118 | $ans->{preview_text_string} = protectHTML($ans->{student_formula}->string); |
| 80 | $ans->{student_ans} = $ans->{student_value}->stringify; |
119 | # |
|
|
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 | if ($self->cmp_collect($ans)) { |
| 81 | $self->$cmp_equal($ans); |
133 | $self->cmp_equal($ans); |
| 82 | $self->$cmp_postprocess($ans) if $cmp_postprocess && !$ans->{error_message}; |
134 | $self->cmp_postprocess($ans) if !$ans->{error_message} && !$ans->{typeError}; |
|
|
135 | $self->cmp_diagnostics($ans); |
|
|
136 | } |
| 83 | } else { |
137 | } else { |
|
|
138 | $self->cmp_collect($ans); |
| 84 | $self->$cmp_error($ans); |
139 | $self->cmp_error($ans); |
| 85 | } |
140 | } |
|
|
141 | contextSet($context,%{$flags}); # restore context values |
|
|
142 | Parser::Context->current(undef,$current); # put back the old context |
| 86 | return $ans; |
143 | return $ans; |
|
|
144 | } |
|
|
145 | |
|
|
146 | # |
|
|
147 | # 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 | {Parser::reportEvalError($@); $self->cmp_error($ans); return 0} |
|
|
167 | $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; |
| 87 | } |
175 | } |
| 88 | |
176 | |
| 89 | # |
177 | # |
| 90 | # Check if the parsed student answer equals the professor's answer |
178 | # Check if the parsed student answer equals the professor's answer |
| 91 | # |
179 | # |
| 92 | sub cmp_equal { |
180 | sub cmp_equal { |
| 93 | my $self = shift; my $ans = shift; |
181 | my $self = shift; my $ans = shift; |
|
|
182 | my $correct = $ans->{correct_value}; |
|
|
183 | my $student = $ans->{student_value}; |
| 94 | if ($ans->{correct_value}->typeMatch($ans->{student_value},$ans)) { |
184 | if ($correct->typeMatch($student,$ans)) { |
| 95 | my $equal = eval {$ans->{correct_value} == $ans->{student_value}}; |
185 | my $equal = $correct->cmp_compare($student,$ans); |
| 96 | if (defined($equal) || !$ans->{showEqualErrors}) {$ans->score(1) if $equal; return} |
186 | if (defined($equal) || !$ans->{showEqualErrors}) {$ans->score(1) if $equal; return} |
| 97 | my $cmp_error = $ans->{cmp_error} || 'cmp_error'; |
|
|
| 98 | $self->$cmp_error($ans); |
187 | $self->cmp_error($ans); |
| 99 | } else { |
188 | } else { |
|
|
189 | return if $ans->{ignoreStrings} && (!Value::isValue($student) || $student->type eq 'String'); |
|
|
190 | $ans->{typeError} = 1; |
| 100 | $ans->{ans_message} = $ans->{error_message} = |
191 | $ans->{ans_message} = $ans->{error_message} = |
| 101 | "Your answer isn't ".lc($ans->{correct_value}->showClass). |
192 | "Your answer isn't ".lc($ans->{cmp_class})."\n". |
| 102 | " (it looks like ".lc($ans->{student_value}->showClass).")" |
193 | "(it looks like ".lc($student->showClass).")" |
| 103 | if !$ans->{isPreview} && $ans->{showTypeWarnings} && !$ans->{error_message}; |
194 | if !$ans->{isPreview} && $ans->{showTypeWarnings} && !$ans->{error_message}; |
| 104 | } |
195 | } |
| 105 | } |
196 | } |
|
|
197 | |
|
|
198 | # |
|
|
199 | # Perform the comparison, either using the checker supplied |
|
|
200 | # by the answer evaluator, or the overloaded == operator. |
|
|
201 | # |
|
|
202 | |
|
|
203 | our $CMP_ERROR = 2; # a fatal error was detected |
|
|
204 | our $CMP_WARNING = 3; # a warning was produced |
|
|
205 | |
|
|
206 | sub cmp_compare { |
|
|
207 | my $self = shift; my $other = shift; my $ans = shift; my $nth = shift || ''; |
|
|
208 | 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 | } |
|
|
215 | return $equal; |
|
|
216 | } |
|
|
217 | |
|
|
218 | sub cmp_list_compare {Value::List::cmp_list_compare(@_)} |
| 106 | |
219 | |
| 107 | # |
220 | # |
| 108 | # Check if types are compatible for equality check |
221 | # Check if types are compatible for equality check |
| 109 | # |
222 | # |
| 110 | sub typeMatch { |
223 | sub typeMatch { |
| 111 | my $self = shift; my $other = shift; |
224 | my $self = shift; my $other = shift; |
| 112 | return 1 unless ref($other); |
225 | return 1 unless ref($other); |
| 113 | $self->type eq $other->type; |
226 | $self->type eq $other->type && $other->class ne 'Formula'; |
|
|
227 | } |
|
|
228 | |
|
|
229 | # |
|
|
230 | # Class name for cmp error messages |
|
|
231 | # |
|
|
232 | sub cmp_class { |
|
|
233 | my $self = shift; my $ans = shift; |
|
|
234 | my $class = $self->showClass; $class =~ s/Real //; |
|
|
235 | return $class if $class =~ m/Formula/; |
|
|
236 | return "an Interval, Set or Union" if $self->isSetOfReals; |
|
|
237 | return $class; |
| 114 | } |
238 | } |
| 115 | |
239 | |
| 116 | # |
240 | # |
| 117 | # Student answer evaluation failed. |
241 | # Student answer evaluation failed. |
| 118 | # Report the error, with formatting, if possible. |
242 | # Report the error, with formatting, if possible. |
| 119 | # |
243 | # |
| 120 | sub cmp_error { |
244 | sub cmp_error { |
| 121 | my $self = shift; my $ans = shift; |
245 | my $self = shift; my $ans = shift; |
| 122 | my $context = $$Value::context; |
246 | my $error = $$Value::context->{error}; |
| 123 | my $message = $context->{error}{message}; |
247 | my $message = $error->{message}; |
| 124 | if ($context->{error}{pos}) { |
248 | if ($error->{pos}) { |
| 125 | my $string = $context->{error}{string}; |
249 | my $string = $error->{string}; |
| 126 | my ($s,$e) = @{$context->{error}{pos}}; |
250 | my ($s,$e) = @{$error->{pos}}; |
| 127 | $message =~ s/; see.*//; # remove the position from the message |
251 | $message =~ s/; see.*//; # remove the position from the message |
| 128 | $ans->{student_ans} = |
252 | $ans->{student_ans} = |
| 129 | protectHTML(substr($string,0,$s)) . |
253 | protectHTML(substr($string,0,$s)) . |
| 130 | '<SPAN CLASS="parsehilight">' . |
254 | '<SPAN CLASS="parsehilight">' . |
| 131 | protectHTML(substr($string,$s,$e-$s)) . |
255 | protectHTML(substr($string,$s,$e-$s)) . |
| 132 | '</SPAN>' . |
256 | '</SPAN>' . |
| 133 | protectHTML(substr($string,$e)); |
257 | protectHTML(substr($string,$e)); |
| 134 | } |
258 | } |
|
|
259 | $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; |
| 135 | $ans->score(0); |
268 | $ans->score(0); |
|
|
269 | $ans->{ans_message} = $ans->{error_message} = join("\n",@_); |
|
|
270 | } |
|
|
271 | |
|
|
272 | # |
|
|
273 | # filled in by sub-classes |
|
|
274 | # |
|
|
275 | sub cmp_postprocess {} |
|
|
276 | |
|
|
277 | # |
|
|
278 | # Check for unreduced reduced Unions and Sets |
|
|
279 | # |
|
|
280 | sub cmp_checkUnionReduce { |
|
|
281 | my $self = shift; my $student = shift; my $ans = shift; my $nth = shift || ''; |
|
|
282 | return unless $ans->{studentsMustReduceUnions} && |
|
|
283 | $ans->{showUnionReduceWarnings} && |
|
|
284 | !$ans->{isPreview} && !Value::isFormula($student); |
|
|
285 | if ($student->type eq 'Union' && $student->length >= 2) { |
|
|
286 | my $reduced = $student->reduce; |
|
|
287 | return "Your$nth union can be written without overlaps" |
|
|
288 | unless $reduced->type eq 'Union' && $reduced->length == $student->length; |
|
|
289 | my @R = $reduced->sort->value; |
|
|
290 | my @S = $student->sort->value; |
|
|
291 | foreach my $i (0..$#R) { |
|
|
292 | return "Your$nth union can be written without overlaps" |
|
|
293 | unless $R[$i] == $S[$i] && $R[$i]->length == $S[$i]->length; |
|
|
294 | } |
|
|
295 | } elsif ($student->type eq 'Set' && $student->length >= 2) { |
|
|
296 | return "Your$nth set should have no repeated elements" |
|
|
297 | unless $student->reduce->length == $student->length; |
|
|
298 | } |
|
|
299 | return; |
|
|
300 | } |
|
|
301 | |
|
|
302 | # |
|
|
303 | # 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 | my %options = (open=>'.',close=>'.',sep=>'',@_); |
|
|
369 | $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 | $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 | 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 | $HTML .= '<TR ALIGN="MIDDLE"><TD>'.join($sep,EVALUATE(@{$array->[$i]})).'</TD></TR>'."\n"; |
|
|
394 | } |
|
|
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 | sub EVALUATE {map {(Value::isFormula($_) && $_->isConstant? $_->eval: $_)} @_} |
|
|
415 | |
|
|
416 | 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 | # |
|
|
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 | '[' => ['','','',''], |
|
|
443 | ']' => ['','','',''], |
|
|
444 | '(' => ['','','',''], |
|
|
445 | ')' => ['','','',''], |
|
|
446 | '{' => ['','','',''], |
|
|
447 | '}' => ['','','',''], |
|
|
448 | '|' => ['|','','|','|'], |
|
|
449 | '<' => ['<'], |
|
|
450 | '>' => ['>'], |
|
|
451 | '\lgroup' => ['','','',''], |
|
|
452 | '\rgroup' => ['','','',''], |
|
|
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 | my @ans_cmp_defaults = (showCoodinateHints => 0, checker => sub {0}); |
|
|
486 | |
|
|
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 | my @row = (); my $entry; |
|
|
497 | foreach my $j (0..$cols-1) { |
|
|
498 | if ($i || $j) { |
|
|
499 | $entry = $inputs->{ANS_NAME($self->{ans_name},$i,$j)}; |
|
|
500 | } else { |
|
|
501 | $entry = $ans->{original_student_ans}; |
|
|
502 | $ans->{student_formula} = $ans->{student_value} = undef unless $entry =~ m/\S/; |
|
|
503 | } |
|
|
504 | 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 | } |
|
|
509 | push(@array,[@row]); |
|
|
510 | } |
|
|
511 | $ans->{student_formula} = [@array]; |
| 136 | $ans->{ans_message} = $ans->{error_message} = $message; |
512 | $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 | } |
|
|
522 | |
|
|
523 | sub entryMessage { |
|
|
524 | my $message = shift; return unless $message; |
|
|
525 | 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 | push(@{$errors},"<TR VALIGN=\"TOP\"><TD NOWRAP STYLE=\"text-align:right; border:0px\"><I>$title</I>: </TD>". |
|
|
531 | "<TD STYLE=\"text-align:left; border:0px\">$message</TD></TR>"); |
|
|
532 | } |
|
|
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 | # 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; |
| 137 | } |
553 | } |
| 138 | |
554 | |
| 139 | # |
555 | # |
| 140 | # Quote HTML characters |
556 | # Quote HTML characters |
| 141 | # |
557 | # |
| 142 | sub protectHTML { |
558 | sub protectHTML { |
| 143 | my $string = shift; |
559 | my $string = shift; |
|
|
560 | return unless defined($string); |
|
|
561 | return $string if eval ('$main::displayMode') eq 'TeX'; |
| 144 | $string =~ s/&/\&/g; |
562 | $string =~ s/&/\&/g; |
| 145 | $string =~ s/</\</g; |
563 | $string =~ s/</\</g; |
| 146 | $string =~ s/>/\>/g; |
564 | $string =~ s/>/\>/g; |
| 147 | $string; |
565 | $string; |
| 148 | } |
566 | } |
| 149 | |
567 | |
| 150 | # |
568 | # |
|
|
569 | # 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 | # |
| 151 | # Get a value from the safe compartment |
580 | # Get a value from the safe compartment |
| 152 | # |
581 | # |
| 153 | sub getPG { |
582 | sub getPG { |
| 154 | my $self = shift; |
583 | my $self = shift; |
| 155 | (WeBWorK::PG::Translator::PG_restricted_eval(shift))[0]; |
584 | # (WeBWorK::PG::Translator::PG_restricted_eval(shift))[0]; |
|
|
585 | eval ('package main; '.shift); # faster |
| 156 | } |
586 | } |
| 157 | |
587 | |
| 158 | ############################################################# |
588 | ############################################################# |
| 159 | ############################################################# |
589 | ############################################################# |
| 160 | |
590 | |
| 161 | package Value::Real; |
591 | package Value::Real; |
| 162 | |
592 | |
| 163 | our $cmp_defaults = { |
593 | sub cmp_defaults {( |
| 164 | %{$Value::cmp_defaults}, |
594 | shift->SUPER::cmp_defaults(@_), |
| 165 | ignoreStrings => 1, |
595 | ignoreInfinity => 1, |
| 166 | }; |
596 | )} |
| 167 | |
597 | |
| 168 | sub typeMatch { |
598 | sub typeMatch { |
| 169 | my $self = shift; my $other = shift; my $ans = shift; |
599 | my $self = shift; my $other = shift; my $ans = shift; |
| 170 | return 1 unless ref($other); |
600 | return 1 unless ref($other); |
| 171 | if ($other->type eq 'String' && $ans->{ignoreStrings}) { |
601 | return 0 if Value::isFormula($other); |
| 172 | $ans->{showEqualErrors} = 0; |
602 | return 1 if $other->type eq 'Infinity' && $ans->{ignoreInfinity}; |
| 173 | return 1; |
|
|
| 174 | } |
|
|
| 175 | $self->type eq $other->type; |
603 | $self->type eq $other->type; |
| 176 | } |
604 | } |
| 177 | |
605 | |
| 178 | ############################################################# |
606 | ############################################################# |
| 179 | |
607 | |
| 180 | package Value::Point; |
608 | package Value::Infinity; |
| 181 | |
609 | |
| 182 | our $cmp_defaults = { |
610 | sub cmp_class {'a Number'}; |
| 183 | %{$Value::cmp_defaults}, |
|
|
| 184 | showDimensionWarnings => 1, |
|
|
| 185 | }; |
|
|
| 186 | |
611 | |
| 187 | sub typeMatch { |
612 | sub typeMatch { |
| 188 | my $self = shift; my $other = shift; my $ans = shift; |
613 | my $self = shift; my $other = shift; my $ans = shift; |
| 189 | return 0 unless ref($other); |
614 | return 1 unless ref($other); |
| 190 | return 0 unless $other->type eq 'Point'; |
615 | return 0 if Value::isFormula($other); |
| 191 | if (!$ans->{isPreview} && $ans->{showDimensionWarnings} && |
616 | return 1 if $other->type eq 'Number'; |
| 192 | $self->length != $other->length) { |
617 | $self->type eq $other->type; |
| 193 | $ans->{ans_message} = $ans->{error_message} = "The dimension is incorrect"; |
|
|
| 194 | return 0; |
|
|
| 195 | } |
|
|
| 196 | return 1; |
|
|
| 197 | } |
618 | } |
| 198 | |
619 | |
| 199 | ############################################################# |
620 | ############################################################# |
| 200 | |
621 | |
|
|
622 | package Value::String; |
|
|
623 | |
|
|
624 | sub cmp_defaults {( |
|
|
625 | Value::Real->cmp_defaults(@_), |
|
|
626 | typeMatch => 'Value::Real', |
|
|
627 | )} |
|
|
628 | |
|
|
629 | sub cmp_class { |
|
|
630 | my $self = shift; my $ans = shift; my $typeMatch = $ans->{typeMatch}; |
|
|
631 | return 'a Word' if !Value::isValue($typeMatch) || $typeMatch->class eq 'String'; |
|
|
632 | return $typeMatch->cmp_class; |
|
|
633 | }; |
|
|
634 | |
|
|
635 | sub typeMatch { |
|
|
636 | my $self = shift; my $other = shift; my $ans = shift; |
|
|
637 | # return 0 if ref($other) && Value::isFormula($other); |
|
|
638 | my $typeMatch = $ans->{typeMatch}; |
|
|
639 | return &$typeMatch($other,$ans) if ref($typeMatch) eq 'CODE'; |
|
|
640 | return 1 if !Value::isValue($typeMatch) || $typeMatch->class eq 'String' || |
|
|
641 | $self->type eq $other->type; |
|
|
642 | return $typeMatch->typeMatch($other,$ans); |
|
|
643 | } |
|
|
644 | |
|
|
645 | # |
|
|
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 | ############################################################# |
|
|
665 | |
|
|
666 | package Value::Point; |
|
|
667 | |
|
|
668 | sub cmp_defaults {( |
|
|
669 | shift->SUPER::cmp_defaults(@_), |
|
|
670 | showDimensionHints => 1, |
|
|
671 | showCoordinateHints => 1, |
|
|
672 | )} |
|
|
673 | |
|
|
674 | sub typeMatch { |
|
|
675 | my $self = shift; my $other = shift; my $ans = shift; |
|
|
676 | return ref($other) && $other->type eq 'Point' && $other->class ne 'Formula'; |
|
|
677 | } |
|
|
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 | 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 | $self->cmp_Error($ans,"The number of coordinates is incorrect"); return; |
|
|
689 | } |
|
|
690 | if ($ans->{showCoordinateHints}) { |
|
|
691 | my @errors; |
|
|
692 | foreach my $i (1..$self->length) { |
|
|
693 | push(@errors,"The ".$self->NameForNumber($i)." coordinate is incorrect") |
|
|
694 | if ($self->{data}[$i-1] != $student->{data}[$i-1]); |
|
|
695 | } |
|
|
696 | $self->cmp_Error($ans,@errors); return; |
|
|
697 | } |
|
|
698 | } |
|
|
699 | |
|
|
700 | sub correct_ans { |
|
|
701 | my $self = shift; |
|
|
702 | return $self->SUPER::correct_ans unless $self->{ans_name}; |
|
|
703 | Value::VERBATIM($self->format_matrix([[@{$self->{data}}]],@{$self->{format_options}},tth_delims=>1)); |
|
|
704 | } |
|
|
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 | ############################################################# |
|
|
720 | |
| 201 | package Value::Vector; |
721 | package Value::Vector; |
| 202 | |
722 | |
| 203 | our $cmp_defaults = { |
723 | sub cmp_defaults {( |
| 204 | %{$Value::cmp_defaults}, |
724 | shift->SUPER::cmp_defaults(@_), |
| 205 | showDimensionWarnings => 1, |
725 | showDimensionHints => 1, |
|
|
726 | showCoordinateHints => 1, |
| 206 | promotePoints => 0, |
727 | promotePoints => 0, |
| 207 | parallel => 0, |
728 | parallel => 0, |
| 208 | sameDirection => 0, |
729 | sameDirection => 0, |
| 209 | cmp_postprocess => 'cmp_postprocess', |
730 | )} |
| 210 | }; |
|
|
| 211 | |
731 | |
| 212 | sub typeMatch { |
732 | sub typeMatch { |
| 213 | my $self = shift; my $other = shift; my $ans = shift; |
733 | my $self = shift; my $other = shift; my $ans = shift; |
| 214 | return 0 unless ref($other); |
734 | return 0 unless ref($other) && $other->class ne 'Formula'; |
| 215 | return 0 unless $other->type eq 'Vector' || |
735 | return $other->type eq 'Vector' || |
| 216 | ($ans->{promotePoints} && $other->type eq 'Point'); |
736 | ($ans->{promotePoints} && $other->type eq 'Point'); |
| 217 | if (!$ans->{isPreview} && $ans->{showDimensionWarnings} && |
|
|
| 218 | $self->length != $other->length) { |
|
|
| 219 | $ans->{ans_message} = $ans->{error_message} = "The dimension is incorrect"; |
|
|
| 220 | return 0; |
|
|
| 221 | } |
|
|
| 222 | return 1; |
|
|
| 223 | } |
737 | } |
| 224 | |
738 | |
| 225 | # |
739 | # |
| 226 | # Handle check for parallel vectors |
740 | # check for dimension mismatch |
|
|
741 | # for parallel vectors, and |
|
|
742 | # for incorrect coordinates |
| 227 | # |
743 | # |
| 228 | sub cmp_postprocess { |
744 | sub cmp_postprocess { |
| 229 | my $self = shift; my $ans = shift; |
745 | my $self = shift; my $ans = shift; |
| 230 | return unless $ans->{parallel} && $ans->{score} == 0; |
746 | return unless $ans->{score} == 0 && !$ans->{isPreview}; |
| 231 | $ans->score(1) if $self->isParallel($ans->{student_value},$ans->{sameDirection}); |
747 | my $student = $ans->{student_value}; |
|
|
748 | return if $ans->{ignoreStrings} && (!Value::isValue($student) || $student->type eq 'String'); |
|
|
749 | if ($ans->{showDimensionHints} && $self->length != $student->length) { |
|
|
750 | $self->cmp_Error($ans,"The number of coordinates is incorrect"); return; |
|
|
751 | } |
|
|
752 | if ($ans->{parallel} && $student->class ne 'String' && |
|
|
753 | $self->isParallel($student,$ans->{sameDirection})) { |
|
|
754 | $ans->score(1); return; |
|
|
755 | } |
|
|
756 | if ($ans->{showCoordinateHints} && !$ans->{parallel}) { |
|
|
757 | my @errors; |
|
|
758 | foreach my $i (1..$self->length) { |
|
|
759 | push(@errors,"The ".$self->NameForNumber($i)." coordinate is incorrect") |
|
|
760 | if ($self->{data}[$i-1] != $student->{data}[$i-1]); |
|
|
761 | } |
|
|
762 | $self->cmp_Error($ans,@errors); return; |
|
|
763 | } |
| 232 | } |
764 | } |
| 233 | |
765 | |
|
|
766 | sub correct_ans { |
|
|
767 | my $self = shift; |
|
|
768 | return $self->SUPER::correct_ans unless $self->{ans_name}; |
|
|
769 | return Value::VERBATIM($self->format_matrix([[$self->value]],@{$self->{format_options}},tth_delims=>1)) |
|
|
770 | unless $self->{ColumnVector}; |
|
|
771 | my @array = (); foreach my $x ($self->value) {push(@array,[$x])} |
|
|
772 | return Value::VERBATIM($self->format_matrix([@array],@{$self->{format_options}},tth_delims=>1)); |
|
|
773 | } |
|
|
774 | |
|
|
775 | 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 | |
|
|
788 | 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,@_)} |
| 234 | |
791 | |
| 235 | |
792 | |
| 236 | ############################################################# |
793 | ############################################################# |
| 237 | |
794 | |
| 238 | package Value::Matrix; |
795 | package Value::Matrix; |
| 239 | |
796 | |
| 240 | our $cmp_defaults = { |
797 | sub cmp_defaults {( |
| 241 | %{$Value::cmp_defaults}, |
798 | shift->SUPER::cmp_defaults(@_), |
| 242 | showDimensionWarnings => 1, |
799 | showDimensionHints => 1, |
| 243 | }; |
800 | showEqualErrors => 0, |
|
|
801 | )} |
| 244 | |
802 | |
| 245 | sub typeMatch { |
803 | sub typeMatch { |
| 246 | my $self = shift; my $other = shift; my $ans = shift; |
804 | my $self = shift; my $other = shift; my $ans = shift; |
| 247 | return 0 unless ref($other); |
805 | return 0 unless ref($other) && $other->class ne 'Formula'; |
| 248 | $other = $self->make($other->{data}) if $other->class eq 'Point'; |
|
|
| 249 | return 0 unless $other->type eq 'Matrix'; |
806 | return $other->type eq 'Matrix' || |
| 250 | return 1 unless $ans->{showDimensionWarnings}; |
807 | ($other->type =~ m/^(Point|list)$/ && |
|
|
808 | $other->{open}.$other->{close} eq $self->{open}.$self->{close}); |
|
|
809 | } |
|
|
810 | |
|
|
811 | sub cmp_postprocess { |
|
|
812 | my $self = shift; my $ans = shift; |
|
|
813 | return unless $ans->{score} == 0 && |
|
|
814 | !$ans->{isPreview} && $ans->{showDimensionHints}; |
|
|
815 | my $student = $ans->{student_value}; |
|
|
816 | return if $ans->{ignoreStrings} && (!Value::isValue($student) || $student->type eq 'String'); |
| 251 | my @d1 = $self->dimensions; my @d2 = $other->dimensions; |
817 | my @d1 = $self->dimensions; my @d2 = $student->dimensions; |
| 252 | if (scalar(@d1) != scalar(@d2)) { |
818 | if (scalar(@d1) != scalar(@d2)) { |
| 253 | $ans->{ans_message} = $ans->{error_message} = |
|
|
| 254 | "Matrix dimension is not correct"; |
819 | $self->cmp_Error($ans,"Matrix dimension is not correct"); |
| 255 | return 0; |
820 | return; |
| 256 | } else { |
821 | } else { |
| 257 | foreach my $i (0..scalar(@d1)-1) { |
822 | foreach my $i (0..scalar(@d1)-1) { |
| 258 | if ($d1[$i] != $d2[$i]) { |
823 | if ($d1[$i] != $d2[$i]) { |
| 259 | $ans->{ans_message} = $ans->{error_message} = |
824 | $self->cmp_Error($ans,"Matrix dimension is not correct"); |
| 260 | "Matrix dimension is not correct"; |
|
|
| 261 | return 0; |
825 | return; |
| 262 | } |
826 | } |
| 263 | } |
827 | } |
| 264 | } |
828 | } |
| 265 | return 1; |
|
|
| 266 | } |
829 | } |
|
|
830 | |
|
|
831 | 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 | Value::VERBATIM($self->format_matrix([$self->value],@{$self->{format_options}},tth_delims=>1)); |
|
|
836 | } |
|
|
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 | Value::Error("Can't create ans_array for %d-dimensional matrix",scalar(@d)) |
|
|
846 | 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,@_)} |
| 267 | |
854 | |
| 268 | ############################################################# |
855 | ############################################################# |
| 269 | |
856 | |
| 270 | package Value::Interval; |
857 | package Value::Interval; |
| 271 | |
858 | |
| 272 | ## @@@ report interval-type mismatch? @@@ |
859 | sub cmp_defaults {( |
|
|
860 | shift->SUPER::cmp_defaults(@_), |
|
|
861 | showEndpointHints => 1, |
|
|
862 | showEndTypeHints => 1, |
|
|
863 | requireParenMatch => 1, |
|
|
864 | )} |
| 273 | |
865 | |
| 274 | sub typeMatch { |
866 | sub typeMatch { |
| 275 | my $self = shift; my $other = shift; |
867 | my $self = shift; my $other = shift; |
| 276 | return 0 unless ref($other); |
868 | return 0 if !Value::isValue($other) || $other->isFormula; |
|
|
869 | return $other->canBeInUnion; |
|
|
870 | } |
|
|
871 | |
|
|
872 | # |
|
|
873 | # Check for unreduced sets and unions |
|
|
874 | # |
|
|
875 | sub cmp_compare { |
|
|
876 | my $self = shift; my $student = shift; my $ans = shift; |
|
|
877 | my $error = $self->cmp_checkUnionReduce($student,$ans,@_); |
|
|
878 | if ($error) {$$Value::context->setError($error,'',undef,undef,$CMP_WARNING); return} |
|
|
879 | $self->SUPER::cmp_compare($student,$ans,@_); |
|
|
880 | } |
|
|
881 | |
|
|
882 | # |
|
|
883 | # 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 | return if $ans->{ignoreStrings} && (!Value::isValue($other) || $other->type eq 'String'); |
|
|
890 | return unless $other->class eq 'Interval'; |
|
|
891 | 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 | if (scalar(@errors) == 0 && $ans->{showEndTypeHints} && $ans->{requireParenMatch}) { |
|
|
899 | 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 | ############################################################# |
|
|
906 | |
|
|
907 | package Value::Set; |
|
|
908 | |
|
|
909 | sub typeMatch { |
|
|
910 | my $self = shift; my $other = shift; |
|
|
911 | return 0 if !Value::isValue($other) || $other->isFormula; |
|
|
912 | return $other->canBeInUnion; |
|
|
913 | } |
|
|
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 | # error messages). |
|
|
933 | # |
|
|
934 | sub cmp_equal { |
|
|
935 | my ($self,$ans) = @_; |
|
|
936 | return Value::List::cmp_equal(@_) if $ans->{student_value}->type eq 'Set'; |
|
|
937 | $self->SUPER::cmp_equal($ans); |
|
|
938 | } |
|
|
939 | |
|
|
940 | # |
|
|
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 | if ($error) {$$Value::context->setError($error,'',undef,undef,$CMP_WARNING); return} |
|
|
947 | $self->SUPER::cmp_compare($student,$ans,@_); |
|
|
948 | } |
|
|
949 | |
|
|
950 | ############################################################# |
|
|
951 | |
|
|
952 | package Value::Union; |
|
|
953 | |
|
|
954 | sub typeMatch { |
|
|
955 | my $self = shift; my $other = shift; |
|
|
956 | return 0 unless ref($other) && $other->class ne 'Formula'; |
| 277 | return $other->length == 2 && |
957 | return $other->length == 2 && |
| 278 | ($other->{open} eq '(' || $other->{open} eq '[') && |
958 | ($other->{open} eq '(' || $other->{open} eq '[') && |
| 279 | ($other->{close} eq ')' || $other->{close} eq ']') |
959 | ($other->{close} eq ')' || $other->{close} eq ']') |
| 280 | if $other->type =~ m/^(Point|List)$/; |
960 | if $other->type =~ m/^(Point|List)$/; |
| 281 | $other->type =~ m/^(Interval|Union)$/; |
961 | $other->isSetOfReals; |
| 282 | } |
962 | } |
| 283 | |
963 | |
| 284 | ############################################################# |
964 | # |
| 285 | |
965 | # Use the List checker for unions, in order to get |
| 286 | package Value::Union; |
966 | # partial credit. Set the various types for error |
| 287 | |
967 | # messages. |
| 288 | sub typeMatch { |
968 | # |
| 289 | my $self = shift; my $other = shift; |
|
|
| 290 | return 0 unless ref($other); |
|
|
| 291 | return $other->length == 2 && |
|
|
| 292 | ($other->{open} eq '(' || $other->{open} eq '[') && |
|
|
| 293 | ($other->{close} eq ')' || $other->{close} eq ']') |
|
|
| 294 | if $other->type =~ m/^(Point|List)$/; |
|
|
| 295 | $other->type =~ m/^(Interval|Union)/; |
|
|
| 296 | } |
|
|
| 297 | |
|
|
| 298 | ############################################################# |
|
|
| 299 | |
|
|
| 300 | package Value::List; |
|
|
| 301 | |
|
|
| 302 | our $cmp_defaults = { |
969 | sub cmp_defaults {( |
| 303 | %{$Value::cmp_defaults}, |
970 | Value::List::cmp_defaults(@_), |
| 304 | showHints => undef, |
971 | typeMatch => 'Value::Interval', |
| 305 | showLengthHints => undef, |
972 | list_type => 'an interval, set or union', |
| 306 | # partialCredit => undef, |
973 | short_type => 'a union', |
| 307 | partialCredit => 0, # only allow this once WW can deal with partial credit |
974 | entry_type => 'an interval or set', |
| 308 | ordered => 0, |
975 | )} |
| 309 | entry_type => undef, |
|
|
| 310 | list_type => undef, |
|
|
| 311 | typeMatch => undef, |
|
|
| 312 | allowParens => 0, |
|
|
| 313 | }; |
|
|
| 314 | |
|
|
| 315 | sub typeMatch {1} |
|
|
| 316 | |
976 | |
| 317 | sub cmp_equal { |
977 | sub cmp_equal { |
| 318 | my $self = shift; my $ans = shift; |
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 | |
|
|
984 | # |
|
|
985 | # Check for unreduced sets and unions |
|
|
986 | # |
|
|
987 | sub cmp_compare { |
|
|
988 | my $self = shift; my $student = shift; my $ans = shift; |
|
|
989 | my $error = $self->cmp_checkUnionReduce($student,$ans,@_); |
|
|
990 | if ($error) {$$Value::context->setError($error,'',undef,undef,$CMP_WARNING); return} |
|
|
991 | $self->SUPER::cmp_compare($student,$ans,@_); |
|
|
992 | } |
|
|
993 | |
|
|
994 | ############################################################# |
|
|
995 | |
|
|
996 | package Value::List; |
|
|
997 | |
|
|
998 | sub cmp_defaults { |
|
|
999 | my $self = shift; |
|
|
1000 | my %options = (@_); |
|
|
1001 | my $element = Value::makeValue($self->{data}[0]); |
|
|
1002 | $element = Value::Formula->new($element) unless Value::isValue($element); |
|
|
1003 | return ( |
|
|
1004 | Value::Real->cmp_defaults(@_), |
|
|
1005 | showHints => undef, |
|
|
1006 | showLengthHints => undef, |
|
|
1007 | showParenHints => undef, |
|
|
1008 | partialCredit => undef, |
|
|
1009 | ordered => 0, |
|
|
1010 | entry_type => undef, |
|
|
1011 | list_type => undef, |
|
|
1012 | typeMatch => $element, |
|
|
1013 | firstElement => $element, |
|
|
1014 | extra => undef, |
|
|
1015 | requireParenMatch => 1, |
|
|
1016 | removeParens => 1, |
|
|
1017 | implicitList => 1, |
|
|
1018 | ); |
|
|
1019 | } |
|
|
1020 | |
|
|
1021 | # |
|
|
1022 | # Match anything but formulas |
|
|
1023 | # |
|
|
1024 | sub typeMatch {return !ref($other) || $other->class ne 'Formula'} |
|
|
1025 | |
|
|
1026 | # |
|
|
1027 | # Handle removal of outermost parens in correct answer. |
|
|
1028 | # |
|
|
1029 | sub cmp { |
|
|
1030 | my $self = shift; |
|
|
1031 | my %params = @_; |
|
|
1032 | my $cmp = $self->SUPER::cmp(@_); |
|
|
1033 | if ($cmp->{rh_ans}{removeParens}) { |
|
|
1034 | $self->{open} = $self->{close} = ''; |
|
|
1035 | $cmp->ans_hash(correct_ans => $self->stringify) |
|
|
1036 | unless defined($self->{correct_ans}) || defined($params{correct_ans}); |
|
|
1037 | } |
|
|
1038 | return $cmp; |
|
|
1039 | } |
|
|
1040 | |
|
|
1041 | sub cmp_equal { |
|
|
1042 | my $self = shift; my $ans = shift; |
| 319 | my $showPartialCorrectAnswers = $self->getPG('$showPartialCorrectAnswers'); |
1043 | $ans->{showPartialCorrectAnswers} = $self->getPG('$showPartialCorrectAnswers'); |
| 320 | my $showTypeWarnings = $ans->{showTypeWarnings}; |
1044 | |
| 321 | my $showHints = getOption($ans->{showHints},$showPartialCorrectAnswers); |
1045 | # |
|
|
1046 | # get the paramaters |
|
|
1047 | # |
|
|
1048 | my $showHints = getOption($ans,'showHints'); |
| 322 | my $showLengthHints = getOption($ans->{showLengthHints},$showPartialCorrectAnswers); |
1049 | my $showLengthHints = getOption($ans,'showLengthHints'); |
| 323 | my $partialCredit = getOption($ans->{partialCredit},$showPartialCorrectAnswers); |
1050 | my $showParenHints = getOption($ans,'showParenHints'); |
| 324 | my $ordered = $ans->{ordered}; my $allowParens = $ans->{allowParens}; |
1051 | my $partialCredit = getOption($ans,'partialCredit'); |
| 325 | my $typeMatch = $ans->{typeMatch} || $self->{data}[0]; |
1052 | my $requireParenMatch = $ans->{requireParenMatch}; |
| 326 | $typeMatch = Value::Real->make($typeMatch) |
1053 | my $implicitList = $ans->{implicitList}; |
| 327 | if !ref($typeMatch) && Value::matchNumber($typeMatch); |
1054 | my $typeMatch = $ans->{typeMatch}; |
| 328 | my $value = getOption($ans->{entry_type}, |
1055 | my $value = $ans->{entry_type}; |
|
|
1056 | my $ltype = $ans->{list_type} || lc($self->type); |
|
|
1057 | my $stype = $ans->{short_type} || $ltype; |
|
|
1058 | |
| 329 | Value::isValue($typeMatch)? lc($typeMatch->showClass): 'value'); |
1059 | $value = (Value::isValue($typeMatch)? lc($typeMatch->cmp_class): 'a value') |
| 330 | $value =~ s/^an? //; $value =~ s/(real|complex) //; |
1060 | unless defined($value); |
| 331 | my $ltype = getOption($ans->{list_type},lc($self->type)); |
1061 | $value =~ s/(real|complex) //; $ans->{cmp_class} = $value; |
|
|
1062 | $value =~ s/^an? //; $value = 'formula' if $value =~ m/formula/; |
|
|
1063 | $ltype =~ s/^an? //; $stype =~ s/^an? //; |
| 332 | $showTypeWarnings = $showHints = $showLengthHints = 0 if $ans->{isPreview}; |
1064 | $showHints = $showLengthHints = 0 if $ans->{isPreview}; |
| 333 | |
1065 | |
| 334 | my $student = $ans->{student_value}; |
1066 | # |
|
|
1067 | # Get the lists of correct and student answers |
|
|
1068 | # (split formulas that return lists or unions) |
|
|
1069 | # |
|
|
1070 | my @correct = (); my ($cOpen,$cClose); |
|
|
1071 | if ($self->class ne 'Formula') { |
| 335 | my @correct = $self->value; |
1072 | @correct = $self->value; |
| 336 | my @student = |
1073 | $cOpen = $ans->{correct_value}{open}; $cClose = $ans->{correct_value}{close}; |
| 337 | $student->class eq 'List' && |
1074 | } else { |
| 338 | ($allowParens || (!$student->{open} && !$student->{close})) ? |
1075 | @correct = Value::List->splitFormula($self,$ans); |
| 339 | @{$student->{data}} : ($student); |
1076 | $cOpen = $self->{tree}{open}; $cClose = $self->{tree}{close}; |
|
|
1077 | } |
|
|
1078 | my $student = $ans->{student_value}; my @student = ($student); |
|
|
1079 | my ($sOpen,$sClose) = ('',''); |
|
|
1080 | if (Value::isFormula($student) && $student->type eq $self->type) { |
|
|
1081 | 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 | } elsif ($student->class ne 'Formula' && $student->class eq $self->type) { |
|
|
1088 | if ($implicitList && $student->{open} ne '') { |
|
|
1089 | @student = ($student); |
|
|
1090 | } else { |
|
|
1091 | @student = @{$student->{data}}; |
|
|
1092 | $sOpen = $student->{open}; $sClose = $student->{close}; |
|
|
1093 | } |
|
|
1094 | } |
|
|
1095 | return if $ans->{split_error}; |
|
|
1096 | # |
|
|
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 | else {$message .= "seem to be missing"} |
|
|
1106 | $self->cmp_Error($ans,$message) unless $ans->{isPreview}; |
|
|
1107 | } |
|
|
1108 | return; |
|
|
1109 | } |
| 340 | |
1110 | |
|
|
1111 | # |
|
|
1112 | # Determine the maximum score |
|
|
1113 | # |
| 341 | my $maxscore = scalar(@correct); |
1114 | my $M = scalar(@correct); |
| 342 | my $m = scalar(@student); |
1115 | my $m = scalar(@student); |
| 343 | $maxscore = $m if ($m > $maxscore); |
1116 | my $maxscore = ($m > $M)? $m : $M; |
|
|
1117 | |
|
|
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 | $value =~ s/( or|,) /s$1 /g; # fix "interval or union" |
|
|
1139 | push(@errors,"There should be more ${value}s in your $stype") |
|
|
1140 | if ($score < $maxscore && $score == $m); |
|
|
1141 | push(@errors,"There should be fewer ${value}s in your $stype") |
|
|
1142 | if ($score < $maxscore && $score == $M && !$showHints); |
|
|
1143 | } |
|
|
1144 | |
|
|
1145 | # |
|
|
1146 | # 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 | # 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 | my $error = join("\n",@errors); $error =~ s!</DIV>\n!</DIV>!g; |
|
|
1163 | $ans->{error_message} = $ans->{ans_message} = $error; |
|
|
1164 | } |
|
|
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 | my $extra = $ans->{extra} || |
|
|
1177 | (Value::isValue($typeMatch) ? $typeMatch: $ans->{firstElement}) || |
|
|
1178 | "Value::List"; |
|
|
1179 | my $showHints = getOption($ans,'showHints') && !$ans->{isPreview}; |
|
|
1180 | my $error = $$Value::context->{error}; |
| 344 | my $score = 0; my @errors; my $i = 0; |
1181 | my $score = 0; my @errors; my $i = 0; |
| 345 | |
1182 | |
|
|
1183 | # |
|
|
1184 | # Check for empty lists |
|
|
1185 | # |
|
|
1186 | if (scalar(@correct) == 0) {$ans->score($m == 0); return} |
|
|
1187 | |
|
|
1188 | # |
|
|
1189 | # Loop through student answers looking for correct ones |
|
|
1190 | # |
| 346 | ENTRY: foreach my $entry (@student) { |
1191 | ENTRY: foreach my $entry (@student) { |
| 347 | $i++; |
1192 | $i++; $$Value::context->clearError; |
| 348 | $entry = Value::Real->make($entry) if !ref($entry) && Value::matchNumber($entry); |
1193 | $entry = Value::makeValue($entry); |
| 349 | $entry = Value::Formula->new($entry) if !Value::isValue($entry); |
1194 | $entry = Value::Formula->new($entry) if !Value::isValue($entry); |
|
|
1195 | |
|
|
1196 | # |
|
|
1197 | # Some words differ if ther eis only one entry in the student's list |
|
|
1198 | # |
|
|
1199 | my $nth = ''; my $answer = 'answer'; |
|
|
1200 | my $class = $ans->{list_type} || $ans->{cmp_class}; |
|
|
1201 | 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 | # |
| 350 | if ($ordered) { |
1211 | if ($ordered) { |
| 351 | if (eval {shift(@correct) == $entry}) {$score++; next ENTRY} |
1212 | if (scalar(@correct)) { |
|
|
1213 | if (shift(@correct)->cmp_compare($entry,$ans,$nth,$value)) {$score++; next ENTRY} |
|
|
1214 | } else { |
|
|
1215 | # do syntax check |
|
|
1216 | if (ref($extra) eq 'CODE') {&$extra($entry,$ans,$nth,$value)} |
|
|
1217 | else {$extra->cmp_compare($entry,$ans,$nth,$value)} |
|
|
1218 | } |
|
|
1219 | if ($error->{flag} == $CMP_ERROR) {$self->cmp_error($ans); return} |
| 352 | } else { |
1220 | } else { |
| 353 | foreach my $k (0..$#correct) { |
1221 | foreach my $k (0..$#correct) { |
| 354 | if (eval {$correct[$k] == $entry}) { |
1222 | if ($correct[$k]->cmp_compare($entry,$ans,$nth,$value)) { |
| 355 | splice(@correct,$k,1); |
1223 | splice(@correct,$k,1); |
| 356 | $score++; next ENTRY; |
1224 | $score++; next ENTRY; |
| 357 | } |
1225 | } |
|
|
1226 | if ($error->{flag} == $CMP_ERROR) {$self->cmp_error($ans); return} |
| 358 | } |
1227 | } |
|
|
1228 | $$Value::context->clearError; |
|
|
1229 | # do syntax check |
|
|
1230 | if (ref($extra) eq 'CODE') {&$extra($entry,$ans,$nth,$value)} |
|
|
1231 | else {$extra->cmp_compare($entry,$ans,$nth,$value)} |
|
|
1232 | } |
| 359 | } |
1233 | # |
|
|
1234 | # Give messages about incorrect answers |
|
|
1235 | # |
|
|
1236 | my $match = (ref($typeMatch) eq 'CODE')? &$typeMatch($entry,$ans) : |
|
|
1237 | $typeMatch->typeMatch($entry,$ans); |
| 360 | if ($showTypeWarnings && defined($typeMatch) && |
1238 | if ($showTypeWarnings && !$match && |
| 361 | !$typeMatch->typeMatch($entry,$ans)) { |
1239 | !($ans->{ignoreStrings} && $entry->class eq 'String')) { |
| 362 | push(@errors, |
1240 | push(@errors,"Your$nth $answer isn't ".lc($class). |
| 363 | "Your ".NameForNumber($i)." value isn't ".lc($typeMatch->showClass). |
|
|
| 364 | " (it looks like ".lc($entry->showClass).")"); |
1241 | " (it looks like ".lc($entry->showClass).")"); |
| 365 | next ENTRY; |
1242 | } elsif ($error->{flag} && $ans->{showEqualErrors}) { |
| 366 | } |
1243 | my $message = $error->{message}; $message =~ s/\s+$//; |
| 367 | push(@errors,"Your ".NameForNumber($i)." $value is incorrect") |
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)} |
| 368 | if $showHints && $m > 1; |
1248 | } elsif ($showHints && $m > 1) { |
|
|
1249 | push(@errors,"Your$nth $value is incorrect"); |
| 369 | } |
1250 | } |
| 370 | |
|
|
| 371 | if ($showLengthHints) { |
|
|
| 372 | $value =~ s/ or /s or /; # fix "interval or union" |
|
|
| 373 | push(@errors,"There should be more ${value}s in your $ltype") |
|
|
| 374 | if ($score == $m && scalar(@correct) > 0); |
|
|
| 375 | push(@errors,"There should be fewer ${value}s in your $ltype") |
|
|
| 376 | if ($score < $maxscore && $score == scalar($self->value)); |
|
|
| 377 | } |
1251 | } |
| 378 | |
1252 | |
| 379 | $score = 0 if ($score != $maxscore && !$partialCredit); |
1253 | # |
| 380 | $ans->score($score/$maxscore); |
1254 | # Return the score and errors |
| 381 | push(@errors,"Score = $ans->{score}") if $ans->{debug}; |
1255 | # |
| 382 | $ans->{error_message} = $ans->{ans_message} = join("\n",@errors); |
1256 | return ($score,@errors); |
| 383 | } |
1257 | } |
| 384 | |
1258 | |
| 385 | # |
1259 | # |
|
|
1260 | # 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 | if ($formula->type eq 'Union') {@entries = $formula->{tree}->makeUnion} |
|
|
1267 | else {@entries = @{$formula->{tree}{coords}}} |
|
|
1268 | 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 | if (!defined($v)) {$ans->{split_error} = 1; $self->cmp_error; return} |
|
|
1277 | } |
|
|
1278 | return @formula; |
|
|
1279 | } |
|
|
1280 | |
|
|
1281 | # |
| 386 | # Return the value if it is defined, otherwise a default |
1282 | # Return the value if it is defined, otherwise use a default |
| 387 | # |
1283 | # |
| 388 | sub getOption { |
1284 | sub getOption { |
| 389 | my $value = shift; my $default = shift; |
1285 | my $ans = shift; my $name = shift; |
|
|
1286 | my $value = $ans->{$name}; |
| 390 | return $value if defined($value); |
1287 | return $value if defined($value); |
| 391 | return $default; |
1288 | return $ans->{showPartialCorrectAnswers}; |
| 392 | } |
|
|
| 393 | |
|
|
| 394 | # |
|
|
| 395 | # names for numbers |
|
|
| 396 | # |
|
|
| 397 | sub NameForNumber { |
|
|
| 398 | my $n = shift; |
|
|
| 399 | my $name = ('zeroth','first','second','third','fourth','fifth', |
|
|
| 400 | 'sixth','seventh','eighth','ninth','tenth')[$n]; |
|
|
| 401 | $name = "$n-th" if ($n > 10); |
|
|
| 402 | return $name; |
|
|
| 403 | } |
1289 | } |
| 404 | |
1290 | |
| 405 | ############################################################# |
1291 | ############################################################# |
| 406 | |
1292 | |
| 407 | package Value::Formula; |
1293 | package Value::Formula; |
| 408 | |
1294 | |
|
|
1295 | sub cmp_defaults { |
|
|
1296 | my $self = shift; |
|
|
1297 | |
|
|
1298 | return ( |
|
|
1299 | Value::Union::cmp_defaults($self,@_), |
|
|
1300 | typeMatch => Value::Formula->new("(1,2]"), |
|
|
1301 | showDomainErrors => 1, |
|
|
1302 | ) if $self->type eq 'Union'; |
|
|
1303 | |
|
|
1304 | my $type = $self->type; |
|
|
1305 | $type = ($self->isComplex)? 'Complex': 'Real' if $type eq 'Number'; |
|
|
1306 | $type = 'Value::'.$type.'::'; |
|
|
1307 | |
|
|
1308 | return ( |
|
|
1309 | &{$type.'cmp_defaults'}($self,@_), |
|
|
1310 | upToConstant => 0, |
|
|
1311 | showDomainErrors => 1, |
|
|
1312 | ) if defined(%$type) && $self->type ne 'List'; |
|
|
1313 | |
|
|
1314 | 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 | return ( |
|
|
1318 | Value::List::cmp_defaults($self,@_), |
|
|
1319 | removeParens => $self->{autoFormula}, |
|
|
1320 | typeMatch => $element, |
|
|
1321 | showDomainErrors => 1, |
|
|
1322 | ); |
|
|
1323 | } |
|
|
1324 | |
| 409 | # |
1325 | # |
| 410 | # No cmp function (for now) |
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 | $other = eval {($other->createRandomPoints(1))[1]->[0]} if Value::isFormula($other); |
|
|
1334 | return 1 unless defined($other); # can't really tell, so don't report type mismatch |
|
|
1335 | $typeMatch->typeMatch($other,$ans); |
|
|
1336 | } |
|
|
1337 | |
|
|
1338 | # |
|
|
1339 | # Handle removal of outermost parens in a list. |
|
|
1340 | # Evaluate answer, if the eval option is used. |
|
|
1341 | # Handle the UpToConstant option. |
| 411 | # |
1342 | # |
| 412 | sub cmp { |
1343 | sub cmp { |
| 413 | die "Answer checker for formulas is not yet defined"; |
1344 | my $self = shift; |
|
|
1345 | my $cmp = $self->SUPER::cmp(@_); |
|
|
1346 | if ($cmp->{rh_ans}{removeParens} && $self->type eq 'List') { |
|
|
1347 | $self->{tree}{open} = $self->{tree}{close} = ''; |
|
|
1348 | $cmp->ans_hash(correct_ans => $self->stringify) |
|
|
1349 | unless defined($self->{correct_ans}); |
|
|
1350 | } |
|
|
1351 | if ($cmp->{rh_ans}{eval} && $self->isConstant) { |
|
|
1352 | $cmp->ans_hash(correct_value => $self->eval); |
|
|
1353 | return $cmp; |
|
|
1354 | } |
|
|
1355 | 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 | $context->variables->add('C0' => 'Parameter'); |
|
|
1360 | my $f = Value::Formula->new('C0')+$self; |
|
|
1361 | for ('limits','test_points','test_values','num_points','granularity','resolution', |
|
|
1362 | 'checkUndefinedPoints','max_undefined') |
|
|
1363 | {$f->{$_} = $self->{$_} if defined($self->{$_})} |
|
|
1364 | $cmp->ans_hash(correct_value => $f); |
|
|
1365 | Parser::Context->current(undef,$current); |
|
|
1366 | } |
|
|
1367 | return $cmp; |
|
|
1368 | } |
|
|
1369 | |
|
|
1370 | sub cmp_equal { |
|
|
1371 | my $self = shift; my $ans = shift; |
|
|
1372 | # |
|
|
1373 | # Get the problem's seed |
|
|
1374 | # |
|
|
1375 | $self->{context}->flags->set( |
|
|
1376 | random_seed => $self->getPG('$PG_original_problemSeed') |
|
|
1377 | ); |
|
|
1378 | |
|
|
1379 | # |
|
|
1380 | # Use the list checker if the formula is a list or union |
|
|
1381 | # Otherwise use the normal checker |
|
|
1382 | # |
|
|
1383 | if ($self->type =~ m/^(List|Union|Set)$/) { |
|
|
1384 | Value::List::cmp_equal($self,$ans); |
|
|
1385 | } else { |
|
|
1386 | $self->SUPER::cmp_equal($ans); |
|
|
1387 | } |
|
|
1388 | } |
|
|
1389 | |
|
|
1390 | sub cmp_postprocess { |
|
|
1391 | my $self = shift; my $ans = shift; |
|
|
1392 | return unless $ans->{score} == 0 && !$ans->{isPreview}; |
|
|
1393 | 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 | my $other = $ans->{student_value}; |
|
|
1400 | return if $ans->{ignoreStrings} && (!Value::isValue($other) || $other->type eq 'String'); |
|
|
1401 | 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 | $self->cmp_Error($ans,"The dimension of your result is incorrect"); |
|
|
1405 | } |
|
|
1406 | |
|
|
1407 | # |
|
|
1408 | # 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 | my $points = [map {$_->[0]} @{$self->{test_points}}]; |
|
|
1435 | |
|
|
1436 | # |
|
|
1437 | # The graphs of the functions and errors |
|
|
1438 | # |
|
|
1439 | if ($formulas->{showGraphs}) { |
|
|
1440 | my @G = (); |
|
|
1441 | if ($formulas->{combineGraphs}) { |
|
|
1442 | push(@G,$self->cmp_graph($diagnostics,[$student,$self], |
|
|
1443 | title=>'Student Answer (red)<BR>Correct Answer (green)<BR>', |
|
|
1444 | points=>$points,showDomain=>1)); |
|
|
1445 | } else { |
|
|
1446 | push(@G,$self->cmp_graph($diagnostics,$self,title=>'Correct Answer')); |
|
|
1447 | push(@G,$self->cmp_graph($diagnostics,$student,title=>'Student Answer')); |
|
|
1448 | } |
|
|
1449 | my $cutoff = Value::Formula->new($self->getFlag('tolerance')); |
|
|
1450 | if ($formulas->{graphAbsoluteErrors}) { |
|
|
1451 | push(@G,$self->cmp_graph($diagnostics,[abs($self-$student),$cutoff], |
|
|
1452 | clip=>$formulas->{clipAbsoluteError}, |
|
|
1453 | title=>'Absolute Error',points=>$points)); |
|
|
1454 | } |
|
|
1455 | if ($formulas->{graphRelativeErrors}) { |
|
|
1456 | push(@G,$self->cmp_graph($diagnostics,[abs(($self-$student)/$self),$cutoff], |
|
|
1457 | clip=>$formulas->{clipRelativeError}, |
|
|
1458 | title=>'Relative Error',points=>$points)); |
|
|
1459 | } |
|
|
1460 | $output .= '<TABLE BORDER="0" CELLSPACING="0" CELLPADDING="0">' |
|
|
1461 | . '<TR VALIGN="TOP">'.join('<TD WIDTH="20"></TD>',@G).'</TR></TABLE>'; |
|
|
1462 | } |
|
|
1463 | |
|
|
1464 | # |
|
|
1465 | # The test points and values |
|
|
1466 | # |
|
|
1467 | my @rows = (); my $colsep = '</TD><TD WIDTH="20"></TD><TD ALIGN="RIGHT">'; |
|
|
1468 | my @P = (map {(scalar(@{$_}) == 1)? $_->[0]: Value::Point->make(@{$_})} @{$self->{test_points}}); |
|
|
1469 | my @i = sort {$P[$a] <=> $P[$b]} (0..$#P); |
|
|
1470 | if ($formulas->{showTestPoints}) { |
|
|
1471 | $student->createPointValues($self->{test_points},0,1,1) unless $student->{test_values}; |
|
|
1472 | my @p = ("Input:",(map {$P[$i[$_]]} (0..$#P))); |
|
|
1473 | push(@rows,'<TR><TD ALIGN="RIGHT">'.join($colsep,@p).'</TD></TR>'); |
|
|
1474 | push(@rows,'<TR><TD ALIGN="RIGHT">'.join($colsep,("<HR>")x scalar(@p)).'</TD></TR>'); |
|
|
1475 | push(@rows,'<TR><TD ALIGN="RIGHT">' |
|
|
1476 | .join($colsep,"Correct Answer:", map {$self->{test_values}[$i[$_]]} (0..$#P)) |
|
|
1477 | .'</TD></TR>'); |
|
|
1478 | my $test = $student->{test_values}; |
|
|
1479 | push(@rows,'<TR><TD ALIGN="RIGHT">' |
|
|
1480 | .join($colsep,"Student Answer:", map {Value::isNumber($test->[$i[$_]])? $test->[$i[$_]]: "undefined"} (0..$#P)) |
|
|
1481 | .'</TD></TR>'); |
|
|
1482 | } |
|
|
1483 | # |
|
|
1484 | # The absolute errors (colored by whether they are ok or too big) |
|
|
1485 | # |
|
|
1486 | if ($formulas->{showAbsoluteErrors}) { |
|
|
1487 | my @p = ("Absolute Error:"); |
|
|
1488 | my $tolerance = $self->getFlag('tolerance'); |
|
|
1489 | my $tolType = $self->getFlag('tolType'); my $error; |
|
|
1490 | foreach my $j (0..$#P) { |
|
|
1491 | if (Value::isNumber($student->{test_values}[$i[$j]])) { |
|
|
1492 | $error = abs($self->{test_values}[$i[$j]]-$student->{test_values}[$i[$j]]); |
|
|
1493 | $error = '<SPAN STYLE="color:#'.($error<$tolerance ? '00AA00': 'AA0000').'">'.$error.'</SPAN>' |
|
|
1494 | if $tolType eq 'absolute'; |
|
|
1495 | } else {$error = "---"} |
|
|
1496 | push(@p,$error); |
|
|
1497 | } |
|
|
1498 | push(@rows,'<TR><TD ALIGN="RIGHT">'.join($colsep,@p).'</TD></TR>'); |
|
|
1499 | } |
|
|
1500 | # |
|
|
1501 | # The relative errors (colored by whether they are OK ro too big) |
|
|
1502 | # |
|
|
1503 | if ($formulas->{showRelativeErrors}) { |
|
|
1504 | my @p = ("Relative Error:"); |
|
|
1505 | my $tolerance = $self->getFlag('tolerance'); |
|
|
1506 | my $tolType = $self->getFlag('tolType'); my $error; |
|
|
1507 | foreach my $j (0..$#P) { |
|
|
1508 | if (Value::isNumber($student->{test_values}[$i[$j]])) { |
|
|
1509 | $error = abs(($self->{test_values}[$i[$j]]-$student->{test_values}[$i[$j]])/ |
|
|
1510 | ($self->{test_values}[$i[$j]]||1E-10)); |
|
|
1511 | $error = '<SPAN STYLE="color:#'.($error<$tolerance ? '00AA00': 'AA0000').'">'.$error.'</SPAN>' |
|
|
1512 | if $tolType eq 'relative'; |
|
|
1513 | } else {$error = "---"} |
|
|
1514 | push(@p,$error); |
|
|
1515 | } |
|
|
1516 | push(@rows,'<TR><TD ALIGN="RIGHT">'.join($colsep,@p).'</TD></TR>'); |
|
|
1517 | } |
|
|
1518 | # |
|
|
1519 | # Put the data into a table |
|
|
1520 | # |
|
|
1521 | if (scalar(@rows)) { |
|
|
1522 | $output .= '<p><HR><p><TABLE BORDER="0" CELLSPACING="0" CELLPADDING="0">' |
|
|
1523 | . join('<TR><TD HEIGHT="3"></TD>',@rows) |
|
|
1524 | . '</TABLE>'; |
|
|
1525 | } |
|
|
1526 | } |
|
|
1527 | # |
|
|
1528 | # Put all the diagnostic output into a frame |
|
|
1529 | # |
|
|
1530 | return unless $output; |
|
|
1531 | $output |
|
|
1532 | = '<TABLE BORDER="1" CELLSPACING="2" CELLPADDING="20" BGCOLOR="#F0F0F0">' |
|
|
1533 | . '<TR><TD ALIGN="LEFT"><B>Diagnostics for '.$self->string .':</B>' |
|
|
1534 | . '<P><CENTER>' . $output . '</CENTER></TD></TR></TABLE><P>'; |
|
|
1535 | warn $output; |
|
|
1536 | } |
|
|
1537 | |
|
|
1538 | # |
|
|
1539 | # Draw a graph from a given Formula object |
|
|
1540 | # |
|
|
1541 | sub cmp_graph { |
|
|
1542 | my $self = shift; my $diagnostics = shift; |
|
|
1543 | my $F1 = shift; my $F2; ($F1,$F2) = @{$F1} if (ref($F1) eq 'ARRAY'); |
|
|
1544 | # |
|
|
1545 | # Get the various options |
|
|
1546 | # |
|
|
1547 | my %options = (title=>'',points=>[],@_); |
|
|
1548 | my $graphs = $diagnostics->{graphs}; |
|
|
1549 | my $limits = $graphs->{limits}; $limits = $self->getFlag('limits',[-2,2]) unless $limits; |
|
|
1550 | $limits = $limits->[0] if ref($limits) eq 'ARRAY' && ref($limits->[0]) eq 'ARRAY'; |
|
|
1551 | my $size = $graphs->{size}; $size = [$size,$size] unless ref($size) eq 'ARRAY'; |
|
|
1552 | my $steps = $graphs->{divisions}; |
|
|
1553 | my $points = $options{points}; my $clip = $options{clip}; |
|
|
1554 | my ($my,$My) = (0,0); my ($mx,$Mx) = @{$limits}; |
|
|
1555 | my $dx = ($Mx-$mx)/$steps; my $f; my $y; |
|
|
1556 | |
|
|
1557 | # |
|
|
1558 | # Find the max and min values of the function |
|
|
1559 | # |
|
|
1560 | foreach $f ($F1,$F2) { |
|
|
1561 | next unless defined($f); |
|
|
1562 | unless (scalar(keys(%{$f->{variables}})) < 2) { |
|
|
1563 | warn "Only formulas with one variable can be graphed"; |
|
|
1564 | return ""; |
|
|
1565 | } |
|
|
1566 | if ($f->isConstant) { |
|
|
1567 | $y = $f->eval; |
|
|
1568 | $my = $y if $y < $my; $My = $y if $y > $My; |
|
|
1569 | } else { |
|
|
1570 | my $F = $f->perlFunction; |
|
|
1571 | foreach my $i (0..$steps-1) { |
|
|
1572 | $y = eval {&{$F}($mx+$i*$dx)}; next unless defined($y) && Value::isNumber($y); |
|
|
1573 | $my = $y if $y < $my; $My = $y if $y > $My; |
|
|
1574 | } |
|
|
1575 | } |
|
|
1576 | } |
|
|
1577 | $My = 1 if abs($My - $my) < 1E-5; |
|
|
1578 | $my *= 1.1; $My *= 1.1; |
|
|
1579 | if ($clip) { |
|
|
1580 | $my = -$clip if $my < -$clip; |
|
|
1581 | $My = $clip if $My > $clip; |
|
|
1582 | } |
|
|
1583 | $my = -$My/10 if $my > -$My/10; $My = -$my/10 if $My < -$my/10; |
|
|
1584 | my $a = Value::Real->new(($My-$my)/($Mx-$mx)); |
|
|
1585 | |
|
|
1586 | # |
|
|
1587 | # Create the graph itself, with suitable title |
|
|
1588 | # |
|
|
1589 | my $grf = $self->getPG('$_grf_ = {n => 0}'); |
|
|
1590 | $grf->{Goptions} = [ |
|
|
1591 | $mx,$my,$Mx,$My, |
|
|
1592 | axes => $graphs->{axes}, |
|
|
1593 | grid => $graphs->{grid}, |
|
|
1594 | size => $size, |
|
|
1595 | ]; |
|
|
1596 | $grf->{G} = $self->getPG('init_graph(@{$_grf_->{Goptions}})'); |
|
|
1597 | $grf->{G}->imageName($grf->{G}->imageName.'-'.time()); # avoid browser cache |
|
|
1598 | $self->cmp_graph_function($grf,$F2,"green",$steps,$points) if defined($F2); |
|
|
1599 | $self->cmp_graph_function($grf,$F1,"red",$steps,$points); |
|
|
1600 | my $image = $self->getPG('alias(insertGraph($_grf_->{G}))'); |
|
|
1601 | $image = '<IMG SRC="'.$image.'" WIDTH="'.$size->[0].'" HEIGHT="'.$size->[1].'" BORDER="0" STYLE="margin-bottom:5px">'; |
|
|
1602 | my $title = $options{title}; $title .= '<DIV STYLE="margin-top:5px"></DIV>' if $title; |
|
|
1603 | $title .= "<SMALL>Domain: [$mx,$Mx]</SMALL><BR>" if $options{showDomain}; |
|
|
1604 | $title .= "<SMALL>Range: [$my,$My]<BR>Aspect ratio: $a:1</SMALL>"; |
|
|
1605 | return '<TD ALIGN="CENTER" VALIGN="TOP" NOWRAP>'.$image.'<BR>'.$title.'</TD>'; |
|
|
1606 | } |
|
|
1607 | |
|
|
1608 | # |
|
|
1609 | # Add a function to a graph object, and plot the points |
|
|
1610 | # that are used to test the function |
|
|
1611 | # |
|
|
1612 | sub cmp_graph_function { |
|
|
1613 | my $self = shift; my $grf = shift; my $F = shift; |
|
|
1614 | my $color = shift; my $steps = shift; my $points = shift; |
|
|
1615 | $grf->{n}++; my $Fn = "F".$grf->{n}; $grf->{$Fn} = $F; my $f; |
|
|
1616 | if ($F->isConstant) { |
|
|
1617 | my $y = $F->eval; |
|
|
1618 | $f = $self->getPG('new Fun(sub {'.$y.'},$_grf_->{G})'); |
|
|
1619 | } else { |
|
|
1620 | my $X = (keys %{$F->{variables}})[0]; |
|
|
1621 | $f = $self->getPG('new Fun(sub {Parser::Evaluate($_grf_->{'.$Fn.'},'.$X.'=>shift)},$_grf_->{G})'); |
|
|
1622 | foreach my $x (@{$points}) { |
|
|
1623 | my $y = Parser::Evaluate($F,($X)=>$x); next unless defined($y) && Value::isNumber($y); |
|
|
1624 | $grf->{x} = $x; $grf->{y} = $y; |
|
|
1625 | my $C = $self->getPG('new Circle($_grf_->{x},$_grf_->{y},4,"'.$color.'","'.$color.'")'); |
|
|
1626 | $grf->{G}->stamps($C); |
|
|
1627 | } |
|
|
1628 | } |
|
|
1629 | $f->color($color); $f->weight(2); $f->steps($steps); |
|
|
1630 | } |
|
|
1631 | |
|
|
1632 | # |
|
|
1633 | # If an answer array was used, get the data from the |
|
|
1634 | # Matrix, Vector or Point, and format the array of |
|
|
1635 | # data using the original parameter |
|
|
1636 | # |
|
|
1637 | sub correct_ans { |
|
|
1638 | my $self = shift; |
|
|
1639 | return $self->SUPER::correct_ans unless $self->{ans_name}; |
|
|
1640 | my @array = (); |
|
|
1641 | if ($self->{tree}->type eq 'Matrix') { |
|
|
1642 | foreach my $row (@{$self->{tree}{coords}}) { |
|
|
1643 | my @row = (); |
|
|
1644 | foreach my $x (@{$row->coords}) {push(@row,$x->string)} |
|
|
1645 | push(@array,[@row]); |
|
|
1646 | } |
|
|
1647 | } else { |
|
|
1648 | foreach my $x (@{$self->{tree}{coords}}) {push(@array,$x->string)} |
|
|
1649 | if ($self->{tree}{ColumnVector}) {foreach my $x (@array) {$x = [$x]}} |
|
|
1650 | else {@array = [@array]} |
|
|
1651 | } |
|
|
1652 | Value::VERBATIM($self->format_matrix([@array],@{$self->{format_options}},tth_delims=>1)); |
|
|
1653 | } |
|
|
1654 | |
|
|
1655 | # |
|
|
1656 | # Get the size of the array and create the appropriate answer array |
|
|
1657 | # |
|
|
1658 | sub ANS_MATRIX { |
|
|
1659 | my $self = shift; |
|
|
1660 | my $extend = shift; my $name = shift; |
|
|
1661 | my $size = shift || 5; my $type = $self->type; |
|
|
1662 | my $cols = $self->length; my $rows = 1; my $sep = ','; |
|
|
1663 | if ($type eq 'Matrix') { |
|
|
1664 | $sep = ''; $rows = $cols; $cols = $self->{tree}->typeRef->{entryType}{length}; |
|
|
1665 | } |
|
|
1666 | if ($self->{tree}{ColumnVector}) { |
|
|
1667 | $sep = ""; $type = "Matrix"; |
|
|
1668 | my $tmp = $rows; $rows = $cols; $cols = $tmp; |
|
|
1669 | $self->{ColumnVector} = 1; |
|
|
1670 | } |
|
|
1671 | my $def = ($self->{context} || $$Value::context)->lists->get($type); |
|
|
1672 | my $open = $self->{open} || $self->{tree}{open} || $def->{open}; |
|
|
1673 | my $close = $self->{close} || $self->{tree}{close} || $def->{close}; |
|
|
1674 | $self->ans_matrix($extend,$name,$rows,$cols,$size,$open,$close,$sep); |
|
|
1675 | } |
|
|
1676 | |
|
|
1677 | sub ans_array { |
|
|
1678 | my $self = shift; |
|
|
1679 | return $self->SUPER::ans_array(@_) unless $self->array_OK; |
|
|
1680 | $self->ANS_MATRIX(0,'',@_); |
|
|
1681 | } |
|
|
1682 | sub named_ans_array { |
|
|
1683 | my $self = shift; |
|
|
1684 | return $self->SUPER::named_ans_array(@_) unless $self->array_OK; |
|
|
1685 | $self->ANS_MATRIX(0,@_); |
|
|
1686 | } |
|
|
1687 | sub named_ans_array_extension { |
|
|
1688 | my $self = shift; |
|
|
1689 | return $self->SUPER::named_ans_array_extension(@_) unless $self->array_OK; |
|
|
1690 | $self->ANS_MATRIX(1,@_); |
|
|
1691 | } |
|
|
1692 | |
|
|
1693 | sub array_OK { |
|
|
1694 | my $self = shift; my $tree = $self->{tree}; |
|
|
1695 | return $tree->type =~ m/^(Point|Vector|Matrix)$/ && $tree->class eq 'List'; |
|
|
1696 | } |
|
|
1697 | |
|
|
1698 | # |
|
|
1699 | # Get an array of values from a Matrix, Vector or Point |
|
|
1700 | # |
|
|
1701 | sub value { |
|
|
1702 | my $self = shift; |
|
|
1703 | my @array = (); |
|
|
1704 | if ($self->{tree}->type eq 'Matrix') { |
|
|
1705 | foreach my $row (@{$self->{tree}->coords}) { |
|
|
1706 | my @row = (); |
|
|
1707 | foreach my $x (@{$row->coords}) {push(@row,Value::Formula->new($x))} |
|
|
1708 | push(@array,[@row]); |
|
|
1709 | } |
|
|
1710 | } else { |
|
|
1711 | foreach my $x (@{$self->{tree}->coords}) { |
|
|
1712 | push(@array,Value::Formula->new($x)); |
|
|
1713 | } |
|
|
1714 | } |
|
|
1715 | return @array; |
| 414 | } |
1716 | } |
| 415 | |
1717 | |
| 416 | ############################################################# |
1718 | ############################################################# |
| 417 | |
1719 | |
| 418 | 1; |
1720 | 1; |