[In a similar vein: is there any way to require the answer be given in fractional form, e.g. 4/5, and not decimal form, e.g. 0.8, short of explicitly requiring a numerator and denominator?]
The second question has been addressed here previously but I'm curious as to the conclusions people drew: what is the best way to implement factorizing in webwork? For example, given x^2 + 2x - 3, what's the best way to write/accept x-1 & x+3 as the solutions? In particular, I'm worried about clever ways of breaking this, like x-1 and (x^2 + 2x - 3)/(x-1), as well as potentially accepting factorization-up-to-unit, like 2x - 2 and (x/2 + 3/2).
Finally -- and this one's going to be a monster -- a number of problems that we're trying to implement involve "simplifying" the given expression. Obviously "simplifying" is an ill-defined term in the first place; I'm particularly concerned about implementing it in webwork, though. My current plan is to accept their answer as a formula then, in addition to checking its correctness as a function, impose some heuristic on its functional tree (e.g. depth no greater than four & no more than 15 nodes). Is this a good idea? If so, how does one go about implementing this in webwork? I sort of know how to do it when the required answer involves nothing but BOPs -- though I could definitely use a primer if anyone has the chance -- but I'm not sure how to implement it in general.
I'd greatly appreciate an answer as soon as possible since the summer curriculum just got changed and I need to get some of this problems up and running by next week. Thank you very much for your time, and for the efforts you've all made in creating webwork.
The first question is about disallowing decimals in the answers. If you use the MathObjects rather than the traditional answer checkers, you will be able to exercise more control over what the students can enter. For example:
loadMacros("MathObjects.pl"); Context("Numeric"); Parser::Number::NoDecimals; ANS(Compute("(-3+sqrt(21))/2")->cmp);will force the students to enter the answer without using explicit decimals.
You are also allowed to limit the operations and functions that the student can access. For example, you can disable exponentiation via:
Context()->operators->remove('^','**');so students would have to use sqrt() rather than raising to the 1/2 power, for example.
To limit the functions available, you can use
Context()->functions->disable("sqrt")to disallow the sqrt() function, or
Context()->functions->disable("Trig");to disabled all the trig functions, or
Context()->functions->disable("Trig"); Context()->functions->enable("sin","cos");to allow only the sin() and cos() trig functions, or
Context()->functions->disable("All");to allow no function calls. There was a discussion of this at one point, but I can't find the reference to it. There are a variety of categories that can be used; see the file pg/lib/Parser/Context/Functions.pm for the complete list.
Davide
Thanks
You can force the student to enter just a single number by disabling all the operators and functions. There is already a context that does this:
Context("LimitedNumeric");It corresponds to num_cmp's "strict" mode (in fact, this is the context that implements that mode behind the scenes).
It is possible to overcome the issues of tolerances I describe above by starting with the LimitedNumeric context, and setting a special value that is used to check the numbers entered in a formula:
Context("LimitedNumeric"); Context()->flags->set( NumberCheck => sub { my $self = shift; $self->Error("You should enter numbers with at most 2 decimal places") if $self->{value_string} =~ m/~~.~~d~~d~~d/; }, tolType => 'absolute', tolerance => .005, ); $ans = Real(prfmt(sqrt(2),"%.2f")); ANS($ans->cmp);This restricts the student to entering only numbers with at most 2 decimal places, and uses absolute tolerances that should only match the correct answer.
Davide
Our basic feeling on the 2dp answer is simply to disallow the
student from inputting the answer from their calculator wholesale, or from entering sqrt(2), by setting the tolerance sufficiently fine. You're quite right that doing an exactly 2dp answer is hard; my solution was simply to write a custom answer checker that required the literal string of the students answer [pulled out of the answer hash] to pattern match (after stripping out whitespace) and evaluate correctly. Something like
sub decimalChecker {
my $correct = shift; my $student = shift; my $ah = shift;
my $originalAnswer = $ah->{original_student_ans};
$originalAnswer =~ s/~~s+//g;
return ($student == $correct && $originalAnswer =~ /^~~d*~~.~~d~~d$/;
}
as the answer checker. I think this covers the bases, but maybe I've missed something?
Also, for future reference: can the answer checkers receive parameters
from a MathObject? That is, can I do something like
ANS( Value("sqrt(2)")->cmp( checker => ~~&decimalChecker(6) );
so that I could, for example, write a single routine that would
automatically check answers to n dp?
I think this covers the bases, but maybe I've missed something?
Assuming you have used LimitedNumeric context, then I think your method will be OK. A slightly easier final check would be
return($originalAnswer eq $correct->string);which avoids going through the fuzzy real check unnecessarily (it could return false if the tolerances aren't set correctly, even when the answer is "right"). Actually, the "->string" is not strictly necessary since the correct answer will be stringified automatically.
I assume that if the answer is 1.41 and the student answers 1.410 you want to mark that as false? This is what your code currently will do, so it will be important to make it clear in the wording of the problem or students may become confused (and irate).
You might also consider setting the LaTeX preview string so that it shows what the student actually typed (since it will lose the trailing 0's otherwise and may cause confusion in that case when their answer is marked wrong especially when the correct answer looks identical). You could even set the student_ans field so it contains the trailing 0's as well.
My version of the answer checker is the following:
sub decimalChecker { my ($correct,$student,$ans) = @_; return 0 unless $correct->class eq $student->class; my $original = $ans->{original_student_ans}; $original =~ s/~~s+//g; $ans->{preview_latex_string} = $original; return $original eq $correct; }
can the answer checkers receive parameters from a MathObject?
Not the way you have suggested. You can't mix getting a pointer to a subroutine with passing arguments to the subroutine like that. You can, however, add more flags to the cmp call and they will be available in the answer hash:
ANS(Real(1.41)->cmp(decimals=>2,checker=>sub { my ($correct,$student,$ans) = @_; my $decimals = $ans->{decimals}; ... }));(It would also be possible to make a decimalChecker subroutine that returns a code reference to a subroutine that does the correct number of digits, something like
sub decimalChecker { my $n = shift; sub { my ($correct,$student,$ans) = @_; ... (uses $n internally) } }and then you can use ANS(Real(1.41)->cmp(checker=>decimalChecker(2))) but it is probably not worth it. If you do, you should be sure to set the correct_ans field of the answer hash so that the student will see the correct number of digits when he views the correct answer.)
I have a better method that I will post later, but I have to run off to graduation now.
Davide
Also, if you are going to be asking for exactly the number of digits you want, you probably want to set
Context()->{format}{number} = "";rather than the default "%g" so that numbers will be shown in their full precision rather than only to 6 digits or so.
Davide
First, make a file called "fixedPrecision.pl" in your course's templates/macros directory that contains the following:
sub _fixedPrecision_init {} package FixedPrecision; our @ISA = ("Value::Real"); sub new { my $self = shift; my $class = ref($self) || $self; my $x = shift; my $n = shift; Value::Error("Too many arguments") if scalar(@_) > 0; if (defined($n)) { $x = main::prfmt($x,"%.${n}f"); } else { $x =~ s/\s+//g; my ($int,$dec) = split(/\./,$x); $n = length($dec); } $self = bless $self->SUPER::new($x), $class; $self->{decimals} = $n; $self->{isValue} = 1; return $self; } sub string { my $self = shift; main::prfmt($self->value,"%.".$self->{decimals}."f"); } sub compare { my ($l,$r,$flag) = @_; if ($l->promotePrecedence($r)) {return $r->compare($l,!$flag)} $l cmp $r; } package FixedPrecisionNumber; our @ISA = ("Parser::Number"); sub new { my $self = shift; my $class = ref($self) || $self; my $equation = shift; my $context = $equation->{context}; $self = bless $self->SUPER::new($equation,@_), $class; $self->{value} = FixedPrecision->new($self->{value_string}); return $self; } sub string {(shift)->{value}->string(@_)} sub TeX {(shift)->{value}->TeX(@_)} package main; Context()->{parser}{Number} = "FixedPrecisionNumber"; sub FixedPrecision {FixedPrecision->new(@_)}; 1;(I've attached a copy of this file for your convenience.)
This file defined a new MathObject class (FixedPrecision) that is a subclass of the Real class that adds a new field {decimals} that determines how many decimal places to use. It also creates a Parser class that replaces the standard Number class that uses the new FixedPrecision object to implement numbers in the Parser.
By default, FixedPrecsion(n) will take however many decimal digits there are in n as the number of digits, or you can specify the number explicitly as FixedPrecision(n,digits). For example, FixedPrecision(sqrt(2),3) would produce a value 1.414 that the student must match exactly.
You should use this within the LimitedNumeric context, as in the following example:
DOCUMENT();
loadMacros(
"PGstandard.pl",
"Parser.pl",
);
TEXT(beginproblem);
Context("LimitedNumeric");
loadMacros("fixedPrecision.pl"); # must come after Context is set.
$n = FixedPrecision(sqrt(2),3);
BEGIN_TEXT
\($n\) = \{ans_rule(10)\}
END_TEXT
ANS($n->cmp);
ENDDOCUMENT();
The FixedPrecision class overrides the string and compare methods to implement the fixed-precision output and to do a string comparison rather than a (fuzzy) numeric comparison, so that only the exact number of digits will be marked correct.
Note that this is not a full implementation of a MathObject class, which would have to be more sophisticated in order to handle things like addition, subtraction, and so on correctly. This means it can only be used in the LimitedNumeric context where those operations will never be performed. This class also won't handle scientific notation properly. It would be possible to make a more complete implementation of this idea, but there are details to be worked out.
Davide
It's been a while since I checked back in here, but: we've implemented the FixedPrecision class as you recommended above, and everything works fine provided the answers are all positive. If the student enters a *negative* answer, however, the student's answer becomes artificially truncated by the comparitor. Specifically, a negative answer is automatically truncated to 0 dp, irrespective of the parameters of the FixedPrecision object. So, for example, code like
ANS( $py3->cmp );
won't accept anything, because the student's answer can be, at best, -1. The only way an answer of -.917 can be accepted is to write something like
ANS( $py3->cmp );
which (obviously) isn't what we want.
I haven't followed the boards in a while so I don't know if there's something bigger and better out there, but could someone please advise on how to fix this problem?
Thanks much,
-- Robert
Davide
package FixedPrecision;
our @ISA = ("Value::Real");
sub new {
my $self = shift; my $class = ref($self) || $self;
my $x = shift; my $n = shift;
Value::Error("Too many arguments") if scalar(@_) > 0;
if (defined($n)) {
$x = main::prfmt($x,"%.${n}f");
} else {
$x =~ s/\s+//g;
my ($int,$dec) = split(/\./,$x);
$n = length($dec);
}
$self = bless $self->SUPER::new($x), $class;
$self->{decimals} = $n; $self->{isValue} = 1;
return $self;
}
sub string {
my $self = shift;
main::prfmt($self->value,"%.".$self->{decimals}."f");
}
sub compare {
my ($l,$r,$flag) = @_;
if ($l->promotePrecedence($r)) {return $r->compare($l,!$flag)}
$l cmp $r;
}
package FixedPrecisionNumber;
our @ISA = ("Parser::Number");
sub new {
my $self = shift; my $class = ref($self) || $self;
my $equation = shift; my $context = $equation->{context};
$self = bless $self->SUPER::new($equation,@_), $class;
$self->{value} = FixedPrecision->new($self->{value_string});
return $self;
}
sub string {(shift)->{value}->string(@_)}
sub TeX {(shift)->{value}->TeX(@_)}
package main;
Context()->{parser}{Number} = "FixedPrecisionNumber";
sub FixedPrecision {FixedPrecision->new(@_)};
1;
And here's some of the code in which it's being implemented:
"PGbasicmacros.pl",
"PGchoicemacros.pl",
"PGanswermacros.pl",
"PGauxiliaryFunctions.pl",
"PGgraphmacros.pl",
"Parser.pl",
"UWMadisonMacros.pl");
# Should be MathObjects nowadays, I believe
# UWMadisonMacros isn't actually being used in this problem, I'm just including it for completeness
$pxexact = "2/5";
$pyexact = "-sqrt(1-($pxexact)**2)";
$pxapprox = 0.400;
$pyapprox = -sqrt(1-$pxapprox**2);
$qxexact = $pyexact;
$qyexact = $pyexact;
$qxapprox = $pyapprox;
$qyapprox = $pyapprox;
$rxexact = $qxexact;
$ryexact = "-" . $pxexact;
$rxapprox = $qxapprox;
$ryapprox = - $pxapprox;
# Ask for exact answers here...
# This works fine
ANS(num_cmp($pxexact));
ANS(num_cmp($pyexact));
ANS(num_cmp($qxexact));
ANS(num_cmp($qyexact));
ANS(num_cmp($rxexact));
ANS(num_cmp($ryexact));
# Ask for approximate answers here
# Want answers to 3dp
Context("LimitedNumeric");
loadMacros("fixedPrecision.pl"); # must come after Context is set.
$px3 = FixedPrecision($pxapprox, 3); #student must enter .400 not .4
$py3 = FixedPrecision($pyapprox, 3);
ANS($px3->cmp);
ANS($py3->cmp);
$qx3 = FixedPrecision($qxapprox, 3);
$qy3 = FixedPrecision($qyapprox, 3);
ANS($qx3->cmp);
ANS($qy3->cmp);
$rx3 = FixedPrecision($rxapprox, 3);
$ry3 = FixedPrecision($ryapprox, 3); # Student must enter .400 not .4
ANS($rx3->cmp);
ANS($ry3->cmp);
[And I don't recall offhand how to check which version of PG we're using. Would you mind reminding me? We're in the 2.x but -- especially not having been on the project for a few months -- I'd be reluctant to guess.]
You could use cvs status on one of the files in the pg directory to see whether there are any sticky tags set (like rel-2.4.0 or something like that). If not, then do cvs status on pg/lib/Value/AnswerChecker.pm and tell me what the working revision number is.
Davide
File: AnswerChecker.pm Status: Up-to-date
Working revision: 1.91.2.1
Repository revision: 1.91.2.1 /webwork/cvs/system/pg/lib/Value/AnswerChecker.pm,v
Sticky Tag: rel-2-4-dev (branch: 1.91.2)
Sticky Date: (none)
Sticky Options: (none)
There have been a number of important changes to the MathObjects since version 1.91 of AnswerChecker.pm, so I suspect the differences we are seeing are due to that. The "up-to-date" indicator is that the file you have is up to date as far as the rel-2.4-dev release is concerned. The current HEAD version is 1.115.
I would suggest updating to the HEAD version of PG; it seems to be in a pretty stable state right now. That can be done without updating the rest of WeBWorK, but might not be something you want to do in the middle of the semester, so you may want to wait until the end of the term for that. I think you will find that the FixePrecision object will work correctly with the more current version of MathObjects.
Davide
Context("LimitedNumeric-StrictFraction");This disables all functions and operations (except those needed for fractions), and sets the context's NumberCheck to one that prevents decimal numbers from being entered. (There is no mode that corresponds to this in the traditional num_cmp() checker.)
I think this context may still have "pi" and "e" defined in it, so students can enter fractions that include these (e.g., pi/2), but that is unlikely. You can disable them with
Context("LimitedNumeric-StrictFraction"); Context()->constants->remove("pi","e"); ANS(Compute("4/5")->cmp);The Compute function creates an appropriate MathObject from the string provided, and in addition makes that string the correct answer string, rather than 0.8 which it would be if you used Real(4/5) instead. (At some point I need to make an honest fraction MathObject class.)
Hope that does what you want.
Davide
This was discussed a couple of summers ago at
http://webwork.maa.org/moodle/mod/forum/discuss.php?d=2608and there are a number of code examples that illustrate various approaches to the problem. I think the MultiPart ones are your best choices, but you may have different needs.
Since that discussion, we now have a context that allows only polynomial answers:
loadMacros("contextLimitedPolynomial.pl"); Context("LimitedPolynomial");You can use this rather than Context("Numeric") to force the individual responses to be polynomials (preventing answers like (x^2 + 2x - 3)/(x-1) from being entered). It would also be possible to extend the LimitedPolynomial context to allow products of polynomials (or even limiting to monic polynomials, or linear ones), but that is not currently available, and I'm not sure when I'll get to it.
Davide
The Formula MathObject contains the parse tree for the formula, and it is possible to inspect the structure of the tree, but there are no pre-defined methods for traversing the tree, so it is not easy to do. But you at least you don't have to parse the equation yourself. Analyzing the structure algebraically can be quite tricky, and your students will almost certainly come up with situations you didn't think of.
I am a little concerned that you need to have these by next week. Writing significantly new answer checkers like this is a subtle and time-consuming process. I would not expect to have production versions in that short a time.
Good luck with your course.
Davide
[And yes, this relies crucially on the fact that the MathObject comes pre-parsed. Just thinking about parsing by hand gives me the chills.]
At the moment, btw, we're implementing a mix of computational complexity and pattern matching as a rough heuristic to define "simplified", since we can already compare the answers as functions. It's risky in the sense that we're assuming people will go wrong in predictable ways, but given the context it's about as good as we're gonna get. Once again I find myself using the $ah->{original_student_ans}; it seems deprecated in the code (and in these forums) but it's just so useful for these kinds of questions. Is there anything I should be careful of when using the answer hash beyond the obvious (e.g. don't write over the student's answer)? Is it considered déclassé? Or have I just been looking in the wrong places?
Also, since I didn't say so above: thank you very much for your time. Given the time-frame I'm working in, your speedy response was a godsend.
The main issue is to know the various node types (BOP, UOP, List, etc.) and how to find the leaves from each type. You have already worked out BOP and UOP. The other nodes correspond to the various subclasses of Parser::Item (all in the top level of pg/lib/Parser/). It's not too bad, really, but nothing is documented, and you have to do all the tree traversal by hand. The parser was not designed with user-traversal in mind, but this is not the first time it has come up, and I should think about making an API to access it.
Concerning $ah->{original_student_ans}, I don't have any problem with your using it, but you might consider using the parsed student answer to produce its corresponding string rather than the original string. This will be more "normalized" in the sense that all the operations are shown consistently and without extra spacing, and so on, so you don't have to worry about irregularities of the student's input.
For example, if you have used my ($correct,$student,$ah) = @_; in a custom checker, then you can use $student->string to get the string version of the student's answer. If I recall correctly, this is also what has been set as $ah->{student_ans} initially.
There are also two other values in the answer-hash that might be of interest: $ah->{student_formula} and $ah->{student_value}. The latter is what gets passed to $student, and is the MathObject holding the student's answer (it will be a Real, Complex, Vector, etc.). If the student's answer is a numeric one (as opposed to a Formula), then all you get is the final answer, not the parse tree that produced it. The $ah->{student_formula} is the parsed version of the student's answer, even if it ends up producing a numeric result. (If it is a formula, then $ah->{student_value} and $ah->{student_formula} will be the same.) This lets you analyze the student's answer even when it is not a formula.
Note that if the student gives a constant answer, even when the answer checker is looking for a Formula, $ah->{student_value} (and so $student) will be a Value object rather than a Parser object, and so it will not have a {tree} field. You may want to use $ah->{student_formula} rather than $student if you are going to traverse the tree. Otherwise, use $student = main::Formula($student) to force it to be a (constant-valued) formula rather than a constant so that it has a {tree} to work with.
Good luck with your project.
Davide
Is anyone aware of any library factoring problems that use the (newer) methods described above. The old factoring problems usually require that each factor is entered in a separate box, and are pretty confusing for students.
Thanks for any help.
Lars.
I have some factoring problems for an Intermediate Algebra course which use MathObjects. I hope to have them out to the library by the first part of August 2008. You may look into them at
http://docralph.collegeofidaho.edu/webwork2/Intermediate_Algebra/
You may log in with any of the usernames: student1, student2, student3, student4 or student5
The password for all is: practice
Currently, the problems are all "pinked" since I have some unfinished work to do yet and there is a file missing which makes the config file "unhappy" -- I don't think the factoring problems need this file. So, the problems should work. I hope to get back to work on these now that my classes are over for the year.
--rac
When I first started WeBWorK I knew no Perl, and thought there was no difference between == and eq. Early on, I once wrote a custom answer checker and used eq when I probably would have been using == had I known any better. But upon later review of this problem I realized eq was making the factoring business work almost beautifully.
Simple example first:
Send $ans to a custom checker, where $ans = Compute("2(x+1)"). Have a custom checker simply compare $correct->reduce with $student->reduce using eq instead of ==, and here's what happens:
1. You're using a Math Object, so Math Object syntax error messages still work.
2. These things count as correct:
2(x+1)
2 ( x + 1)
2*(x+1)
([([2])]) * (1*x+1)
3. These things do not, (but *would* with ==) :
(x+1)*2
2(1+x)
2x+2
Despite the last examples being functionally equivalent Formulas to 2(x+1), they are not counted as correct because their reduced strings are different. And yet the ->reduce method removes most of the annoying types of equivalency, like extra space, multiplication by 1, and extra parens, to name a few. Even this small example could be the answer to a beginning algebra "distribute out the greatest common factor" problem.
But you can take this concept and apply it to (x+1)(x+2) in a factoring problem. It's functionally equivalent to x^2+3x+2, but you don't want that to count as correct. You only want:
(x+1)(x+2) (x+1)(2+x)
(1+x)(x+2) (1+x)(2+x)
(x+2)(x+1) (x+2)(1+x)
(2+x)(x+1) (2+x)(1+x)
to count as correct, including all variations with more parens, the inclusion of a * between factors, and other things that don't matter when using eq on reduced Formulas.
Using this idea, I wrote a problem that can take up to three factors, each having up to three terms, present the product, and ask for a factorization. As far as I can tell, it works exactly the way any of us would want it to. I'll include it below. It does not try to manually list out permutations like those 8 above, but rather uses permutation hashes to automatically generate them in an array.
The bigger discovery that I want to share is how using eq might solve certain problems with Math Objects, when they become "too forgiving" with equivalency. eq is a little more discriminatory than ==.
Alex
_______________________________
Here is the code for that factoring problem. I'd love to hear if anyone has a way to:
-upgrade it to any number of factors.
-upgrade it to factors with any number of terms
-allow (1+x)^2 to be entered that way, instead of forced as something like (1+x)(1+x)
# DESCRIPTION
# WeBWorK problem written by Alex Jordan
# Portland Community College
# ENDDESCRIPTION
## DBsubject('Algebra')
## DBchapter('Polynomial and Rational Functions')
## DBsection('Polynomial Functions')
## KEYWORDS('factoring')
## Author('Alex Jordan')
## Institution('PCC')
##############################################
DOCUMENT();
loadMacros(
"PGstandard.pl",
"MathObjects.pl",
"PGpolynomialmacros.pl",
);
##############################################
#This problem displays an expanded polynomial and asks for it to be factored. The answer is a Formula and will give appropriate syntax error messages for Formula in Numeric context. The order of factors does not matter. The order of terms within any factor does not matter.
#The answer must be a product of binomials and trinomials, perhaps with a monomial. If constants can be distributed out they must be, and they must be simplified. Having more than three factors, or using factors with more than three terms would require more than just surface alteration, but is theoretically do-able. However, an array of super factorial size is being created here, so maybe moving up to more factors would be a bad idea. As it is, 3 factors each with three terms makes an array of size (3!)^4. You might want to stick to no more than one trinomial.
#To use this problem for <= 3 factors with <= 3 terms,
#1. Fill in the coefficient array @coef below to reflect the factors. The factors should already be irreducible, except that a monomial divisor is allowed in any *one* factor, and a constant divisor is allowed in any factor. If you want a doubled or tripled factor like (ax+b)^2, it will have to be written by students as a product: (ax+b)(ax+b) [or (ax+b)(b + xa), etc]. Take all of this into consideration when randomizing the coefficients.
#2. If you change the number of factors down to two or one, leave trivial [1] rows in @coef.
#Define an hash of hashes for permutations in S_3, S_2.
#Perl might have a classier way to do this so that it wouldn't be tedious to #upgrade to four factors.
%S3 = (A => {0 => 0, 1 => 1, 2 => 2}, B => {0 => 0, 1 => 2, 2 => 1},
C => {0 => 1, 1 => 0, 2 => 2}, D => {0 => 1, 1 => 2, 2 => 0},
E => {0 => 2, 1 => 1, 2 => 0}, F => {0 => 2, 1 => 0, 2 => 1},);
%S2 = (A => {0 => 0, 1 => 1}, B => {0 => 1, 1 => 0},);
%S1 = (A => {0 => 0},);
Context("Numeric");
Context()->reduction->set('(-x)-y'=>0, '(-x)+y'=>0);
#Not desired reductions for this kind of problem.
@coef = ([2,4,10], #One factor is (x+4), etc. Could be randomized.
[1,4],
[3,0]);
$n = $#coef;
#Get the array of coefficients for the product
@prod = PolyMult(~~@{$coef[0]},~~@{$coef[1]});
@prod = PolyMult(~~@prod,~~@{$coef[2]});
#Make the presented polynomial. I'm using PolyString (included in
#PGpolynomialmacros.pl) but it currently doesn't handle negative Math Objects
#correctly. That could easily be fixed. Anyway, for this current example,
#keep the original coefficients as Perl reals.
$polynomial = Formula(PolyString(~~@prod))->reduce;
#Pull out any constants that can be pulled out.
$const = 1;
foreach my $i (0..$n) {
my $z = @{$coef[$i]}-1;
my $g = 1;
if ($z > 0) {
$g = gcd(@{$coef[$i]});
foreach my $j (2..$z) {
$g = gcd($g,$coef[$i]->[$j]);
}
}
else {$g = $coef[$i]->[0];};
$const = $const * $g;
my @temp = ();
foreach my $j (0..$z) {
@temp = (@temp,($coef[$i]->[$j])/$g);
};
$coef[$i] = [ @temp ];
}
#A Formula reduction pushes constants to the left anyway, so we won't worry about all the places student could conceivable put this constant.
#Upgrade @coef so that each entry is a Formula with the proper
#power of x attached. Remove all empty slots, like 0*x.
@terms =([]);
foreach my $i (0..$n) {
my @temp = ();
my $temp2 = @{$coef[$i]}-1;
foreach my $j (0..$temp2) {
my $c = $coef[$i]->[$j];
my $d = $temp2-$j;
if ($c != 0) {
@temp = (@temp, Formula("$c x^{$d}")->reduce);
};
};
$terms[$i] = [ @temp ];
};
#Make the array of acceptable alternative factorizations.
#First, make a two-dimensional array indexed by the original factors,
#then by order of terms within that factor. If the factorization includes a
#trinomial, that row should be handled differently.
@altF = ([]);
foreach my $i (0..$n) {
my $t = @{$terms[$i]};
if ($t == 1) { #for monomial factors
$altF[$i] = [ ( $terms[$i]->[0] ) ];
};
if ($t == 2) { #for binomial factors
my @tmp = ();
foreach my $j (A,B) {
@tmp = (@tmp, ($terms[$i]->[$S2{$j}{0}]
+ $terms[$i]->[$S2{$j}{1}])->reduce );
};
$altF[$i] = [ @tmp ];
};
if ($t == 3) { #for trinomial factors
my @tmp = ();
foreach my $j (A..F) {
@tmp = (@tmp, Compute($terms[$i]->[$S3{$j}{0}] + $terms[$i]->[$S3{$j}{1}]
+ $terms[$i]->[$S3{$j}{2}])->reduce );
};
$altF[$i] = [ @tmp ];
};
};
#Make a one-dimensional array with all legal alternative factorizations.
#We will make use of a two-dimensional array of indices
$t0 = @{$altF[0]}-1;
$t1 = @{$altF[1]}-1;
$t2 = @{$altF[2]}-1;
@indices = ([0..$t0],[0..$t1],[0..$t2],);
@altFtions = ();
foreach my $k (A..F) {
foreach my $m0 (0..$t0) {
foreach my $m1 (0..$t1) {
foreach my $m2 (0..$t2) {
@altFtions = (@altFtions, $const
*($altF[$S3{$k}{0}]->[$indices[$S3{$k}{0}]->[$m0]])
*($altF[$S3{$k}{1}]->[$indices[$S3{$k}{1}]->[$m1]])
*($altF[$S3{$k}{2}]->[$indices[$S3{$k}{2}]->[$m2]])
);
};
};
};
};
#Pick the version that is displayed as correct. Return the turned-off reductions.
Context()->reduction->set('(-x)-y'=>1, '(-x)+y'=>1);
$factored = Compute("$altFtions[0]")->reduce;
##############################################
Context()->texStrings;
BEGIN_TEXT
Factor this polynomial.
$PAR
$SPACE $SPACE $SPACE \( $polynomial \)
$PAR
\{ans_rule(30)\}
END_TEXT
Context()->normalStrings;
##############################################
ANS($factored->cmp(typeMatch=>Formula("x"), checker => sub {
my ($correct,$student,$ans) = @_;
my $ret = 0;
my $l = 0;
while (($ret != 1) && ($l <= $#altFtions)) {
if ($student->reduce eq $altFtions[$l]->reduce) {$ret = 1;};
$l = $l + 1;
}
Value->Error("You haven't factored completely.")
if (($ret == 0) && ($student == $correct));
Value->Error("Your factorization is incorrect.")
if ($student != $correct);
return $ret
}));
ENDDOCUMENT();
That's an interesting observation about the distributive law. For polynomial factoring, there is a new way (as of spring 2010) that is much easier and much better.
http://webwork.maa.org/wiki/FactoredPolynomial1
http://webwork.maa.org/wiki/FactoringAndExpanding
I've spent a considerable amount of time the past year updating the wiki pages:
http://webwork.maa.org/wiki/SubjectAreaTemplates
http://webwork.maa.org/wiki/IndexOfProblemTechniques
If there are things you (and everyone reading this message) would like to be able to do that are not addressed by the wiki, please post your questions to this forum. Who knows? Maybe someone else has already figured out a good solution to your problem. Maybe I've figured it out but didn't put anything up on the wiki about it. Etc.
Best Regards,
Paul Pearson
Thanks for those links. I was wondering where a directory was for all those examples. I've hit a couple of them through Google, but I never knew where the directory was. (Now of course I see that there's a link at the top of each example.)
And thanks for the work too. That catalog will be a great resource. I've bookmarked it!
Alex