WeBWorK Problems

Force reduced fractions in lists

Force reduced fractions in lists

by Jason Aubrey -
Number of replies: 10
Hi All,

We have questions whose answer is a list of fractions. (E.g. below.) The Fraction-NoDecimals context works to force students to enter fractions instead of decimals, but we can't seem to force reduced fractions in lists.

Also, the author of the problem defined a "no solution" string. But, if a student enters "no solution" to a question which in fact has a solution the reply is "numerators must be integers" rather than "incorrect."

Any advice about how to implement the desired behavior?

Thanks,
Jason

# DESCRIPTION
# Solve an abs val eqn of the form k - |ax + b| = k, one solution. 
# ENDDESCRIPTION
## DBsubject('Algebra')
## DBchapter('Equations and Inequalities')
## DBsection('Absolute Value')
## KEYWORDS('linear', 'absolute value', 'equation')
## Author('Rick Lynch')
## Institution('University of Missouri-Columbia') 

###########################################################################
# initialization
###########################################################################

DOCUMENT();
loadMacros(
  "PGstandard.pl",
  "MathObjects.pl",
  "PeriodicRerandomization.pl",
  "contextFraction.pl"
);

TEXT(beginproblem());
$showPartialCorrectAnswers = 1;

PeriodicRerandomization("3"); 

###########################################################################
# setup contexts and variables
###########################################################################

Context("Fraction-NoDecimals");
Context()->strings->add(
  "no solution"=>{},
  "none"=>{alias=>"no solution"}
);

$var = list_random('x','y','z','w','p','q','r','s','t','u','v');

$a = random(2,20)*random(-1,1,2);
$b = -sgn($a)*random(1,20);
$k = non_zero_random(-15,15);

$eqn = "$k - \left| $a $var + $b\right| = $k";
$answer = List(Fraction(-$b,$a));

###########################################################################
# state the problem
###########################################################################

Context()->texStrings;
BEGIN_TEXT
Solve the following equation for \($var\):
$PAR
\[ $eqn \]
$PAR
\( $var = \) \{ ans_rule(20) \}
END_TEXT

###########################################################################
# check the answer
###########################################################################

ANS($answer->cmp(studentMustReduceFractions=>1)); 

###########################################################################
# use PeriodicRerandomization to write the answer and generate a new
# version of the problem
###########################################################################

if ($attempts_modp == 0 && $actualAttempts != 0) {
BEGIN_TEXT
$PAR
$BBOLD Answer: $EBOLD \($var = $answer\)
$PAR
END_TEXT
} else {
BEGIN_TEXT
$PAR
$BBOLD Help: $EBOLD Enter your answers as a comma separated list
if there is more than one correct answer. Write "no solution"
if the equation has no solution. $BR
END_TEXT
}
Context()->normalStrings;

PeriodicStatus();

