Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

#163 - Better sum-product factoring steps #210

Merged
merged 10 commits into from
Aug 9, 2017
4 changes: 3 additions & 1 deletion lib/ChangeTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ module.exports = {
// e.g. 2 - - 3 -> 2 + 3
RESOLVE_DOUBLE_MINUS: 'RESOLVE_DOUBLE_MINUS',

// COLLECT AND COMBINE
// COLLECT AND COMBINE AND BREAK UP

// e.g. 2 + x + 3 + x -> 5 + 2x
COLLECT_AND_COMBINE_LIKE_TERMS: 'COLLECT_AND_COMBINE_LIKE_TERMS',
Expand Down Expand Up @@ -185,4 +185,6 @@ module.exports = {
FACTOR_PERFECT_SQUARE: 'FACTOR_PERFECT_SQUARE',
// e.g. x^2 + 3x + 2 -> (x + 1)(x + 2)
FACTOR_SUM_PRODUCT_RULE: 'FACTOR_SUM_PRODUCT_RULE',
// e.g. 2x^2 + 4x + 2 -> 2x^2 + 2x + 2x + 2
BREAK_UP_TERM: 'BREAK_UP_TERM',
};
94 changes: 83 additions & 11 deletions lib/factor/factorQuadratic.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ function factorPerfectSquare(node, symbol, aValue, bValue, cValue, negate) {
// e.g. x^2 + 3x + 2 -> (x + 1)(x + 2) or
// or 2x^2 + 5x + 3 -> (2x - 1)(x + 3)
function factorSumProductRule(node, symbol, aValue, bValue, cValue, negate) {
let newNode;

if (bValue && cValue) {
// we factor out the gcd first, providing us with a modified expression to
// factor with new a, b and c values
Expand All @@ -225,45 +227,110 @@ function factorSumProductRule(node, symbol, aValue, bValue, cValue, negate) {
for (const pair of factorPairs) {
if (pair[0] + pair[1] === bValue) {
// To factor, we go through some transformations
// TODO: these should be actual substeps
// 1. Break apart the middle term into two terms using our factor pair (p and q):
// e.g. ax^2 + bx + c -> ax^2 + px + qx + c
// 1. Break apart the middle term into two terms using our factor pair
// (p and q): e.g. ax^2 + bx + c -> ax^2 + px + qx + c
// 2. Consider the first two terms together and the second two terms
// together (this doesn't require any actual change to the expression)
// e.g. first group: [ax^2 + px] and second group: [qx + c]
// 3. Factor both groups separately
// e.g first group: [ux(rx + s)] and second group [v(rx + s)]
// 4. Finish factoring by combining the factored terms through grouping:
// e.g. (ux + v)(rx + s)
const substeps = [];
let status;

const a = Node.Creator.constant(aValue);
const b = Node.Creator.constant(bValue);
const c = Node.Creator.constant(cValue);
const ax2 = Node.Creator.polynomialTerm(symbol, Node.Creator.constant(2), a);
const bx = Node.Creator.polynomialTerm(symbol, null, b);

// OPTIONAL SUBSTEP (this happens iff a is negative)
// ax^2 + bx + c -> -(-ax^2 - bx - c)
if (negate) {
newNode = Node.Creator.operator('+', [ax2, bx, c], true);
newNode = Negative.negate(newNode);
status = Node.Status.nodeChanged(
ChangeTypes.REARRANGE_COEFF, node, newNode);
substeps.push(status);
newNode = Node.Status.resetChangeGroups(status.newNode);
}

// SUBSTEP 1: ax^2 + bx + c -> ax^2 + px + qx + c
const pValue = pair[0];
const qValue = pair[1];
const p = Node.Creator.constant(pValue);
const q = Node.Creator.constant(qValue);
const px = Node.Creator.polynomialTerm(symbol, null, p);
const qx = Node.Creator.polynomialTerm(symbol, null, q);

newNode = Node.Creator.operator('+', [ax2, px, qx, c], true);
if (negate) {
newNode = Negative.negate(newNode);
}
status = Node.Status.nodeChanged(
ChangeTypes.BREAK_UP_TERM, node, newNode);
substeps.push(status);
newNode = Node.Status.resetChangeGroups(status.newNode);

// STEP 2: ax^2 + px + qx + c -> (ax^2 + px) + (qx + c)
const firstTerm = Node.Creator.parenthesis(
Node.Creator.operator('+', [ax2, px]));
const secondTerm = Node.Creator.parenthesis(
Node.Creator.operator('+', [qx, c]));

newNode = Node.Creator.operator('+', [firstTerm, secondTerm], true);
if (negate) {
newNode = Negative.negate(newNode);
}
status = Node.Status.nodeChanged(
ChangeTypes.COLLECT_LIKE_TERMS, node, newNode);
substeps.push(status);
newNode = Node.Status.resetChangeGroups(status.newNode);

// factor the first group to get u, r and s:
// u = gcd(a, p), r = a/u and s = p/u
// SUBSTEP 3A: (ax^2 + px) + (qx + c) -> ux(rx + s) + (qx + c)
const u = Node.Creator.constant(math.gcd(aValue, pValue));
const r = Node.Creator.constant(aValue/u);
const s = Node.Creator.constant(pValue/u);
const ux = Node.Creator.polynomialTerm(symbol, null, u);

// create the first group's part that's in parentheses: (rx + s)
const rx = Node.Creator.polynomialTerm(symbol, null, r);
const firstParen = Node.Creator.parenthesis(
Node.Creator.operator('+', [rx, s]));

// factor the second group to get v, we don't need to find r and s again
// v = gcd(c, q)
const firstFactoredGroup = Node.Creator.operator('*', [ux, firstParen], true);
newNode = Node.Creator.operator('+', [firstFactoredGroup, secondTerm], true);
if (negate) {
newNode = Negative.negate(newNode);
}
status = Node.Status.nodeChanged(
ChangeTypes.FACTOR_SYMBOL, node, newNode);
substeps.push(status);
newNode = Node.Status.resetChangeGroups(status.newNode);

// STEP 3B: ux(rx + s) + (qx + c) -> ux(rx + s) + v(rx + s)
let vValue = math.gcd(cValue, qValue);
if (qValue < 0) {
vValue = vValue * -1;
}
const v = Node.Creator.constant(vValue);

// create the second parenthesis
const ux = Node.Creator.polynomialTerm(symbol, null, u);
const secondParen = Node.Creator.parenthesis(
Node.Creator.operator('+', [ux, v]));

// create a node in the general factored form for expression
let newNode;
const secondFactoredGroup = Node.Creator.operator('*', [v, firstParen], true);
newNode = Node.Creator.operator('+', [firstFactoredGroup, secondFactoredGroup], true);
if (negate) {
newNode = Negative.negate(newNode);
}
status = Node.Status.nodeChanged(
ChangeTypes.FACTOR_SYMBOL, node, newNode);
substeps.push(status);
newNode = Node.Status.resetChangeGroups(status.newNode);

// STEP 4: ux(rx + s) + v(rx + s) -> (ux + v)(rx + s)
if (gcd === 1) {
newNode = Node.Creator.operator(
'*', [firstParen, secondParen], true);
Expand All @@ -277,8 +344,13 @@ function factorSumProductRule(node, symbol, aValue, bValue, cValue, negate) {
newNode = Negative.negate(newNode);
}

return Node.Status.nodeChanged(
status = Node.Status.nodeChanged(
ChangeTypes.FACTOR_SUM_PRODUCT_RULE, node, newNode);
substeps.push(status);
newNode = Node.Status.resetChangeGroups(status.newNode);

return Node.Status.nodeChanged(
ChangeTypes.FACTOR_SUM_PRODUCT_RULE, node, newNode, true, substeps);
}
}
}
Expand Down
62 changes: 62 additions & 0 deletions test/factor/factorQuadratic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,65 @@ describe('factorQuadratic', function () {
];
tests.forEach(t => testFactorQuadratic(t[0], t[1]));
});

function testFactorSumProductRuleSubsteps(exprString, outputList) {
const lastString = outputList[outputList.length - 1];
TestUtil.testSubsteps(factorQuadratic, exprString, outputList, lastString);
}

describe('factorSumProductRule', function() {
const tests = [
// sum product rule
['x^2 + 3x + 2',
['x^2 + x + 2x + 2',
'(x^2 + x) + (2x + 2)',
'x * (x + 1) + (2x + 2)',
'x * (x + 1) + 2 * (x + 1)',
'(x + 1) * (x + 2)']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eee this is awesome look at this sum product rule awesomeness

],
['x^2 - 3x + 2',
['x^2 - x - 2x + 2',
'(x^2 - x) + (-2x + 2)',
'x * (x - 1) + (-2x + 2)',
'x * (x - 1) - 2 * (x - 1)',
'(x - 1) * (x - 2)']
],
['x^2 + x - 2',
['x^2 - x + 2x - 2',
'(x^2 - x) + (2x - 2)',
'x * (x - 1) + (2x - 2)',
'x * (x - 1) + 2 * (x - 1)',
'(x - 1) * (x + 2)']
],
['-x^2 - 3x - 2',
['-(x^2 + 3x + 2)',
'-(x^2 + x + 2x + 2)',
'-((x^2 + x) + (2x + 2))',
'-(x * (x + 1) + (2x + 2))',
'-(x * (x + 1) + 2 * (x + 1))',
'-(x + 1) * (x + 2)']
],
['2x^2 + 5x + 3',
['2x^2 + 2x + 3x + 3',
'(2x^2 + 2x) + (3x + 3)',
'2x * (x + 1) + (3x + 3)',
'2x * (x + 1) + 3 * (x + 1)',
'(x + 1) * (2x + 3)']
],
['2x^2 - 5x - 3',
['2x^2 + x - 6x - 3',
'(2x^2 + x) + (-6x - 3)',
'x * (2x + 1) + (-6x - 3)',
'x * (2x + 1) - 3 * (2x + 1)',
'(2x + 1) * (x - 3)']
],
['2x^2 - 5x + 3',
['2x^2 - 2x - 3x + 3',
'(2x^2 - 2x) + (-3x + 3)',
'2x * (x - 1) + (-3x + 3)',
'2x * (x - 1) - 3 * (x - 1)',
'(x - 1) * (2x - 3)']
],
];
tests.forEach(t => testFactorSumProductRuleSubsteps(t[0], t[1]));
});