diff --git a/macros/parsers/parserMultiAnswer.pl b/macros/parsers/parserMultiAnswer.pl index 2bc718521..c8691e90a 100644 --- a/macros/parsers/parserMultiAnswer.pl +++ b/macros/parsers/parserMultiAnswer.pl @@ -14,7 +14,6 @@ package parser::MultiAnswer; our $separator = ';'; # separator for singleResult previews my @ans_defaults = ( - checker => sub {0}, showCoordinateHints => 0, showEndpointHints => 0, showEndTypeHints => 0, @@ -90,17 +89,13 @@ sub cmp { die "Your checker must be a subroutine." if defined($self->{checker}); $self->{checker} = sub { my ($correct, $student, $self, $ans) = @_; - my @scores; - - for (0 .. $self->length - 1) { - push(@scores, $correct->[$_] == $student->[$_] ? 1 : 0); - } - return \@scores if $self->{partialCredit}; - for (@scores) { - return 0 unless $_; + return $self->{scores} if $self->{partialCredit}; + for (@{ $self->{scores} }) { + return 0 unless $_ == 1; } return 1; - } + }; + $self->{checkTypes} = 'compatible' if $self->{checkTypes} && $self->{checkTypes} ne 'exact'; } if ($self->{allowBlankAnswers}) { @@ -281,7 +276,6 @@ sub entry_check { my $ANS = $self->{cmp}[$i]->evaluate($ans->{student_ans}); $self->{ans}[$i] = $ANS; $ANS->{type} = $ans->{type}; - $ANS->score(0); foreach my $id (keys %{$ans}) { $ANS->{$id} = $ans->{$id} unless defined($ANS->{$id}); @@ -306,21 +300,48 @@ sub perform_check { my $rh_ans = shift; my $context = $self->context; $context->clearError; + + # + # Get correct and student answers, and scores. + # Initially mark all answer blanks as wrong. + # my @correct; my @student; - foreach my $ans (@{ $self->{ans} }) { - push(@correct, $ans->{correct_value}); - push(@student, $ans->{student_value}); - return if $ans->{ans_message} || !defined($ans->{student_value}); + $self->{scores} = []; + for my $ans (@{ $self->{ans} }) { + push(@correct, $ans->{correct_value}); + push(@student, $ans->{student_value}); + push(@{ $self->{scores} }, $ans->{score}); + $ans->score(0); + } + + # + # Check for error messages and correct answer types + # + my $checkTypes = $self->{checkTypes}; + $checkTypes = 'equal' if $checkTypes && $checkTypes ne 'compatible'; + for my $ans (@{ $self->{ans} }) { + return if ($checkTypes ne 'compatible' && $ans->{ans_message}) || !defined($ans->{student_value}); return - if $self->{checkTypes} + if $checkTypes eq 'equal' && $ans->{student_value}->type ne $ans->{correct_value}->type && !($self->{allowBlankAnswers} && $ans->{student_ans} !~ m/\S/); + return + if $checkTypes eq 'compatible' + && !$ans->{correct_value}->typeMatch($ans->{student_value}, $ans) + && !($self->{allowBlankAnswers} && $ans->{student_ans} !~ m/\S/); } + + # + # Set the isPreview flag. + # my $inputs = $main::inputs_ref; $rh_ans->{isPreview} = $inputs->{previewAnswers} - || ($inputs_{action} && $inputs->{action} =~ m/^Preview/); + || ($inputs->{action} && $inputs->{action} =~ m/^Preview/); + # + # Call the user's answer checker. + # Parser::Context->current(undef, $context); # change to multi-answer's context my $flags = Value::contextSet($context, $self->cmp_contextFlags($rh_ans)); # save old context flags $context->{answerHash} = $rh_ans; # attach the answerHash @@ -329,16 +350,20 @@ sub perform_check { $context->{answerHash} = undef; # remove answerHash if (!@result && $context->{error}{flag}) { $self->cmp_error($self->{ans}[0]); return 1 } + # + # Mark the answers as correct or incorrect. + # my $result = (scalar(@result) > 1 ? [@result] : $result[0] || 0); if (ref($result) eq 'ARRAY') { die "Checker subroutine returned the wrong number of results" if (scalar(@{$result}) != $self->length); - foreach my $i (0 .. $self->length - 1) { $self->{ans}[$i]->score($result->[$i]) } + for my $i (0 .. $self->length - 1) { $self->{ans}[$i]->score($result->[$i]) } } elsif (Value::matchNumber($result)) { - foreach my $ans (@{ $self->{ans} }) { $ans->score($result) } + for my $ans (@{ $self->{ans} }) { $ans->score($result) } } else { die "Checker subroutine should return a number or array of numbers ($result)"; } + return; } @@ -524,9 +549,14 @@ =head2 cmpOpts =head2 checkTypes -Specifies whether the types of the student and professor's answers must match exactly -(C<< checkTypes => 1 >>) or just pass the usual type-match error checking (in which case, you should -check the types before you use the data). Default: 1. +Specifies whether the types of the student and instructor's answers must match and to what extent. +if C<< checkTypes => 'compatible' >> then the student answers only need to be compatible with the +instructor answers in the sense that they parse into objects that can be compared to the instructor +answers. If C<< checkTypes => 1 >> then the types of the student answers must match the types of the +instructor answers exactly. Otherwise no type checking is done other than the the usual type-match +error checking (in which case, you should check the types before you use the data). Default: 1. +Note that if the default checker is used, i.e., if C is not set, then C<< checkTypes => 1 >> +is the same as C<< checkTypes => 'compatible' >>. =head2 allowBlankAnswers diff --git a/macros/parsers/parserRadioMultiAnswer.pl b/macros/parsers/parserRadioMultiAnswer.pl index 57163dc1b..07a822f17 100644 --- a/macros/parsers/parserRadioMultiAnswer.pl +++ b/macros/parsers/parserRadioMultiAnswer.pl @@ -151,8 +151,14 @@ =head1 OPTIONS =item checkTypes (Default: checkTypes => 1) -Whether the types of the student and correct answers must match exactly or just pass the usual -type-match error checking (in which case, you should check the types before you use the data). +Specifies whether the types of the student and instructor's answers must match and to what extent. +if C<< checkTypes => 'compatible' >> then the student answers only need to be compatible with the +instructor answers in the sense that they parse into objects that can be compared to the instructor +answers. If C<< checkTypes => 1 >> then the types of the student answers must match the types of the +instructor answers exactly. Otherwise no type checking is done other than the the usual type-match +error checking (in which case, you should check the types before you use the data). Default: 1. +Note that if the default checker is used, i.e., if C is not set, then C<< checkTypes => 1 >> +is the same as C<< checkTypes => 'compatible' >>. =item allowBlankAnswers (Default: allowBlankAnswers => 0) @@ -260,7 +266,6 @@ package parser::RadioMultiAnswer; our $answerPrefix = 'RaDiOMuLtIaNsWeR_'; # answer rule prefix my @ans_defaults = ( - checker => sub {0}, showCoordinateHints => 0, showEndpointHints => 0, showEndTypeHints => 0 @@ -350,16 +355,18 @@ sub cmp { } unless (ref($self->{checker}) eq 'CODE') { + die "Your checker must be a subroutine." if defined $self->{checker}; $self->{checker} = sub { my ($correct, $student, $self, $ans) = @_; return 0 if ($correct->[0] != $student->[0]); for (0 .. $#{ $correct->[ $correct->[0] ] }) { - return 0 if $correct->[ $correct->[0] ][$_] != $student->[ $correct->[0] ][$_]; + return 0 unless $self->{ans}[ $correct->[0] - 1 ][$_]{score}; } return 1; }; + $self->{checkTypes} = 'compatible' if $self->{checkTypes} && $self->{checkTypes} ne 'exact'; } if ($self->{allowBlankAnswers}) { @@ -531,13 +538,18 @@ sub perform_check { my ($self, $rh_ans) = @_; my $context = $self->context; $context->clearError; - my @correct; - my @student; + + my $checkTypes = $self->{checkTypes}; + $checkTypes = 'equal' if $checkTypes && $checkTypes ne 'compatible'; + # The answers for all parts are sent to the grader. The answers in the incorrect parts from # the correct answer may be used in the grader. The answers for each part are in a separate # list. The radio answer is sent as the numerical index of the choice. The count starts at # one, so that it is the location in the list sent to the grader of the answers for the # selected part (the radio answer is in position 0). + my @correct; + my @student; + push(@correct, $self->{correct} + 1); push(@student, $self->getIndexByValue($main::inputs_ref->{ $self->ANS_NAME(0) }) + 1); my $part_index = 1; @@ -551,9 +563,13 @@ sub perform_check { next if $student[0] != $part_index; return if $ans->{ans_message} ne '' || !defined $ans->{student_value}; return - if ($self->{checkTypes} - && $ans->{student_value}->type ne $ans->{correct_value}->type - && !($self->{allowBlankAnswers} && $ans->{student_ans} !~ m/\S/)); + if $checkTypes eq 'equal' + && $ans->{student_value}->type ne $ans->{correct_value}->type + && !($self->{allowBlankAnswers} && $ans->{student_ans} !~ m/\S/); + return + if $checkTypes eq 'compatible' + && !$ans->{correct_value}->typeMatch($ans->{student_value}, $ans) + && !($self->{allowBlankAnswers} && $ans->{student_ans} !~ m/\S/); } push(@correct, [@part_correct]); push(@student, [@part_student]);