mathsteps
Version:
Step by step math solutions
362 lines (310 loc) • 13.1 kB
JavaScript
const math = require('mathjs');
const ConstantFactors = require('./ConstantFactors');
const ChangeTypes = require('../ChangeTypes');
const evaluate = require('../util/evaluate');
const Negative = require('../Negative');
const Node = require('../node');
const FACTOR_FUNCTIONS = [
// factor just the symbol e.g. x^2 + 2x -> x(x + 2)
factorSymbol,
// factor difference of squares e.g. x^2 - 4
factorDifferenceOfSquares,
// factor perfect square e.g. x^2 + 2x + 1
factorPerfectSquare,
// factor sum product rule e.g. x^2 + 3x + 2
factorSumProductRule
];
// Given a node, will check if it's in the form of a quadratic equation
// `ax^2 + bx + c`, and
// if it is, will factor it using one of the following rules:
// - Factor out the symbol e.g. x^2 + 2x -> x(x + 2)
// - Difference of squares e.g. x^2 - 4 -> (x+2)(x-2)
// - Perfect square e.g. x^2 + 2x + 1 -> (x+1)^2
// - Sum/product rule e.g. x^2 + 3x + 2 -> (x+1)(x+2)
// - TODO: quadratic formula
// requires us simplify the following only within the parens:
// a(x - (-b + sqrt(b^2 - 4ac)) / 2a)(x - (-b - sqrt(b^2 - 4ac)) / 2a)
function factorQuadratic(node) {
// get a, b and c
let symbol, aValue = 0, bValue = 0, cValue = 0;
for (const term of node.args) {
if (Node.Type.isConstant(term)) {
cValue = evaluate(term);
}
else if (Node.PolynomialTerm.isPolynomialTerm(term)) {
const polyTerm = new Node.PolynomialTerm(term);
const exponent = polyTerm.getExponentNode(true);
if (exponent.value === '2') {
symbol = polyTerm.getSymbolNode();
aValue = polyTerm.getCoeffValue();
}
else if (exponent.value === '1') {
bValue = polyTerm.getCoeffValue();
}
else {
return Node.Status.noChange(node);
}
}
else {
return Node.Status.noChange(node);
}
}
if (!symbol || !aValue) {
return Node.Status.noChange(node);
}
let negate = false;
if (aValue < 0) {
negate = true;
aValue = -aValue;
bValue = -bValue;
cValue = -cValue;
}
for (let i = 0; i < FACTOR_FUNCTIONS.length; i++) {
const nodeStatus = FACTOR_FUNCTIONS[i](node, symbol, aValue, bValue, cValue, negate);
if (nodeStatus.hasChanged()) {
return nodeStatus;
}
}
return Node.Status.noChange(node);
}
// Will factor the node if it's in the form of ax^2 + bx
function factorSymbol(node, symbol, aValue, bValue, cValue, negate) {
if (!bValue || cValue) {
return Node.Status.noChange(node);
}
const gcd = math.gcd(aValue, bValue);
const gcdNode = Node.Creator.constant(gcd);
const aNode = Node.Creator.constant(aValue/gcd);
const bNode = Node.Creator.constant(bValue/gcd);
const factoredNode = Node.Creator.polynomialTerm(symbol, null, gcdNode);
const polyTerm = Node.Creator.polynomialTerm(symbol, null, aNode);
const paren = Node.Creator.parenthesis(
Node.Creator.operator('+', [polyTerm, bNode]));
let newNode = Node.Creator.operator('*', [factoredNode, paren], true);
if (negate) {
newNode = Negative.negate(newNode);
}
return Node.Status.nodeChanged(ChangeTypes.FACTOR_SYMBOL, node, newNode);
}
// Will factor the node if it's in the form of ax^2 - c, and the aValue
// and cValue are perfect squares
// e.g. 4x^2 - 4 -> (2x + 2)(2x - 2)
function factorDifferenceOfSquares(node, symbol, aValue, bValue, cValue, negate) {
// check if difference of squares:
// (i) abs(a) and abs(c) are squares,
// (ii) b = 0,
// (iii) c is negative
if (bValue || !cValue) {
return Node.Status.noChange(node);
}
// we factor out the gcd first, providing us with a modified expression to
// factor with new a and c values
const gcd = math.gcd(aValue, cValue);
aValue = aValue/gcd;
cValue = cValue/gcd;
const aRootValue = Math.sqrt(Math.abs(aValue));
const cRootValue = Math.sqrt(Math.abs(cValue));
// must be a difference of squares
if (Number.isInteger(aRootValue) &&
Number.isInteger(cRootValue) &&
cValue < 0) {
const aRootNode = Node.Creator.constant(aRootValue);
const cRootNode = Node.Creator.constant(cRootValue);
const polyTerm = Node.Creator.polynomialTerm(symbol, null, aRootNode);
const firstParen = Node.Creator.parenthesis(
Node.Creator.operator('+', [polyTerm, cRootNode]));
const secondParen = Node.Creator.parenthesis(
Node.Creator.operator('-', [polyTerm, cRootNode]));
// create node in difference of squares form
let newNode = Node.Creator.operator('*', [firstParen, secondParen], true);
if (gcd !== 1) {
const gcdNode = Node.Creator.constant(gcd);
newNode = Node.Creator.operator('*', [gcdNode, newNode], true);
}
if (negate) {
newNode = Negative.negate(newNode);
}
return Node.Status.nodeChanged(
ChangeTypes.FACTOR_DIFFERENCE_OF_SQUARES, node, newNode);
}
return Node.Status.noChange(node);
}
// Will factor the node if it's in the form of ax^2 + bx + c, where a and c
// are perfect squares and b = 2*sqrt(a)*sqrt(c)
// e.g. x^2 + 2x + 1 -> (x + 1)^2
function factorPerfectSquare(node, symbol, aValue, bValue, cValue, negate) {
// check if perfect square: (i) a and c squares, (ii) b = 2*sqrt(a)*sqrt(c)
if (!bValue || !cValue) {
return Node.Status.noChange(node);
}
// we factor out the gcd first, providing us with a modified expression to
// factor with new a and c values
const gcd = math.gcd(aValue, bValue, cValue);
aValue = aValue/gcd;
cValue = cValue/gcd;
const aRootValue = Math.sqrt(Math.abs(aValue));
let cRootValue = Math.sqrt(Math.abs(cValue));
// if the second term is negative, then the constant in the parens is
// subtracted: e.g. x^2 - 2x + 1 -> (x - 1)^2
if (bValue < 0) {
cRootValue = cRootValue * -1;
}
// apply the perfect square test
const perfectProduct = 2 * aRootValue * cRootValue;
if (Number.isInteger(aRootValue) &&
Number.isInteger(cRootValue) &&
(bValue/gcd) === perfectProduct) {
const aRootNode = Node.Creator.constant(aRootValue);
const cRootNode = Node.Creator.constant(cRootValue);
const polyTerm = Node.Creator.polynomialTerm(symbol, null, aRootNode);
const paren = Node.Creator.parenthesis(
Node.Creator.operator('+', [polyTerm, cRootNode]));
const exponent = Node.Creator.constant(2);
// create node in perfect square form
let newNode = Node.Creator.operator('^', [paren, exponent]);
if (gcd !== 1) {
const gcdNode = Node.Creator.constant(gcd);
newNode = Node.Creator.operator('*', [gcdNode, newNode], true);
}
if (negate) {
newNode = Negative.negate(newNode);
}
return Node.Status.nodeChanged(
ChangeTypes.FACTOR_PERFECT_SQUARE, node, newNode);
}
return Node.Status.noChange(node);
}
// Will factor the node if it's in the form of ax^2 + bx + c, by
// applying the sum product rule: finding factors of a*c that add up to b.
// 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
const gcd = math.gcd(aValue, bValue, cValue);
const gcdNode = Node.Creator.constant(gcd);
aValue = aValue/gcd;
bValue = bValue/gcd;
cValue = cValue/gcd;
// try sum/product rule: find a factor pair of a*c that adds up to b
const product = aValue * cValue;
const factorPairs = ConstantFactors.getFactorPairs(product, true);
for (const pair of factorPairs) {
if (pair[0] + pair[1] === bValue) {
// To factor, we go through some transformations
// 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);
// 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]));
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 secondParen = Node.Creator.parenthesis(
Node.Creator.operator('+', [ux, v]));
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);
}
else {
newNode = Node.Creator.operator(
'*', [gcdNode, firstParen, secondParen], true);
}
if (negate) {
newNode = Negative.negate(newNode);
}
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);
}
}
}
return Node.Status.noChange(node);
}
module.exports = factorQuadratic;