COMMENT('Features Periodic Rerandomization. Edited and updated in 2012/2013. $BR
Desc: Solve an abs val eqn of the form k - |ax + b| = k, one solution.');
ENDDOCUMENT();
In reply to Jason Aubrey

Re: Force reduced fractions in lists

by Paul Pearson -
Hi Jason,

The bogus error message can be replaced by "incorrect" using error message customization:

http://webwork.maa.org/wiki/ErrorMessageCustomization

I don't know how to fix the other problem (forcing reduced fractions).

Good luck!

Paul Pearson
In reply to Jason Aubrey

Re: Force reduced fractions in lists

by Robin Cruz -

Jason,

You can change the error message and you can do it without loading the answerHints.pl. Here's an example where the correct answer could be a string or a list. I took a substring of the original error message I wanted to change to use in the checker--You could just put " " for the error message if you didn't want a message to show up. I put the code at the end of this note.

--rac

-------------------------------------------------


#SET-UP

Context()->strings->add(
  "No zeros"=>{alias=>'None'},
  "No solution"=>{alias=>'None'}
);

$a1 = list_random(3,5);
$b1 = list_random(7,4,2,1,-1,-2,-4,-7);
$c1 = list_random(-2,-1,1,2);
$p[0] = Compute("$a1 x^2 + ($a1*$c1+$b1) x + $b1*$c1")->reduce;
$temp = round(-1000*$b1/$a1)/1000;
$z11 = Real("$temp");
$z12 = Real("-1*$c1");
$a[0] = List($z11,$z12);

$a2 = random(2,5,1);
$b2 = non_zero_random(-4,4,1);
$c2 = random(5,20,1);
$p[1] = Compute("$a2 x^2 + $b2 x + $c2")->reduce;
$a[1] = List("No zeros");

$n1 = random(0,1,1);

# ANSWER CHECKED

$ans = $a[$n1];
ANS($ans->cmp->withPostFilter(sub { 
  my $ans = shift; 
  $ans->{ans_message} = "Enter the zeros of the function or $BITALIC No zeros$EITALIC. " 
    if $ans->{ans_message} =~ m/not defined/;
  return $ans; 
}));
In reply to Jason Aubrey

Re: Force reduced fractions in lists

by Davide Cervone -
Jason:

The problem has to do with the way lists do their checking, and how (or actually when) the Fraction object does the reduction check. The reduction check is done within the Fraction object's post-processing of the answer checker, and but the list checker doesn't actually call that (because it is not part of the equality check that it is doing to compare student entries to correct ones). This has come up before, and probably means that the pos-processing code should be refactored, but such changes are delicate with such a complicated package like MathObjects.

Anyway, the solution (to both problems, as it turns out) is to use a custom checker for the list check. Note that for a list object, checker refers to the checker used to compare individual entries in the list to correct ones, not to a checker for the entire list (which is list_checker. I give a modified version of the key parts of the code below:

    DOCUMENT();
    
    loadMacros(
      "PGstandard.pl",
      "MathObjects.pl",
      "contextFraction.pl"
    );
    
    TEXT(beginproblem());
    $showPartialCorrectAnswers = 1;
    
    Context("Fraction-NoDecimals");
    Context()->strings->add(
      "no solution"=>{},
      "none"=>{alias=>"no solution"}
    );
    Context()->flags->set(reduceFractions=>0);
    
    $var = list_random('x','y','z','w','p','q','r','s','t','u','v');
    Context()->variables->are($var => 'Real');
    
    $a = random(2,20)*random(-1,1,2);
    $b = -sgn($a)*random(1,20);
    $k = non_zero_random(-15,15);
    
    $eqn = "$k - \left| $a $var + $b\right| = $k";
    $answer = List("-$b/$a,1/2");
    
    BEGIN_TEXT
    Solve the following equation for \($var\):
    $PAR
    \[ $eqn \]
    $PAR
    \( $var = \) \{ ans_rule(20) \}
    END_TEXT

    ANS($answer->cmp(
      entry_type=>"a fraction",
      checker => sub {
        my ($correct,$student,$ans,$nth,$value) = @_;
        return 0 unless Value::classMatch($student,'Fraction');
        return $correct == $student if $student->isReduced;
        $correct->context->setError("Your $nth $value is not reduced","",undef,undef,$Value::CMP_WARNING)
          unless $ans->{isPreview};
        return 0;
      }
    )); 
    
    ENDDOCUMENT();
Note that I've added the reduceFractions=>0 flag to prevent student answers from being reduced automatically. I also add the variable used for the problem to the context (since the student might type that, and I'd prefer not to see the "variable not defined" message, since the variable IS defined.

The main change is in the custom checker. We set the entry_type to "a fraction" so that the error messages will be a bit better (otherwise it would be "fraction of integers" which is awkward in most of the messages).

The checker itself gets that correct and student answers, plus the answer hash and strings that can be used for error messages to indicate which list entry ($nth) we are, and what type of object ($value). We return 0 (no correct) if the student answer isn't a fraction (among other things, this will take care of the case where they enter "no solution" without causing an error message). Then, if the student's answer is reduced, return whether it equals the correct answer or not. Othewise (not reduced), print a warning message (unless we are previewing the asnwers), and return 0 (no correct).

That should do what you want, I hope.

Davide

In reply to Davide Cervone

Re: Force reduced fractions in lists

by Jason Aubrey -
Thanks! We had to tweak this a bit since we wanted integer answers to be accepted too. The answer checker was accepting 2/1 but rejecting 2. We also added some checking so that it would gracefully handle string answers when fractions were expected and handle fractions when string answers were expected...
ANS($answer->cmp( entry_type => "a fraction",
 checker => sub {
 my ($correct, $student, $ans, $nth, $value) = @_;
  if (Value::classMatch($correct,'String')) {
 return $correct == $student;
  } else {
  return 0 if Value::classMatch($student,'String');
  if (Value::classMatch($student, 'Real')) {
  $student = Fraction($student) if floor($student)==$student;
  }
  return 0 unless Value::classMatch($student,'Fraction');
  return $correct == $student if $student->isReduced;
  $correct->context->setError("Your $nth $value is not reduced", "", undef, undef, $Value::CMP_WARNING)
 unless $ans->{isPreview};
  return 0;
  }
 }
)); 
In reply to Jason Aubrey

Re: Force reduced fractions in lists

by Davide Cervone -
Here is an simpler version:
    ANS($answer->cmp( entry_type => "a fraction",
      checker => sub {
        my ($correct, $student, $ans, $nth, $value) = @_;
        return $correct == $student unless Value::classMatch($student,'Fraction') && !$student->isReduced;
        $correct->context->setError("Your $nth $value is not reduced","",undef,undef,$Value::CMP_WARNING)
          unless $ans->{isPreview};
        return 0;
      }
    ));
It should work for all the situations you indicate as well. The $correct == $student check will work whether the student and correct answers are Strings, Reals, or Fractions, so really the only thing we need to check is if the warning message needs to be given (student entered a fraction that is not reduced).

Hope that works for you.

Davide

In reply to Davide Cervone

Re: Force reduced fractions in lists

by Richard Lynch -
Hi Davide, My name is Rick and I am the one working with Jason on this at Mizzou. I greatly appreciate your help thus far on this issue! I have a further question, though, if you don't mind? 

The code: 

sub checkFracList { 
  return (
    entry_type => "a fraction",
    checker => sub {
      my ($correct, $student, $ans, $nth, $value) = @_;
      if (Value::classMatch($correct,'String')) {
        return $correct == $student;
      } else {
        return 0 if Value::classMatch($student,'String');
        if (Value::classMatch($student, 'Real')) {
          $student = Fraction($student) if (floor($student)==$student));
        }
        return 0 unless Value::classMatch($student,'Fraction');
        return $correct == $student if ($student->isReduced);
        $correct->context->setError("Your $nth $value is not reduced", "", undef, undef, $Value::CMP_WARNING) unless $ans->{isPreview};
        return 0;
      }
    }
  );
}

works, but I realized that if the answer is not correct, it will still tell the students that it is not reduced. For instance, say that the answers are 1/4, -5/8 and a student enter 6/8, -5/8. Now 6/8 is clearly wrong and it will count it as incorrect, however, it still displays the message that 6/8 is not reduced. I think this could give the wrong impression to the student that they are correct, but just need to reduce their answer. Is there anyway to fix this? I tried to ignorantly replace

$correct->context->setError("Your $nth $value is not reduced", "", undef, undef, $Value::CMP_WARNING) unless $ans->{isPreview};

with the if statement:

if ($correct == $student) {
  $correct->context->setError("Your $nth $value is not reduced", "", undef, undef, $Value::CMP_WARNING) unless $ans->{isPreview};
}

but I believe the way that it is looping through the answers to check them makes this approach fail.

Thanks!
Rick
In reply to Richard Lynch

Re: Force reduced fractions in lists

by Davide Cervone -
You are right, the list checking is a little more complicated than you might think. For unordered lists, the checker may be called for the same student answer multiple times (as it looks for a correct answer that it matches). So the error messages from this phase of the checking are not retained (as otherwise spurious messages might be produced from comparing the student answer to the wrong correct ones). After it finds all the student answers that are correct, the list checker does a second round of comparisons on all the incorrect answers (comparing them to the first entry in the list) just to get syntax errors or other error messages.

The reason my original code worked is that an unreduced fraction always produced an error, no matter what it was compared to, so the second phase produces the error message that is needed. But your fails since in order to produce the error message it has to be compared to the reduced answer that it matches (and the second phase just compares to the first entry in the list).

There are two ways around this.

The first is that you are allowed to supply a second checker that is used during the second phase (just to do syntax checking on the answers that don't match anything in the list). We could use the first answer checker to mark a fraction as not reduced when it matches a correct answer but isn't reduced, and then use the second to report that error. E.g.,

    ANS($answer->cmp(
      entry_type => "a fraction",
      checker => sub {
        my ($correct, $student, $ans, $nth, $value) = @_;
        return 0 unless $correct == $student;
        return 1 unless Value::classMatch($student,'Fraction') && !$student->isReduced;
        $student->{notReduced} = 1 unless $ans->{isPreview};
        return 0;
      },
      extra => sub {
        my ($student, $ans, $nth, $value) = @_;
        $student->context->setError("Your $nth $value is not reduced","",undef,undef,$Value::CMP_WARNING)
          if $student->{notReduced};
      }
    ));
Here, we only check it the fraction is reduced when it is actually equal to the correct answer. In that case, we set a flag on the student answer that we can use later. Then the extra subroutine is used to check the extra answers (ones that didn't match anything in the list, i.e., are incorrect), and there we look for the flag and produce the warning if it is needed.

For this situation, it is not actually necessary to use the two separate checkers. The two functions can be combined into a single checker routine, as follows:

    ANS($answer->cmp(
      entry_type => "a fraction",
      checker => sub {
        my ($correct, $student, $ans, $nth, $value) = @_;
        $correct->context->setError("Your $nth $value is not reduced","",undef,undef,$Value::CMP_WARNING)
          if $student->{notReduced};
        return 0 unless $correct == $student;
        return 1 unless Value::classMatch($student,'Fraction') && !$student->isReduced;
        $student->{notReduced} = 1 unless $ans->{isPreview};
        return 0;
      }
    ));
Here, we produce the error message whenever the student answer has been marked as not reduced. If this is still in the first phase, the error message will be ignored, so it doesn't matter if we generate it (and we go on to do the checking to see if the answer is correct and reduced or not, marking the student answer as not reduced for later). But if we are in the second phase, we produce the error message and it is displayed.

So this latter version might be the easiest to use.

Davide

In reply to Davide Cervone

Re: Force reduced fractions in lists

by Richard Lynch -
I ended up using the second form and it works. I really appreciate it! Thanks a ton!! 

Here is the current code I am using:

ANS($answer->cmp(
  entry_type => "a fraction",
  checker => sub {
    my ($correct, $student, $ans, $nth, $value) = @_;
    return $correct == $student if Value::classMatch($correct,'String');
    return 0 if Value::classMatch($student,'String');
    $student = Fraction($student)->reduce if Value::classMatch($student,'Real') && floor($student) == $student;
    return abs($student - $correct) < 1/1000000 if Value::classMatch($student,'Real');
    $correct->context->setError("Your $nth $value is not reduced", "", undef, undef, $Value::CMP_WARNING) if $student->{notReduced};
    return 0 unless $correct == $student;
    return 1 unless Value::classMatch($student,'Fraction') && !$student->isReduced;
    $student->{notReduced} = 1 unless $ans->{isPreview};
    return 0;
  }
));

Now it looks a little different, so first let me explain what more I am trying to do (maybe I'm going a little too crazy with reduced fractions??)

First off, I added the lines 

return $correct == $student if Value::classMatch($correct,'String');
return 0 if Value::classMatch($student,'String');

because it kept saying something along the line numerators of fractions must be integers and the fix you mentioned two posts ago didn't work for some reason. The next two lines 

$student = Fraction($student)->reduce if Value::classMatch($student,'Real') && floor($student) == $student;
return abs($student - $correct) < 1/1000000 if Value::classMatch($student,'Real');

are what I'm trying to do next. My goal is to allow them also enter functions (only when necessary) in their answers as well as reduced fractions. For instance, maybe the problem has as answer sqrt(3)/2 and -1/2 and I'd like to still force no decimals and reduced fractions. Actually, the code I just posted above works for this given I do 

$answer = List(Fraction(sqrt(3)/2),Fraction(-1,2));

since it will just check if the student entered an integer and turn it into a fraction otherwise (the only way a student can enter something that isn't an integer and is real is if they do it with a function since they can't enter decimals).

Now here's the issue. I also want to force reduced functions. Say the answers are sqrt(3)/2, 4. In this case (let's just worry about sqrts for now), I want to make sure they enter sqrt(3)/2, 4 rather than sqrt(3)/2, sqrt(16) in addition to forcing the reduced fractions. Is there a way to do this? I have seen your SimplifiedSqrt example on the wiki, but my attempts at trying to extend it have not worked. In fact, I wasn't even able to extend it to lists (without forcing reduced fractions) as I believe it is the same issue as before with the way that a lists loops through the answers.

Any help would be greatly appreciated. Thanks in advance!
In reply to Richard Lynch

Re: Force reduced fractions in lists

by Alex Jordan -
I've just finished putting together the attached contextLimitedRadicals.pl file. This is 99% Davide Cervone's code from the LimitedSqrt context that can be found in the forums, together with two items that I communicated with him directly about.

One of those items is a general nth root command root(n,x).

The other is a set of bizarro replacements for +, - , and /. The first two effectively require students to group like terms (not get away with sqrt(2)+sqrt(2)) and the last effectively requires students to reduce fractions without the full machinery of the Fraction context.

I've tried to put good notes on usage into the header.

Davide - I was wondering if you could take a look at this some time to check for obvious mistakes or places where something might be done better.