Imre:
There are two approaches that I can think of that would work for your situation. The first is to use the MultiAnswer object, which lets you provide a single answer checker for multiple answer blanks (the checker has access to the student answers for all the blanks, and returns a score for each). This is implemented in the pg/macros/parserMultiAnswer.pl file, and there is documentation in the comments at the top of that file. There are a number of options that control how the parts are to be displayed in the results table when a student enters answers, and there is control over error messages and partial credit.
Here is an example of using this approach. It turns out that the error message mechanism in MultiAnswer objects is not quite general enough to handle both partial credit and a message at the same time (setting an error message automatically gives 0 credit), so that should really be improved. In the meantime,
the same effect can be had by using a post-filter to either do the partial credit or add the error messages. I chose to have it add the messages, since it was a little easier (IMHO).
DOCUMENT();
loadMacros(
"PGstandard.pl",
"MathObjects.pl",
"parserMultiAnswer.pl",
);
TEXT(beginproblem);
#############################################
Context("LimitedNumeric");
Parser::Number::NoDecimals;
($a,$b) = num_sort(random(2,13),random(2,13));
$n = $a * $b;
$mp = MultiAnswer(Real($a),Real($b),Real($n))->with(
singleResult => 1,
separator => ", ", tex_separator => ',\,',
allowBlankAnswers => 1,
checker => sub {
my ($correct,$student,$self,$ans) = @_;
$ans->{distinctFactors} = 1; $ans->{nonFactors} = [];
my @N = (map {$_->value} @$student);
foreach my $i (0..$self->length-1) {
next if $N[$i] eq '';
foreach my $j (0..$i-1) {
if ($N[$j] ne '' && $N[$i] == $N[$j]) {
$ans->{distinctFactors} = 0;
$N[$i] = '';
break;
}
}
}
my @score = (0) x $self->length;
foreach my $i (0..$self->length-1) {
next if $N[$i] eq '';
if ($n % $N[$i] == 0) {$score[$i] = 1}
else {push(@{$ans->{nonFactors}},$N[$i])}
}
return @score;
}
);
my $cmp = ($mp->cmp)[0]->withPostFilter(sub {
my $ans = shift;
if (!$ans->{isPreview}) {
my @errors;
push(@errors,"Your factors should all be distinct") unless $ans->{distinctFactors};
foreach my $m (@{$ans->{nonFactors}}) {push(@errors,"$m is not a factor of $n")}
$ans->{ans_message} = join("<BR>",@errors);
}
return $ans;
});
#############################################
BEGIN_TEXT
Give three distinct factors for \($n\):
\{$mp->ans_rule(5)\}, \{$mp->ans_rule(5)\}, and \{$mp->ans_rule(5)\}.
END_TEXT
#############################################
ANS($cmp);
#############################################
ENDDOCUMENT();
Here we use the LimitedNumeric context so that only numbers (not operations or function calls) can be entered by the student, and we turn on the NoDecimals check so that only integers are allowed to be entered. This avoids the problem you were having of dealing with real numbers; we are guaranteed that the student answers are integers.
The MultiAnswer object is given the three correct answers (these are needed for the correct answer display, as you noticed in your version of the problem). It is set to appear as a single result (i.e., only one row in the results table for all three entries). The three factors are displayed with a comma separating them, and this also true for the factors in the correct answer (the usual separator is a semicolon, and we override that here). It also allows the answer checker to run when some answers are blank, so you can give partial credit in that case.
Finally, the checker is provided. It gets the actual underlying perl reals from the student's Real() objects, and looks through them to see whether any are duplicated, and checks whether they are actually factors of the number in question. Fields of the AnwserHash are set to indicate when error messages are needed. The score is an array of scores, one for each answer the student provides; the percentages are handled automatically by the MultiAnswer object.
Once the MultiAnswer object is initialized, its answer checker is obtained (MultiAnswer->cmp returns an ARRAY of checkers, since when singleResult is not set, there will be multiple answer checkers), but in this case, there is only one, so we add a post-filter to it to add the error messages when there were any. Note that this is only done when not in Preview mode, since you don't want to give away actual information in that case.
The answer rules in the problem are created by the MultiPart object itself (note the $mp usage in the BEGIN_TEXT/END_TEXT block. Finally, the answer checker is passed to ANS().
The need for the post-filter is a bit unsatisfying, so one you might consider a second alternative: a List() object with a custom list checker. In this case, the student enters the factors in a single blank but separated by commas.
DOCUMENT();
loadMacros(
"PGstandard.pl",
"MathObjects.pl",
"parserMultiAnswer.pl",
);
TEXT(beginproblem);
#############################################
Context("LimitedNumeric");
Parser::Number::NoDecimals;
Context()->operators->redefine(',');
($a,$b) = num_sort(random(2,13),random(2,13));
$n = $a * $b;
$factors = List($a,$b,$n);
#############################################
BEGIN_TEXT
Give three distinct factors for \($n\) separated by commas: \{ans_rule\}
END_TEXT
#############################################
$showPartialCorrectAnswers = 1;
ANS($factors->cmp(
showHints => 0,
entry_type => "factor",
list_checker => sub {
my ($correct,$student,$ans) = @_;
my @errors = ();
my @N = (map {$_->value} @$student);
foreach my $i (0..$#N) {
next if $N[$i] eq '';
foreach my $j (0..$i-1) {
if ($N[$j] ne '' && $N[$i] == $N[$j]) {
push(@errors,"Your factors should all be distinct")
unless $ans->{isPreview} || @errors;
$N[$i] = '';
break;
}
}
}
my $score = 0;
foreach my $i (0..$#N) {
if ($N[$i] ne '') {
if ($n % $N[$i] == 0) {$score++}
elsif (!$ans->{isPreview})
{push(@errors,"$N[$i] is not a factor of $n")}
}
}
return ($score,@errors);
}
));
#############################################
ENDDOCUMENT();
In this case, we need to add the comma operator back into the LimitedNumeric context so that the student can enter a factor list. The factors are stored in a List() object, and the List's cmp() routine is passed a custom list_checker. This checker does essentially the same task as the previous one, but instead of setting flags in the AnswerHash, it stores messages in @errors and returns them along with the score. The List checker then issues the error messages and sets the score based on the length of the lists. This avoids the need of having a separate post-filter for handling the messages.
This approach uses only a single answer blank, and there are advantages and disadvantages to that. If you want to reinforce that the student has to enter three factors, then showing the three blanks may be valuable. If you want the student to have to think about how many entries he or she may need to type (for example, if you asked for all factors, and don't want to give away how many there are), then you might like the List approach better. Note that when the student has entered correct answer, but is missing some, he or she will get a message indicating that more answers are needed (this is taken care of by the List checker automatically); similarly, if he or she enters too many answers, an error may be issued about that as well.
Note, however, that by indicating which entries are nor correct factors, it is possible for the student simply to type in lots of numbers until he or she hits on the factors. In practice, I doubt that will be a serious problem, however, for numbers of the size you are using.
Hope that helps.
Davide