diff --git a/lib/ChangeTypes.js b/lib/ChangeTypes.js index 640c137b..a02ccca1 100644 --- a/lib/ChangeTypes.js +++ b/lib/ChangeTypes.js @@ -3,114 +3,147 @@ module.exports = { NO_CHANGE: 'NO_CHANGE', - // e.g. |-3| -> 3 - ABSOLUTE_VALUE: 'ABSOLUTE_VALUE', + // ARITHMETIC + + // e.g. 2 + 2 -> 4 or 2 * 2 -> 4 + SIMPLIFY_ARITHMETIC: 'SIMPLIFY_ARITHMETIC', + + // BASICS + + // e.g. 2/-1 -> -2 + DIVISION_BY_NEGATIVE_ONE: 'DIVISION_BY_NEGATIVE_ONE', + // e.g. 2/1 -> 2 + DIVISION_BY_ONE: 'DIVISION_BY_ONE', + // e.g. x * 0 -> 0 + MULTIPLY_BY_ZERO: 'MULTIPLY_BY_ZERO', + // e.g. x * 2 -> 2x + REARRANGE_COEFF: 'REARRANGE_COEFF', + // e.g. x ^ 0 -> 1 + REDUCE_EXPONENT_BY_ZERO: 'REDUCE_EXPONENT_BY_ZERO', + // e.g. 0/1 -> 0 + REDUCE_ZERO_NUMERATOR: 'REDUCE_ZERO_NUMERATOR', + // e.g. 2 + 0 -> 2 + REMOVE_ADDING_ZERO: 'REMOVE_ADDING_ZERO', + // e.g. x ^ 1 -> x + REMOVE_EXPONENT_BY_ONE: 'REMOVE_EXPONENT_BY_ONE', + // e.g. 1 ^ x -> 1 + REMOVE_EXPONENT_BASE_ONE: 'REMOVE_EXPONENT_BASE_ONE', + // e.g. x * -1 -> -x + REMOVE_MULTIPLYING_BY_NEGATIVE_ONE: 'REMOVE_MULTIPLYING_BY_NEGATIVE_ONE', + // e.g. x * 1 -> x + REMOVE_MULTIPLYING_BY_ONE: 'REMOVE_MULTIPLYING_BY_ONE', + // e.g. 2 - - 3 -> 2 + 3 + RESOLVE_DOUBLE_MINUS: 'RESOLVE_DOUBLE_MINUS', + + // COLLECT AND COMBINE + + // e.g. 2 + x + 3 + x -> 5 + 2x + COLLECT_AND_COMBINE_LIKE_TERMS: 'COLLECT_AND_COMBINE_LIKE_TERMS', + // e.g. x + 2 + x^2 + x + 4 -> x^2 + (x + x) + (4 + 2) + COLLECT_LIKE_TERMS: 'COLLECT_LIKE_TERMS', + + // ADDING POLYNOMIALS + // e.g. 2x + x -> 2x + 1x ADD_COEFFICIENT_OF_ONE: 'ADD_COEFFICIENT_OF_ONE', - // e.g. x^2 * x -> x^2 * x^1 - ADD_EXPONENT_OF_ONE: 'ADD_EXPONENT_OF_ONE', - // e.g. 1/2 + 1/3 -> 5/6 - ADD_FRACTIONS: 'ADD_FRACTIONS', - // e.g. (1 + 2)/3 -> 3/3 - ADD_NUMERATORS: 'ADD_NUMERATORS', // e.g. x^2 + x^2 -> 2x^2 ADD_POLYNOMIAL_TERMS: 'ADD_POLYNOMIAL_TERMS', + // e.g. 2x^2 + 3x^2 + 5x^2 -> (2+3+5)x^2 + GROUP_COEFFICIENTS: 'GROUP_COEFFICIENTS', + // e.g. -x + 2x => -1*x + 2x + UNARY_MINUS_TO_NEGATIVE_ONE: 'UNARY_MINUS_TO_NEGATIVE_ONE', + + // MULTIPLYING POLYNOMIALS + + // e.g. x^2 * x -> x^2 * x^1 + ADD_EXPONENT_OF_ONE: 'ADD_EXPONENT_OF_ONE', + // e.g. x^2 * x^3 * x^1 -> x^(2 + 3 + 1) + COLLECT_EXPONENTS: 'COLLECT_EXPONENTS', + // e.g. 2x * 3x -> (2 * 3)(x * x) + MULTIPLY_COEFFICIENTS: 'MULTIPLY_COEFFICIENTS', + // e.g. 2x * x -> 2x ^ 2 + MULTIPLY_POLYNOMIAL_TERMS: 'MULTIPLY_POLYNOMIAL_TERMS', + + // FRACTIONS + // e.g. (x + 2)/2 -> x/2 + 2/2 BREAK_UP_FRACTION: 'BREAK_UP_FRACTION', - // e.g. nthRoot(x ^ 2, 4) -> nthRoot(x, 2) - CANCEL_EXPONENT: 'CANCEL_EXPONENT', - // e.g. nthRoot(x ^ 2, 2) -> x - CANCEL_EXPONENT_AND_ROOT: 'CANCEL_EXPONENT_AND_ROOT', // e.g. -2/-3 => 2/3 CANCEL_MINUSES: 'CANCEL_MINUSES', // e.g. 2x/2 -> x CANCEL_TERMS: 'CANCEL_TERMS', - // e.g. nthRoot(x ^ 4, 2) -> x ^ 2 - CANCEL_ROOT: 'CANCEL_ROOT', - // e.g. 2 + x + 3 + x -> 5 + 2x - COLLECT_AND_COMBINE_LIKE_TERMS: 'COLLECT_AND_COMBINE_LIKE_TERMS', - // e.g. x^2 * x^3 * x^1 -> x^(2 + 3 + 1) - COLLECT_EXPONENTS: 'COLLECT_EXPONENTS', - // e.g. x + 2 + x^2 + x + 4 -> x^2 + (x + x) + (4 + 2) - COLLECT_LIKE_TERMS: 'COLLECT_LIKE_TERMS', - // e.g. 2/6 + 1/4 -> (2*2)/(6*2) + (1*3)/(4*3) - COMMON_DENOMINATOR: 'COMMON_DENOMINATOR', + // e.g. 2/6 -> 1/3 + SIMPLIFY_FRACTION: 'SIMPLIFY_FRACTION', + // e.g. 2/-3 -> -2/3 + SIMPLIFY_SIGNS: 'SIMPLIFY_SIGNS', + + // ADDING FRACTIONS + + // e.g. 1/2 + 1/3 -> 5/6 + ADD_FRACTIONS: 'ADD_FRACTIONS', + // e.g. (1 + 2)/3 -> 3/3 + ADD_NUMERATORS: 'ADD_NUMERATORS', // e.g. (2+1)/5 COMBINE_NUMERATORS: 'COMBINE_NUMERATORS', - // e.g. nthRoot(2, 2) * nthRoot(3, 2) -> nthRoot(2 * 3, 2) - COMBINE_UNDER_ROOT: 'COMBINE_UNDER_ROOT', + // e.g. 2/6 + 1/4 -> (2*2)/(6*2) + (1*3)/(4*3) + COMMON_DENOMINATOR: 'COMMON_DENOMINATOR', // e.g. 3 + 1/2 -> 6/2 + 1/2 (for addition) CONVERT_INTEGER_TO_FRACTION: 'CONVERT_INTEGER_TO_FRACTION', - // e.g. 2 * 2 * 2 -> 2 ^ 3 - CONVERT_MULTIPLICATION_TO_EXPONENT: 'CONVERT_MULTIPLICATION_TO_EXPONENT', + // e.g. 1.2 + 1/2 -> 1.2 + 0.5 + DIVIDE_FRACTION_FOR_ADDITION: 'DIVIDE_FRACTION_FOR_ADDITION', + // e.g. (2*2)/(6*2) + (1*3)/(4*3) -> (2*2)/12 + (1*3)/12 + MULTIPLY_DENOMINATORS: 'MULTIPLY_DENOMINATORS', + // e.g. (2*2)/12 + (1*3)/12 -> 4/12 + 3/12 + MULTIPLY_NUMERATORS: 'MULTIPLY_NUMERATORS', + + // MULTIPLYING FRACTIONS + + // e.g. 1/2 * 2/3 -> 2/6 + MULTIPLY_FRACTIONS: 'MULTIPLY_FRACTIONS', + + // DIVISION + + // e.g. 2/3/4 -> 2/(3*4) + SIMPLIFY_DIVISION: 'SIMPLIFY_DIVISION', + // e.g. x/(2/3) -> x * 3/2 + MULTIPLY_BY_INVERSE: 'MULTIPLY_BY_INVERSE', + + // DISTRIBUTION + // e.g. 2(x + y) -> 2x + 2y DISTRIBUTE: 'DISTRIBUTE', // e.g. -(2 + x) -> -2 - x DISTRIBUTE_NEGATIVE_ONE: 'DISTRIBUTE_NEGATIVE_ONE', + // e.g. 2 * 4x + 2*5 --> 8x + 10 (as part of distribution) + SIMPLIFY_TERMS: 'SIMPLIFY_TERMS', + + // ABSOLUTE + // e.g. |-3| -> 3 + ABSOLUTE_VALUE: 'ABSOLUTE_VALUE', + + // ROOTS + // e.g. nthRoot(x ^ 2, 4) -> nthRoot(x, 2) + CANCEL_EXPONENT: 'CANCEL_EXPONENT', + // e.g. nthRoot(x ^ 2, 2) -> x + CANCEL_EXPONENT_AND_ROOT: 'CANCEL_EXPONENT_AND_ROOT', + // e.g. nthRoot(x ^ 4, 2) -> x ^ 2 + CANCEL_ROOT: 'CANCEL_ROOT', + // e.g. nthRoot(2, 2) * nthRoot(3, 2) -> nthRoot(2 * 3, 2) + COMBINE_UNDER_ROOT: 'COMBINE_UNDER_ROOT', + // e.g. 2 * 2 * 2 -> 2 ^ 3 + CONVERT_MULTIPLICATION_TO_EXPONENT: 'CONVERT_MULTIPLICATION_TO_EXPONENT', // e.g. nthRoot(2 * x) -> nthRoot(2) * nthRoot(x) DISTRIBUTE_NTH_ROOT: 'DISTRIBUTE_NTH_ROOT', - // e.g. 1.2 + 1/2 -> 1.2 + 0.5 - DIVIDE_FRACTION_FOR_ADDITION: 'DIVIDE_FRACTION_FOR_ADDITION', - // e.g. 2/-1 -> -2 - DIVISION_BY_NEGATIVE_ONE: 'DIVISION_BY_NEGATIVE_ONE', - // e.g. 2/1 -> 2 - DIVISION_BY_ONE: 'DIVISION_BY_ONE', // e.g. nthRoot(4) * nthRoot(x^2) -> 2 * x EVALUATE_DISTRIBUTED_NTH_ROOT: 'EVALUATE_DISTRIBUTED_NTH_ROOT', // e.g. 12 -> 2 * 2 * 3 FACTOR_INTO_PRIMES: 'FACTOR_INTO_PRIMES', - // e.g. 2x^2 + 3x^2 + 5x^2 -> (2+3+5)x^2 - GROUP_COEFFICIENTS: 'GROUP_COEFFICIENTS', // e.g. nthRoot(2 * 2 * 2, 2) -> nthRoot((2 * 2) * 2) GROUP_TERMS_BY_ROOT: 'GROUP_TERMS_BY_ROOT', - // e.g. x/(2/3) -> x * 3/2 - MULTIPLY_BY_INVERSE: 'MULTIPLY_BY_INVERSE', - // e.g. x * 0 -> 0 - MULTIPLY_BY_ZERO: 'MULTIPLY_BY_ZERO', - // e.g. 2x * 3x -> (2 * 3)(x * x) - MULTIPLY_COEFFICIENTS: 'MULTIPLY_COEFFICIENTS', - // e.g. 1/2 * 2/3 -> 2/6 - MULTIPLY_FRACTIONS: 'MULTIPLY_FRACTIONS', - // e.g. (2*2)/(6*2) + (1*3)/(4*3) -> (2*2)/12 + (1*3)/12 - MULTIPLY_DENOMINATORS: 'MULTIPLY_DENOMINATORS', - // e.g. (2*2)/12 + (1*3)/12 -> 4/12 + 3/12 - MULTIPLY_NUMERATORS: 'MULTIPLY_NUMERATORS', - // e.g. 2x * x -> 2x ^ 2 - MULTIPLY_POLYNOMIAL_TERMS: 'MULTIPLY_POLYNOMIAL_TERMS', // e.g. nthRoot(4) -> 2 NTH_ROOT_VALUE: 'NTH_ROOT_VALUE', - // e.g. x * 2 -> 2x - REARRANGE_COEFF: 'REARRANGE_COEFF', - // e.g. 2 + 0 -> 2 - REMOVE_ADDING_ZERO: 'REMOVE_ADDING_ZERO', - // e.g. x ^ 1 -> x - REMOVE_EXPONENT_BY_ONE: 'REMOVE_EXPONENT_BY_ONE', - // e.g. 1 ^ x -> 1 - REMOVE_EXPONENT_BASE_ONE: 'REMOVE_EXPONENT_BASE_ONE', - // e.g. x * -1 -> -x - REMOVE_MULTIPLYING_BY_NEGATIVE_ONE: 'REMOVE_MULTIPLYING_BY_NEGATIVE_ONE', - // e.g. x * 1 -> x - REMOVE_MULTIPLYING_BY_ONE: 'REMOVE_MULTIPLYING_BY_ONE', - // e.g. x ^ 0 -> 1 - REDUCE_EXPONENT_BY_ZERO: 'REDUCE_EXPONENT_BY_ZERO', - // e.g. 0/1 -> 0 - REDUCE_ZERO_NUMERATOR: 'REDUCE_ZERO_NUMERATOR', - // e.g. 2 - - 3 -> 2 + 3 - RESOLVE_DOUBLE_MINUS: 'RESOLVE_DOUBLE_MINUS', - // e.g. 2 + 2 -> 4 or 2 * 2 -> 4 - SIMPLIFY_ARITHMETIC: 'SIMPLIFY_ARITHMETIC', - // e.g. 2/3/4 -> 2/(3*4) - SIMPLIFY_DIVISION: 'SIMPLIFY_DIVISION', - // e.g. 2/6 -> 1/3 - SIMPLIFY_FRACTION: 'SIMPLIFY_FRACTION', - // e.g. 2/-3 -> -2/3 - SIMPLIFY_SIGNS: 'SIMPLIFY_SIGNS', - // e.g. 2 * 4x + 2*5 --> 8x + 10 (as part of distribution) - SIMPLIFY_TERMS: 'SIMPLIFY_TERMS', - // e.g. -x + 2x => -1*x + 2x - UNARY_MINUS_TO_NEGATIVE_ONE: 'UNARY_MINUS_TO_NEGATIVE_ONE', - // Expression change types: + // SOLVING FOR A VARIABLE // e.g. x - 3 = 2 -> x - 3 + 3 = 2 + 3 ADD_TO_BOTH_SIDES: 'ADD_TO_BOTH_SIDES', @@ -131,8 +164,21 @@ module.exports = { // e.g. 2 = x -> x = 2 SWAP_SIDES: 'SWAP_SIDES', + // CONSTANT EQUATION + // e.g. 2 = 2 STATEMENT_IS_TRUE: 'STATEMENT_IS_TRUE', // e.g. 2 = 3 STATEMENT_IS_FALSE: 'STATEMENT_IS_FALSE', + + // FACTORING + + // e.g. x^2 - 4x -> x(x - 4) + FACTOR_SYMBOL: 'FACTOR_SYMBOL', + // e.g. x^2 - 4 -> (x - 2)(x + 2) + FACTOR_DIFFERENCE_OF_SQUARES: 'FACTOR_DIFFERENCE_OF_SQUARES', + // e.g. x^2 + 2x + 1 -> (x + 1)^2 + FACTOR_PERFECT_SQUARE: 'FACTOR_PERFECT_SQUARE', + // e.g. x^2 + 3x + 2 -> (x + 1)(x + 2) + FACTOR_SUM_PRODUCT_RULE: 'FACTOR_SUM_PRODUCT_RULE', }; diff --git a/lib/checks/index.js b/lib/checks/index.js index 002d92cc..5bb07252 100644 --- a/lib/checks/index.js +++ b/lib/checks/index.js @@ -3,6 +3,7 @@ const canMultiplyLikeTermPolynomialNodes = require('./canMultiplyLikeTermPolynom const canRearrangeCoefficient = require('./canRearrangeCoefficient'); const canSimplifyPolynomialTerms = require('./canSimplifyPolynomialTerms'); const hasUnsupportedNodes = require('./hasUnsupportedNodes'); +const isQuadratic = require('./isQuadratic'); const resolvesToConstant = require('./resolvesToConstant'); module.exports = { @@ -11,5 +12,6 @@ module.exports = { canRearrangeCoefficient, canSimplifyPolynomialTerms, hasUnsupportedNodes, + isQuadratic, resolvesToConstant, }; diff --git a/lib/checks/isQuadratic.js b/lib/checks/isQuadratic.js new file mode 100644 index 00000000..f23ec7a7 --- /dev/null +++ b/lib/checks/isQuadratic.js @@ -0,0 +1,54 @@ +const Node = require('../node'); +const Symbols = require('../Symbols'); + +// Given a node, will determine if the expression is in the form of a quadratic +// e.g. `x^2 + 2x + 1` OR `x^2 - 1` but not `x^3 + x^2 + x + 1` +function isQuadratic(node) { + if (!Node.Type.isOperator(node, '+')) { + return false; + } + + if (node.args.length > 3) { + return false; + } + + // make sure only one symbol appears in the expression + const symbolSet = Symbols.getSymbolsInExpression(node); + if (symbolSet.size !== 1) { + return false; + } + + const secondDegreeTerms = node.args.filter(isPolynomialTermOfDegree(2)); + const firstDegreeTerms = node.args.filter(isPolynomialTermOfDegree(1)); + const constantTerms = node.args.filter(Node.Type.isConstant); + + // Check that there is one second degree term and at most one first degree + // term and at most one constant term + if (secondDegreeTerms.length !== 1 || firstDegreeTerms.length > 1 || + constantTerms.length > 1) { + return false; + } + + // check that there are no terms that don't fall into these groups + if ((secondDegreeTerms.length + firstDegreeTerms.length + + constantTerms.length) !== node.args.length) { + return false; + } + + return true; +} + +// Given a degree, returns a function that checks if a node +// is a polynomial term of the given degree. +function isPolynomialTermOfDegree(degree) { + return function(node) { + if (Node.PolynomialTerm.isPolynomialTerm(node)) { + const polyTerm = new Node.PolynomialTerm(node); + const exponent = polyTerm.getExponentNode(true); + return exponent && parseFloat(exponent.value) === degree; + } + return false; + }; +} + +module.exports = isQuadratic; diff --git a/lib/factor/ConstantFactors.js b/lib/factor/ConstantFactors.js index b07f5c73..ff72f287 100644 --- a/lib/factor/ConstantFactors.js +++ b/lib/factor/ConstantFactors.js @@ -41,8 +41,11 @@ ConstantFactors.getPrimeFactors = function(number){ ConstantFactors.getFactorPairs = function(number){ const factors = []; - const root = Math.sqrt(number); - for (var divisor = 1; divisor <= root; divisor++) { + const bound = Math.floor(Math.sqrt(Math.abs(number))); + for (var divisor = -bound; divisor <= bound; divisor++) { + if (divisor === 0) { + continue; + } if (number % divisor === 0) { const quotient = number / divisor; factors.push([divisor, quotient]); diff --git a/lib/factor/factorQuadratic.js b/lib/factor/factorQuadratic.js new file mode 100644 index 00000000..7404221b --- /dev/null +++ b/lib/factor/factorQuadratic.js @@ -0,0 +1,222 @@ +const ChangeTypes = require('../ChangeTypes'); +const checks = require('../checks'); +const ConstantFactors = require('./ConstantFactors'); +const flatten = require('../../lib/util/flattenOperands'); +const math = require('mathjs'); +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) { + node = flatten(node); + if (!checks.isQuadratic(node)) { + return Node.Status.noChange(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 = term.eval(); + } + 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); + } + + 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 (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); + } + + 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 === 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 (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 x^2 + bx + c (i.e. a is 1), by +// applying the sum product rule: finding factors of c that add up to b. +// e.g. x^2 + 3x + 2 -> (x + 1)(x + 2) +function factorSumProductRule(node, symbol, aValue, bValue, cValue, negate) { + if (aValue === 1 && bValue && cValue) { + // try sum/product rule: find a factor pair of c that adds up to b + const factorPairs = ConstantFactors.getFactorPairs(cValue, true); + for (const pair of factorPairs) { + if (pair[0] + pair[1] === bValue) { + const firstParen = Node.Creator.parenthesis( + Node.Creator.operator('+', [symbol, Node.Creator.constant(pair[0])])); + const secondParen = Node.Creator.parenthesis( + Node.Creator.operator('+', [symbol, Node.Creator.constant(pair[1])])); + + // create a node in the general factored form for expression + let newNode = Node.Creator.operator('*', [firstParen, secondParen], true); + if (negate) { + newNode = Negative.negate(newNode); + } + + return Node.Status.nodeChanged( + ChangeTypes.FACTOR_SUM_PRODUCT_RULE, node, newNode); + } + } + } + + return Node.Status.noChange(node); +} + + +module.exports = factorQuadratic; diff --git a/lib/node/Creator.js b/lib/node/Creator.js index c19c356f..165a6863 100644 --- a/lib/node/Creator.js +++ b/lib/node/Creator.js @@ -48,16 +48,16 @@ const NodeCreator = { // exponent might be null, which means there's no exponent node. // similarly, coefficient might be null, which means there's no coefficient // the symbol node can never be null. - polynomialTerm (symbol, exponent, coeff, coeffIsNegOne=false) { + polynomialTerm (symbol, exponent, coeff, explicitCoeff=false) { let polyTerm = symbol; if (exponent) { polyTerm = this.operator('^', [polyTerm, exponent]); } - if (coeff) { + if (coeff && (explicitCoeff || parseFloat(coeff.value) !== 1)) { if (NodeType.isConstant(coeff) && parseFloat(coeff.value) === -1 && - !coeffIsNegOne) { - // if you actually want -1 as the coefficient, set coeffIsNegOne to true + !explicitCoeff) { + // if you actually want -1 as the coefficient, set explicitCoeff to true polyTerm = this.unaryMinus(polyTerm); } else { diff --git a/lib/simplifyExpression/collectAndCombineSearch/addLikeTerms.js b/lib/simplifyExpression/collectAndCombineSearch/addLikeTerms.js index 57b6746d..1e1b39a3 100644 --- a/lib/simplifyExpression/collectAndCombineSearch/addLikeTerms.js +++ b/lib/simplifyExpression/collectAndCombineSearch/addLikeTerms.js @@ -84,7 +84,8 @@ function addPositiveOneCoefficient(node) { newNode.args[i] = Node.Creator.polynomialTerm( polyTerm.getSymbolNode(), polyTerm.getExponentNode(), - Node.Creator.constant(1)); + Node.Creator.constant(1), + true /* explicit coefficient */); newNode.args[i].changeGroup = changeGroup; node.args[i].changeGroup = changeGroup; // note that this is the "oldNode" diff --git a/lib/util/print.js b/lib/util/print.js index d61c2220..a1e76611 100644 --- a/lib/util/print.js +++ b/lib/util/print.js @@ -78,7 +78,7 @@ function printTreeTraversal(node) { } else if (Node.Type.isUnaryMinus(node)) { if (Node.Type.isOperator(node.args[0]) && - node.args[0].op !== '/' && + '*/^'.indexOf(node.args[0].op) === -1 && !Node.PolynomialTerm.isPolynomialTerm(node)) { return `-(${printTreeTraversal(node.args[0])})`; } diff --git a/test/checks/isQuadratic.test.js b/test/checks/isQuadratic.test.js new file mode 100644 index 00000000..d5c98d01 --- /dev/null +++ b/test/checks/isQuadratic.test.js @@ -0,0 +1,28 @@ +const checks = require('../../lib/checks'); +const TestUtil = require('../TestUtil'); + +function testIsQuadratic(input, output) { + TestUtil.testBooleanFunction(checks.isQuadratic, input, output); +} + +describe('isQuadratic', function () { + const tests = [ + ['2 + 2', false], + ['x', false], + ['x^2 - 4', true], + ['x^2 + 2x + 1', true], + ['x^2 - 2x + 1', true], + ['x^2 + 3x + 2', true], + ['x^2 - 3x + 2', true], + ['x^2 + x - 2', true], + ['x^2 + x', true], + ['x^2 + 4', true], + ['x^2 + 4x + 1', true], + ['x^2', false], + ['x^3 + x^2 + x + 1', false], + ['x^2 + 4 + 2^x', false], + ['x^2 + 4 + 2y', false], + ['y^2 + 4 + 2x', false], + ]; + tests.forEach(t => testIsQuadratic(t[0], t[1])); +}); diff --git a/test/factor/ConstantFactors.test.js b/test/factor/ConstantFactors.test.js index 98619b60..9fb102fa 100644 --- a/test/factor/ConstantFactors.test.js +++ b/test/factor/ConstantFactors.test.js @@ -29,15 +29,16 @@ function testFactorPairs(input, output) { describe('factor pairs', function() { const tests = [ - [1, [[1, 1]]], - [5, [[1, 5]]], - [12, [[1, 12], [2, 6], [3, 4]]], - [15, [[1, 15], [3, 5]]], - [36, [[1, 36], [2, 18], [3, 12], [4, 9], [6, 6,]]], - [49, [[1, 49], [7, 7]]], - [1260, [[1, 1260], [2, 630], [3, 420], [4, 315], [5, 252], [6, 210], [7, 180], [9, 140], [10, 126], [12, 105], [14, 90], [15, 84], [18, 70], [20, 63], [21, 60], [28, 45], [30, 42], [35, 36]]], - [13195, [[1, 13195], [5, 2639], [7, 1885], [13, 1015], [29, 455], [35, 377], [65, 203], [91, 145]]], - [1234567891, [[1, 1234567891]]] + [1, [[-1, -1], [1, 1]]], + [5, [[-1, -5], [1, 5]]], + [12, [[-3, -4], [-2, -6], [-1, -12], [1, 12], [2, 6], [3, 4]]], + [-12, [[-3, 4], [-2, 6], [-1, 12], [1, -12], [2, -6], [3, -4]]], + [15, [[-3, -5], [-1, -15], [1, 15], [3, 5]]], + [36, [[-6, -6], [-4, -9], [-3, -12], [-2, -18], [-1, -36], [1, 36], [2, 18], [3, 12], [4, 9], [6, 6,]]], + [49, [[-7, -7], [-1, -49], [1, 49], [7, 7]]], + [1260, [[-35, -36], [-30, -42], [-28, -45], [-21, -60], [-20, -63], [-18, -70], [-15, -84], [-14, -90], [-12, -105], [-10, -126], [-9, -140], [-7, -180], [-6, -210], [-5, -252], [-4, -315], [-3, -420], [-2, -630], [-1, -1260], [1, 1260], [2, 630], [3, 420], [4, 315], [5, 252], [6, 210], [7, 180], [9, 140], [10, 126], [12, 105], [14, 90], [15, 84], [18, 70], [20, 63], [21, 60], [28, 45], [30, 42], [35, 36]]], + [13195, [[-91, -145], [-65, -203], [-35, -377], [-29, -455], [-13, -1015], [-7, -1885], [-5, -2639], [-1, -13195], [1, 13195], [5, 2639], [7, 1885], [13, 1015], [29, 455], [35, 377], [65, 203], [91, 145]]], + [1234567891, [[-1, -1234567891], [1, 1234567891]]] ]; tests.forEach(t => testFactorPairs(t[0], t[1])); }); diff --git a/test/factor/factorQuadratic.test.js b/test/factor/factorQuadratic.test.js new file mode 100644 index 00000000..2e290e4d --- /dev/null +++ b/test/factor/factorQuadratic.test.js @@ -0,0 +1,34 @@ +const factorQuadratic = require('../../lib/factor/factorQuadratic'); +const TestUtil = require('../TestUtil'); + +function testFactorQuadratic(input, output) { + TestUtil.testSimplification(factorQuadratic, input, output); +} + +describe('factorQuadratic', function () { + const tests = [ + ['x^2', 'x^2'], + ['x^2 + x^2', 'x^2 + x^2'], + ['x^2 + 2 - 3', 'x^2 + 2 - 3'], + ['x^2 + 2y + 2x + 3', 'x^2 + 2y + 2x + 3'], + ['x^2 + 2x', 'x(x + 2)'], + ['-x^2 - 2x', '-x(x + 2)'], + ['x^2 - 3x', 'x(x - 3)'], + ['2x^2 + 2x', '2x(x + 1)'], + ['x^2 - 4', '(x + 2)(x - 2)'], + ['x^2 + 4', 'x^2 + 4'], + ['x^2 + 2x + 1', '(x + 1)^2'], + ['x^2 - 2x + 1', '(x - 1)^2'], + ['x^2 + 3x + 2', '(x + 1)(x + 2)'], + ['x^2 - 3x + 2', '(x - 1)(x - 2)'], + ['x^2 + x - 2', '(x - 1)(x + 2)'], + ['x^2 + 4x + 1', 'x^2 + 4x + 1'], + ['x^2 - 3x + 1', 'x^2 - 3x + 1'], + ['x^2 + 4 + 2^x', 'x^2 + 4 + 2^x'], + ['-x^2 - 2x - 1', '-(x + 1)^2'], + ['-x^2 - 3x - 2', '-(x + 1)(x + 2)'], + ['-x^2 + 1', '-(x + 1)(x - 1)'], + ['-x^2 - 1', '-x^2 - 1'], + ]; + tests.forEach(t => testFactorQuadratic(t[0], t[1])); +});