Factoring Trinomials and Difference of Squares#41
Factoring Trinomials and Difference of Squares#41aliang8 wants to merge 90 commits intosemantic-math:masterfrom
Conversation
lib/rules.js
Outdated
| // assume monic polynomial | ||
| // TODO: add min and max to evaluate | ||
|
|
||
| export const FACTOR_SYMBOL = defineRuleString('#a^#b_0 + ...', '#a^#eval(min(#b_0, ...)) (#a^#eval(#b_0 - min(#b_0, ...)) + ...)', {b: query.isNumber}) |
There was a problem hiding this comment.
Neato! Could you provide an example what this does?
There was a problem hiding this comment.
Ahhh, this doesn't exactly work yet. This rule only applies to addition (x^2 + x^3 + x^4 -> x^2(x^0 + x^1 + x^2). Not exactly sure if we want two separate rules for addition and subtraction or if there is a way to do both in a rule string. I also need the min and max functions to be exposed in math-evaluator.
There was a problem hiding this comment.
I think this should work with subtraction as is. Have you tried writing a test case with subtraction?
There was a problem hiding this comment.
Yeah, I need min and max to support nary parameters.
lib/rules.js
Outdated
| (node) => { | ||
| const isFactorable = canApplyRule(FACTOR_SUM_PRODUCT_RULE, node) | ||
| if (isFactorable) { | ||
| const result = applyRule(FACTOR_SUM_PRODUCT_RULE, node) |
lib/rules.js
Outdated
| // e.g nthRoot(9, 2) -> 3 | ||
| export const NTH_ROOT_VALUE = defineRuleString('nthRoot(#a, #b)', '#eval(nthRoot(#a, #b))', {a: query.isNumber, b: query.isNumber}) | ||
|
|
||
| // FACTOR |
There was a problem hiding this comment.
Can you put these in their own file? I'd like eventually rename this file to be simple-rules.js and have it contain only rules which can be full expressed with patterns.
lib/rules.js
Outdated
| let isFactorable = false | ||
| const {constants, coefficientMap} = getCoefficientsAndConstants(node) | ||
| const variables = Object.keys(coefficientMap).map(key => parse(key)) | ||
| const coefficients = Object.keys(coefficientMap).map(key => {return coefficientMap[key]}) |
There was a problem hiding this comment.
const coeffs = Object.keys(coeffMap).map(key => coeffMap[key][0])
This keeps it to a single line and you don't have to do coeffs[1][0] later you can just do coeffs[1].
lib/rules.js
Outdated
| const isTrinomial = | ||
| isPolynomialTerm(firstTerm) | ||
| && isPolynomialTerm(secondTerm) | ||
| && (query.isNumber(thirdTerm) || isPolynomialTerm(thirdTerm)) |
There was a problem hiding this comment.
We should probably handle things like 1 + 2x + x^2 as well... maybe we should have a transform to order terms in a polynomial in descending order. This can be a TODO.
There was a problem hiding this comment.
Yup I was thinking to go with the second approach.
lib/rules.js
Outdated
| (node) => { | ||
| const {constants, coefficientMap} = getCoefficientsAndConstants(node) | ||
| const variables = Object.keys(coefficientMap).map(key => parse(key)) | ||
| const coefficients = Object.keys(coefficientMap).map(key => {return coefficientMap[key]}) |
lib/rules.js
Outdated
| for (var i in firstFactors){ | ||
| for (var j in thirdFactors) { | ||
| const l1 = firstFactors[i] | ||
| const l2 = thirdFactors[j] |
There was a problem hiding this comment.
I have no idea what's going on here. Can you add a comment explaining this algorithm?
lib/rules.js
Outdated
| build.apply( | ||
| 'mul', | ||
| [build.number(factor[0]), ...firstPoly.map( | ||
| term => |
lib/rules.js
Outdated
| } | ||
|
|
||
| const result = build.applyNode | ||
| ('mul', |
There was a problem hiding this comment.
All of the these implicit muls can be build using build.implicitMul.
lib/rules.js
Outdated
| } | ||
| if (combo == []) { | ||
| throw new Error('cannot factor polynomial') | ||
| } |
There was a problem hiding this comment.
If feels weird that canApplyRule might be successful, but applyRule might still fail.
There was a problem hiding this comment.
Good point, I have to fix this.
There was a problem hiding this comment.
Not entirely sure how to without replicating the code for rewriteFn.
There was a problem hiding this comment.
I would extract a common function. Obviously it's not ideal to run the same code twice, but I'd rather maintain the invariant that if canApplyRule returns true that applyRule will succeed.
|
|
||
| const getExponent = (node) => { | ||
| if (query.isNumber(node)) { | ||
| return 0 |
There was a problem hiding this comment.
This seems like a weird value to return for a number. I would expect the exponent to be 1. Maybe there should be an if statement before calling getExponent to check if the node is a number.
There was a problem hiding this comment.
Oh, the getExponent function is to get the degree of x for a given polynomial term. So for instance, 6x^2 + 3x + 1, getExponent on 1 would return 0 because it is 1x^0. Maybe the function name itself is a bit misleading?
| } else if (query.isPow(node)) { | ||
| return query.getValue(node.args[1]) | ||
| } else if (query.isMul(node)){ | ||
| return getExponent(node.args[1]) |
There was a problem hiding this comment.
Shouldn't this be return Math.max(...node.args.map(getExponent))?
There was a problem hiding this comment.
It's okay to limit ourselves to single variables for now, but we should have a TODO somewhere.
| } | ||
|
|
||
| // e.g. 2 + 3x^2 + 3x - 4x^3 -> -4x^3 + 3x^2 + 3x + 2 | ||
| export const REARRANGE_TERMS = defineRule( |
There was a problem hiding this comment.
This doesn't take into account polynomials with multiple variables. Maybe add a TODO.
lib/factor-rules.js
Outdated
|
|
||
| 3. Cross multiply the factors and check if | ||
| the result matches one of the eight conditions | ||
| (the second factor shld also multiply to equal the thirdCoef) |
|
|
||
| Check: 1 * 2 + 6 * 1 ?= 7 so on and so forth | ||
| Once we reach 2 * 2 + 3 * 1 ?= 7, we have found a match. | ||
| (Note: 3 * 1 = 3 satisfying the second condition) |
There was a problem hiding this comment.
I'm confused about this note.... don't we want to check if the multiple the first candidate coefficients results in the first coefficient of the trinomial and multiplying the both of the second candidate numbers results in the constant of the trinomial. In this case 2 * 3 = 6 and 2 * 1 = 2 so it works out.
There was a problem hiding this comment.
You already know that 2 * 3 = 6 and 2 * 1 = 2. It comes from the fact that those are the factors of 6 and 2 respectively (pairs that we got from firstFactors and thirdFactors). The note here is just to make sure we don't have an erroneous combo / solution (this happened a few times in my test cases).
There was a problem hiding this comment.
I need to spend some more time with this to really understand what's going on. I have some of pieces but not everything. This is eventually going to get re-reviewed when it gets moved over to mathsteps so I'm okay with merging this.
There was a problem hiding this comment.
Awesome, let me know when you merge this, so I can update my other PRs.
lib/factor-rules.js
Outdated
| } | ||
| ) | ||
|
|
||
| const getFactors = (num) => { |
There was a problem hiding this comment.
This should maybe be called getFactorPairs or add a comment describing what it does. I was confused b/c I thought calling getFactors(12) would return [1, 2, 3, 4, 6, 12] but in fact it returns [[1, 12], [2, 6], [3, 4]].
lib/factor-rules.js
Outdated
| ? query.isMul(variables[2]) | ||
| ? variables[2].args | ||
| : [variables[2]] | ||
| : null |
There was a problem hiding this comment.
Can you rewrite this ternary as an if-else?
lib/factor-rules.js
Outdated
| query.getValue(coeffs[2]) | ||
| : constants[0] | ||
| ? query.getValue(constants[0]) | ||
| : null |
| At this point, we find that the correct combination is | ||
| : [2,3] and [1,2] -> (2x + 3)(1x + 2) | ||
| */ | ||
| const factor_trinomial_helper = (node) => { |
There was a problem hiding this comment.
Does this handle cases where the lead coefficient is negative?
There was a problem hiding this comment.
We assume that lead coefficient is positive because it should have been factored out previously by another rule.
lib/factor-rules.js
Outdated
| ? query.isMul(variables[2]) | ||
| ? variables[2].args | ||
| : [variables[2]] | ||
| : null |
There was a problem hiding this comment.
The indentation is a little off here. It should probably be:
const thirdPoly =
variables[2]
? query.isMul(variables[2])
? variables[2].args
: [variables[2]]
: null
even that is hard to parse... since the default is null maybe do:
let thirdPoly = null;
if (variables[2]) {
if (query.isMul(variables[2]) {
thirdPoly = variables[2].args
} else {
thirdPoly = [variables[2]]
}
}
lib/factor-rules.js
Outdated
| ? query.isMul(variables[2]) | ||
| ? variables[2].args | ||
| : [variables[2]] | ||
| : null |
There was a problem hiding this comment.
Please rewrite as if-else statements.
PERFECT_SQUARE_TRINOMIALandFACTOR_TRINOMIALlogic is somewhat redundant, not sure if we should remove it and just have one rule. Code should be refactored if possible.