diff --git a/.gitignore b/.gitignore index f58d10d0..caea85c4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules *.log .DS_Store +/.vs/MathSteps-Ts/v15 diff --git a/index.js b/index.js deleted file mode 100644 index b96ce3ec..00000000 --- a/index.js +++ /dev/null @@ -1,9 +0,0 @@ -const ChangeTypes = require('./lib/ChangeTypes'); -const simplifyExpression = require('./lib/simplifyExpression'); -const solveEquation = require('./lib/solveEquation'); - -module.exports = { - simplifyExpression, - solveEquation, - ChangeTypes, -}; diff --git a/lib/ChangeTypes.js b/lib/ChangeTypes.js deleted file mode 100644 index a02ccca1..00000000 --- a/lib/ChangeTypes.js +++ /dev/null @@ -1,184 +0,0 @@ -// The text to identify rules for each possible step that can be taken - -module.exports = { - NO_CHANGE: 'NO_CHANGE', - - // 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^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. -2/-3 => 2/3 - CANCEL_MINUSES: 'CANCEL_MINUSES', - // e.g. 2x/2 -> x - CANCEL_TERMS: 'CANCEL_TERMS', - // 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. 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. 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. 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. nthRoot(2 * 2 * 2, 2) -> nthRoot((2 * 2) * 2) - GROUP_TERMS_BY_ROOT: 'GROUP_TERMS_BY_ROOT', - // e.g. nthRoot(4) -> 2 - NTH_ROOT_VALUE: 'NTH_ROOT_VALUE', - - // SOLVING FOR A VARIABLE - - // e.g. x - 3 = 2 -> x - 3 + 3 = 2 + 3 - ADD_TO_BOTH_SIDES: 'ADD_TO_BOTH_SIDES', - // e.g. 2x = 1 -> (2x)/2 = 1/2 - DIVIDE_FROM_BOTH_SIDES: 'DIVIDE_FROM_BOTH_SIDES', - // e.g. (2/3)x = 1 -> (2/3)x * (3/2) = 1 * (3/2) - MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION: 'MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION', - // e.g. -x = 2 -> -1 * -x = -1 * 2 - MULTIPLY_BOTH_SIDES_BY_NEGATIVE_ONE: 'MULTIPLY_BOTH_SIDES_BY_NEGATIVE_ONE', - // e.g. x/2 = 1 -> (x/2) * 2 = 1 * 2 - MULTIPLY_TO_BOTH_SIDES: 'MULTIPLY_TO_BOTH_SIDES', - // e.g. x + 2 - 1 = 3 -> x + 1 = 3 - SIMPLIFY_LEFT_SIDE: 'SIMPLIFY_LEFT_SIDE', - // e.g. x = 3 - 1 -> x = 2 - SIMPLIFY_RIGHT_SIDE: 'SIMPLIFY_RIGHT_SIDE', - // e.g. x + 3 = 2 -> x + 3 - 3 = 2 - 3 - SUBTRACT_FROM_BOTH_SIDES: 'SUBTRACT_FROM_BOTH_SIDES', - // 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/TreeSearch.js b/lib/TreeSearch.js deleted file mode 100644 index ea5c7b18..00000000 --- a/lib/TreeSearch.js +++ /dev/null @@ -1,70 +0,0 @@ -const Node = require('./node'); - -const TreeSearch = {}; - -// Returns a function that performs a preorder search on the tree for the given -// simplifcation function -TreeSearch.preOrder = function(simplificationFunction) { - return function (node) { - return search(simplificationFunction, node, true); - }; -}; - -// Returns a function that performs a postorder search on the tree for the given -// simplifcation function -TreeSearch.postOrder = function(simplificationFunction) { - return function (node) { - return search(simplificationFunction, node, false); - }; -}; - -// A helper function for performing a tree search with a function -function search(simplificationFunction, node, preOrder) { - let status; - - if (preOrder) { - status = simplificationFunction(node); - if (status.hasChanged()) { - return status; - } - } - - if (Node.Type.isConstant(node) || Node.Type.isSymbol(node)) { - return Node.Status.noChange(node); - } - else if (Node.Type.isUnaryMinus(node)) { - status = search(simplificationFunction, node.args[0], preOrder); - if (status.hasChanged()) { - return Node.Status.childChanged(node, status); - } - } - else if (Node.Type.isOperator(node) || Node.Type.isFunction(node)) { - for (let i = 0; i < node.args.length; i++) { - const child = node.args[i]; - const childNodeStatus = search(simplificationFunction, child, preOrder); - if (childNodeStatus.hasChanged()) { - return Node.Status.childChanged(node, childNodeStatus, i); - } - } - } - else if (Node.Type.isParenthesis(node)) { - status = search(simplificationFunction, node.content, preOrder); - if (status.hasChanged()) { - return Node.Status.childChanged(node, status); - } - } - else { - throw Error('Unsupported node type: ' + node); - } - - if (!preOrder) { - return simplificationFunction(node); - } - else { - return Node.Status.noChange(node); - } -} - - - -module.exports = TreeSearch; diff --git a/lib/checks/canAddLikeTermPolynomialNodes.js b/lib/checks/canAddLikeTermPolynomialNodes.js deleted file mode 100644 index 6c9ac1ca..00000000 --- a/lib/checks/canAddLikeTermPolynomialNodes.js +++ /dev/null @@ -1,32 +0,0 @@ -const Node = require('../node'); - -// Returns true if the nodes are polynomial terms that can be added together. -function canAddLikeTermPolynomialNodes(node) { - if (!Node.Type.isOperator(node) || node.op !== '+') { - return false; - } - const args = node.args; - if (!args.every(n => Node.PolynomialTerm.isPolynomialTerm(n))) { - return false; - } - if (args.length === 1) { - return false; - } - - const polynomialTermList = args.map(n => new Node.PolynomialTerm(n)); - - // to add terms, they must have the same symbol name *and* exponent - const firstTerm = polynomialTermList[0]; - const sharedSymbol = firstTerm.getSymbolName(); - const sharedExponentNode = firstTerm.getExponentNode(true); - - const restTerms = polynomialTermList.slice(1); - return restTerms.every(term => { - const haveSameSymbol = sharedSymbol === term.getSymbolName(); - const exponentNode = term.getExponentNode(true); - const haveSameExponent = exponentNode.equals(sharedExponentNode); - return haveSameSymbol && haveSameExponent; - }); -} - -module.exports = canAddLikeTermPolynomialNodes; diff --git a/lib/checks/canMultiplyLikeTermPolynomialNodes.js b/lib/checks/canMultiplyLikeTermPolynomialNodes.js deleted file mode 100644 index 0654ae82..00000000 --- a/lib/checks/canMultiplyLikeTermPolynomialNodes.js +++ /dev/null @@ -1,28 +0,0 @@ -const Node = require('../node'); - -// Returns true if the nodes are symbolic terms with the same symbol and no -// coefficients. -function canMultiplyLikeTermPolynomialNodes(node) { - if (!Node.Type.isOperator(node) || node.op !== '*') { - return false; - } - const args = node.args; - if (!args.every(n => Node.PolynomialTerm.isPolynomialTerm(n))) { - return false; - } - if (args.length === 1) { - return false; - } - - const polynomialTermList = node.args.map(n => new Node.PolynomialTerm(n)); - if (!polynomialTermList.every(polyTerm => !polyTerm.hasCoeff())) { - return false; - } - - const firstTerm = polynomialTermList[0]; - const restTerms = polynomialTermList.slice(1); - // they're considered like terms if they have the same symbol name - return restTerms.every(term => firstTerm.getSymbolName() === term.getSymbolName()); -} - -module.exports = canMultiplyLikeTermPolynomialNodes; diff --git a/lib/checks/canRearrangeCoefficient.js b/lib/checks/canRearrangeCoefficient.js deleted file mode 100644 index 36979a4f..00000000 --- a/lib/checks/canRearrangeCoefficient.js +++ /dev/null @@ -1,25 +0,0 @@ -const Node = require('../node'); - -// Returns true if the expression is a multiplication between a constant -// and polynomial without a coefficient. -function canRearrangeCoefficient(node) { - // implicit multiplication doesn't count as multiplication here, since it - // represents a single term. - if (node.op !== '*' || node.implicit) { - return false; - } - if (node.args.length !== 2) { - return false; - } - if (!Node.Type.isConstantOrConstantFraction(node.args[1])) { - return false; - } - if (!Node.PolynomialTerm.isPolynomialTerm(node.args[0])) { - return false; - } - - const polyNode = new Node.PolynomialTerm(node.args[0]); - return !polyNode.hasCoeff(); -} - -module.exports = canRearrangeCoefficient; diff --git a/lib/checks/canSimplifyPolynomialTerms.js b/lib/checks/canSimplifyPolynomialTerms.js deleted file mode 100644 index eed134d0..00000000 --- a/lib/checks/canSimplifyPolynomialTerms.js +++ /dev/null @@ -1,13 +0,0 @@ -const canAddLikeTermPolynomialNodes = require('./canAddLikeTermPolynomialNodes'); -const canMultiplyLikeTermPolynomialNodes = require('./canMultiplyLikeTermPolynomialNodes'); -const canRearrangeCoefficient = require('./canRearrangeCoefficient'); - -// Returns true if the node is an operation node with parameters that are -// polynomial terms that can be combined in some way. -function canSimplifyPolynomialTerms(node) { - return (canAddLikeTermPolynomialNodes(node) || - canMultiplyLikeTermPolynomialNodes(node) || - canRearrangeCoefficient(node)); -} - -module.exports = canSimplifyPolynomialTerms; diff --git a/lib/checks/hasUnsupportedNodes.js b/lib/checks/hasUnsupportedNodes.js deleted file mode 100644 index fb229ab4..00000000 --- a/lib/checks/hasUnsupportedNodes.js +++ /dev/null @@ -1,34 +0,0 @@ -const Node = require('../node'); -const resolvesToConstant = require('./resolvesToConstant'); - -function hasUnsupportedNodes(node) { - if (Node.Type.isParenthesis(node)) { - return hasUnsupportedNodes(node.content); - } - else if (Node.Type.isUnaryMinus(node)) { - return hasUnsupportedNodes(node.args[0]); - } - else if (Node.Type.isOperator(node)) { - return node.args.some(hasUnsupportedNodes); - } - else if (Node.Type.isSymbol(node) || Node.Type.isConstant(node)) { - return false; - } - else if (Node.Type.isFunction(node, 'abs')) { - if (node.args.length !== 1) { - return true; - } - if (node.args.some(hasUnsupportedNodes)) { - return true; - } - return !resolvesToConstant(node.args[0]); - } - else if (Node.Type.isFunction(node, 'nthRoot')) { - return node.args.some(hasUnsupportedNodes) || node.args.length < 1; - } - else { - return true; - } -} - -module.exports = hasUnsupportedNodes; diff --git a/lib/checks/index.js b/lib/checks/index.js deleted file mode 100644 index 5bb07252..00000000 --- a/lib/checks/index.js +++ /dev/null @@ -1,17 +0,0 @@ -const canAddLikeTermPolynomialNodes = require('./canAddLikeTermPolynomialNodes'); -const canMultiplyLikeTermPolynomialNodes = require('./canMultiplyLikeTermPolynomialNodes'); -const canRearrangeCoefficient = require('./canRearrangeCoefficient'); -const canSimplifyPolynomialTerms = require('./canSimplifyPolynomialTerms'); -const hasUnsupportedNodes = require('./hasUnsupportedNodes'); -const isQuadratic = require('./isQuadratic'); -const resolvesToConstant = require('./resolvesToConstant'); - -module.exports = { - canAddLikeTermPolynomialNodes, - canMultiplyLikeTermPolynomialNodes, - canRearrangeCoefficient, - canSimplifyPolynomialTerms, - hasUnsupportedNodes, - isQuadratic, - resolvesToConstant, -}; diff --git a/lib/checks/isQuadratic.js b/lib/checks/isQuadratic.js deleted file mode 100644 index f23ec7a7..00000000 --- a/lib/checks/isQuadratic.js +++ /dev/null @@ -1,54 +0,0 @@ -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/checks/resolvesToConstant.js b/lib/checks/resolvesToConstant.js deleted file mode 100644 index c70829c9..00000000 --- a/lib/checks/resolvesToConstant.js +++ /dev/null @@ -1,28 +0,0 @@ -const Node = require('../node'); - -// Returns true if the node is a constant or can eventually be resolved to -// a constant. -// e.g. 2, 2+4, (2+4)^2 would all return true. x + 4 would return false -function resolvesToConstant(node) { - if (Node.Type.isOperator(node) || Node.Type.isFunction(node)) { - return node.args.every( - (child) => resolvesToConstant(child)); - } - else if (Node.Type.isParenthesis(node)) { - return resolvesToConstant(node.content); - } - else if (Node.Type.isConstant(node, true)) { - return true; - } - else if (Node.Type.isSymbol(node)) { - return false; - } - else if (Node.Type.isUnaryMinus(node)) { - return resolvesToConstant(node.args[0]); - } - else { - throw Error('Unsupported node type: ' + node.type); - } -} - -module.exports = resolvesToConstant; diff --git a/lib/equation/Equation.js b/lib/equation/Equation.js deleted file mode 100644 index 26b2c1d9..00000000 --- a/lib/equation/Equation.js +++ /dev/null @@ -1,45 +0,0 @@ -const math = require('mathjs'); - -const clone = require('../util/clone'); -const printNode = require('../util/print'); - -// This represents an equation, made up of the leftNode (LHS), the -// rightNode (RHS) and a comparator (=, <, >, <=, or >=) -class Equation { - constructor(leftNode, rightNode, comparator) { - this.leftNode = leftNode; - this.rightNode = rightNode; - this.comparator = comparator; - } - - // Prints an Equation properly using the print module - print(showPlusMinus=false) { - const leftSide = printNode(this.leftNode, showPlusMinus); - const rightSide = printNode(this.rightNode, showPlusMinus); - const comparator = this.comparator; - - return `${leftSide} ${comparator} ${rightSide}`; - } - - clone() { - const newLeft = clone(this.leftNode); - const newRight = clone(this.rightNode); - return new Equation(newLeft, newRight, this.comparator); - } -} - -// Splits a string on the given comparator and returns a new Equation object -// from the left and right hand sides -Equation.createEquationFromString = function(str, comparator) { - const sides = str.split(comparator); - if (sides.length !== 2) { - throw Error('Expected two sides of an equation using comparator: ' + - comparator); - } - const leftNode = math.parse(sides[0]); - const rightNode = math.parse(sides[1]); - - return new Equation(leftNode, rightNode, comparator); -}; - -module.exports = Equation; diff --git a/lib/equation/Status.js b/lib/equation/Status.js deleted file mode 100644 index a8754442..00000000 --- a/lib/equation/Status.js +++ /dev/null @@ -1,73 +0,0 @@ -const ChangeTypes = require('../ChangeTypes'); -const Equation = require('./Equation'); -const Node = require('../node'); - -// This represents the current equation we're solving. -// As we move step by step, an equation might be updated. Functions return this -// status object to pass on the updated equation and information on if/how it was -// changed. -class Status { - constructor(changeType, oldEquation, newEquation, substeps=[]) { - if (!newEquation) { - throw Error('new equation isn\'t defined'); - } - if (changeType === undefined || typeof(changeType) !== 'string') { - throw Error('changetype isn\'t valid'); - } - - this.changeType = changeType; - this.oldEquation = oldEquation; - this.newEquation = newEquation; - this.substeps = substeps; - } - - hasChanged() { - return this.changeType !== ChangeTypes.NO_CHANGE; - } -} - -// A wrapper around the Status constructor for the case where equation -// hasn't been changed. -Status.noChange = function(equation) { - return new Status(ChangeTypes.NO_CHANGE, null, equation); -}; - -Status.addLeftStep = function(equation, leftStep) { - const substeps = []; - leftStep.substeps.forEach(substep => { - substeps.push(Status.addLeftStep(equation, substep)); - }); - let oldEquation = null; - if (leftStep.oldNode) { - oldEquation = equation.clone(); - oldEquation.leftNode = leftStep.oldNode; - } - const newEquation = equation.clone(); - newEquation.leftNode = leftStep.newNode; - return new Status( - leftStep.changeType, oldEquation, newEquation, substeps); -}; - -Status.addRightStep = function(equation, rightStep) { - const substeps = []; - rightStep.substeps.forEach(substep => { - substeps.push(Status.addRightStep(equation, substep)); - }); - let oldEquation = null; - if (rightStep.oldNode) { - oldEquation = equation.clone(); - oldEquation.rightNode = rightStep.oldNode; - } - const newEquation = equation.clone(); - newEquation.rightNode = rightStep.newNode; - return new Status( - rightStep.changeType, oldEquation, newEquation, substeps); -}; - -Status.resetChangeGroups = function(equation) { - const leftNode = Node.Status.resetChangeGroups(equation.leftNode); - const rightNode = Node.Status.resetChangeGroups(equation.rightNode); - return new Equation(leftNode, rightNode, equation.comparator); -}; - -module.exports = Status; diff --git a/lib/equation/index.js b/lib/equation/index.js deleted file mode 100644 index 0f18d847..00000000 --- a/lib/equation/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const Equation = require('./Equation'); -const Status = require('./Status'); - -module.exports = { - Equation, - Status, -}; diff --git a/lib/factor/ConstantFactors.js b/lib/factor/ConstantFactors.js deleted file mode 100644 index ff72f287..00000000 --- a/lib/factor/ConstantFactors.js +++ /dev/null @@ -1,58 +0,0 @@ -// This module deals with getting constant factors, including prime factors -// and factor pairs of a number - -const ConstantFactors = {}; - -// Given a number, will return all the prime factors of that number as a list -// sorted from smallest to largest -ConstantFactors.getPrimeFactors = function(number){ - let factors = []; - if (number < 0) { - factors = [-1]; - factors = factors.concat(ConstantFactors.getPrimeFactors(-1 * number)); - return factors; - } - - const root = Math.sqrt(number); - let candidate = 2; - if (number % 2) { - candidate = 3; // assign first odd - while (number % candidate && candidate <= root) { - candidate = candidate + 2; - } - } - - // if no factor found then the number is prime - if (candidate > root) { - factors.push(number); - } - // if we find a factor, make a recursive call on the quotient of the number and - // our newly found prime factor in order to find more factors - else { - factors.push(candidate); - factors = factors.concat(ConstantFactors.getPrimeFactors(number/candidate)); - } - - return factors; -}; - -// Given a number, will return all the factor pairs for that number as a list -// of 2-item lists -ConstantFactors.getFactorPairs = function(number){ - const factors = []; - - 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]); - } - } - - return factors; -}; - -module.exports = ConstantFactors; diff --git a/lib/factor/factorQuadratic.js b/lib/factor/factorQuadratic.js deleted file mode 100644 index 0a4b60e8..00000000 --- a/lib/factor/factorQuadratic.js +++ /dev/null @@ -1,225 +0,0 @@ -const math = require('mathjs'); - -const ConstantFactors = require('./ConstantFactors'); - -const ChangeTypes = require('../ChangeTypes'); -const checks = require('../checks'); -const evaluate = require('../util/evaluate'); -const flatten = require('../util/flattenOperands'); -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 = 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); - } - - 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 deleted file mode 100644 index 165a6863..00000000 --- a/lib/node/Creator.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - Functions to generate any mathJS node supported by the stepper - see http://mathjs.org/docs/expressions/expression_trees.html#nodes for more - information on nodes in mathJS -*/ - -const math = require('mathjs'); -const NodeType = require('./Type'); - -const NodeCreator = { - operator (op, args, implicit=false) { - switch (op) { - case '+': - return new math.expression.node.OperatorNode('+', 'add', args); - case '-': - return new math.expression.node.OperatorNode('-', 'subtract', args); - case '/': - return new math.expression.node.OperatorNode('/', 'divide', args); - case '*': - return new math.expression.node.OperatorNode( - '*', 'multiply', args, implicit); - case '^': - return new math.expression.node.OperatorNode('^', 'pow', args); - default: - throw Error('Unsupported operation: ' + op); - } - }, - - // In almost all cases, use Negative.negate (with naive = true) to add a - // unary minus to your node, rather than calling this constructor directly - unaryMinus (content) { - return new math.expression.node.OperatorNode( - '-', 'unaryMinus', [content]); - }, - - constant (val) { - return new math.expression.node.ConstantNode(val); - }, - - symbol (name) { - return new math.expression.node.SymbolNode(name); - }, - - parenthesis (content) { - return new math.expression.node.ParenthesisNode(content); - }, - - // 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, explicitCoeff=false) { - let polyTerm = symbol; - if (exponent) { - polyTerm = this.operator('^', [polyTerm, exponent]); - } - if (coeff && (explicitCoeff || parseFloat(coeff.value) !== 1)) { - if (NodeType.isConstant(coeff) && - parseFloat(coeff.value) === -1 && - !explicitCoeff) { - // if you actually want -1 as the coefficient, set explicitCoeff to true - polyTerm = this.unaryMinus(polyTerm); - } - else { - polyTerm = this.operator('*', [coeff, polyTerm], true); - } - } - return polyTerm; - }, - - // Given a root value and a radicand (what is under the radical) - nthRoot (radicandNode, rootNode) { - const symbol = NodeCreator.symbol('nthRoot'); - return new math.expression.node.FunctionNode(symbol, [radicandNode, rootNode]); - } -}; - -module.exports = NodeCreator; diff --git a/lib/node/PolynomialTerm.js b/lib/node/PolynomialTerm.js deleted file mode 100644 index e5aa8919..00000000 --- a/lib/node/PolynomialTerm.js +++ /dev/null @@ -1,186 +0,0 @@ -const NodeCreator = require('./Creator'); -const NodeType = require('./Type'); - -const evaluate = require('../util/evaluate'); - -// For storing polynomial terms. -// Has a symbol (e.g. x), maybe an exponent, and maybe a coefficient. -// These expressions are of the form of a PolynomialTerm: x^2, 2y, z, 3x/5 -// These expressions are not: 4, x^(3+4), 2+x, 3*7, x-z -/* Fields: - - coeff: either a constant node or a fraction of two constant nodes - (might be null if no coefficient) - - symbol: the node with the symbol (e.g. in x^2, the node x) - - exponent: a node that can take any form, e.g. x^(2+x^2) - (might be null if no exponent) -*/ -class PolynomialTerm { - // if onlyImplicitMultiplication is true, an error will be thrown if `node` - // is a polynomial term without implicit multiplication - // (i.e. 2*x instead of 2x) and therefore isPolynomialTerm will return false. - constructor(node, onlyImplicitMultiplication=false) { - if (NodeType.isOperator(node)) { - if (node.op === '^') { - const symbolNode = node.args[0]; - if (!NodeType.isSymbol(symbolNode)) { - throw Error('Expected symbol term, got ' + symbolNode); - } - this.symbol = symbolNode; - this.exponent = node.args[1]; - } - // it's '*' ie it has a coefficient - else if (node.op === '*') { - if (onlyImplicitMultiplication && !node.implicit) { - throw Error('Expected implicit multiplication'); - } - if (node.args.length !== 2) { - throw Error('Expected two arguments to *'); - } - const coeffNode = node.args[0]; - if (!NodeType.isConstantOrConstantFraction(coeffNode)) { - throw Error('Expected coefficient to be constant or fraction of ' + - 'constants term, got ' + coeffNode); - } - this.coeff = coeffNode; - const nonCoefficientTerm = new PolynomialTerm( - node.args[1], onlyImplicitMultiplication); - if (nonCoefficientTerm.hasCoeff()) { - throw Error('Cannot have two coefficients ' + coeffNode + - ' and ' + nonCoefficientTerm.getCoeffNode()); - } - this.symbol = nonCoefficientTerm.getSymbolNode(); - this.exponent = nonCoefficientTerm.getExponentNode(); - } - // this means there's a fraction coefficient - else if (node.op === '/') { - const denominatorNode = node.args[1]; - if (!NodeType.isConstant(denominatorNode)) { - throw Error('denominator must be constant node, instead of ' + - denominatorNode); - } - const numeratorNode = new PolynomialTerm( - node.args[0], onlyImplicitMultiplication); - if (numeratorNode.hasFractionCoeff()) { - throw Error('Polynomial terms cannot have nested fractions'); - } - this.exponent = numeratorNode.getExponentNode(); - this.symbol = numeratorNode.getSymbolNode(); - const numeratorConstantNode = numeratorNode.getCoeffNode(true); - this.coeff = NodeCreator.operator( - '/', [numeratorConstantNode, denominatorNode]); - } - else { - throw Error('Unsupported operatation for polynomial node: ' + node.op); - } - } - else if (NodeType.isUnaryMinus(node)) { - var arg = node.args[0]; - if (NodeType.isParenthesis(arg)) { - arg = arg.content; - } - const polyNode = new PolynomialTerm( - arg, onlyImplicitMultiplication); - this.exponent = polyNode.getExponentNode(); - this.symbol = polyNode.getSymbolNode(); - if (!polyNode.hasCoeff()) { - this.coeff = NodeCreator.constant(-1); - } - else { - this.coeff = negativeCoefficient(polyNode.getCoeffNode()); - } - } - else if (NodeType.isSymbol(node)) { - this.symbol = node; - } - else { - throw Error('Unsupported node type: ' + node.type); - } - } - - /* GETTER FUNCTIONS */ - getSymbolNode() { - return this.symbol; - } - - getSymbolName() { - return this.symbol.name; - } - - getCoeffNode(defaultOne=false) { - if (!this.coeff && defaultOne) { - return NodeCreator.constant(1); - } - else { - return this.coeff; - } - } - - getCoeffValue() { - if (this.coeff) { - return evaluate(this.coeff); - } - else { - return 1; // no coefficient is like a coeff of 1 - } - } - - getExponentNode(defaultOne=false) { - if (!this.exponent && defaultOne) { - return NodeCreator.constant(1); - } - else { - return this.exponent; - } - } - - getRootNode() { - return NodeCreator.polynomialTerm( - this.symbol, this.exponent, this.coeff); - } - - // note: there is no exponent value getter function because the exponent - // can be any expresion and not necessarily a number. - - /* CHECKER FUNCTIONS (returns true / false for certain conditions) */ - - // Returns true if the coefficient is a fraction - hasFractionCoeff() { - // coeffNode is either a constant or a division operation. - return this.coeff && NodeType.isOperator(this.coeff); - } - - hasCoeff() { - return !!this.coeff; - } -} - -// Returns if the node represents an expression that can be considered a term. -// e.g. x^2, 2y, z, 3x/5 are all terms. 4, 2+x, 3*7, x-z are all not terms. -// See the tests for some more thorough examples of exactly what counts and -// what does not. -PolynomialTerm.isPolynomialTerm = function( - node, onlyImplicitMultiplication=false) { - try { - // will throw error if node isn't poly term - new PolynomialTerm(node, onlyImplicitMultiplication); - return true; - } - catch (err) { - return false; - } -}; - -// Multiplies `node`, a constant or fraction of two constant nodes, by -1 -// Returns a node -function negativeCoefficient(node) { - if (NodeType.isConstant(node)) { - node = NodeCreator.constant(0 - parseFloat(node.value)); - } - else { - const numeratorValue = 0 - parseFloat(node.args[0].value); - node.args[0] = NodeCreator.constant(numeratorValue); - } - return node; -} - -module.exports = PolynomialTerm; diff --git a/lib/node/Status.js b/lib/node/Status.js deleted file mode 100644 index cc4440e5..00000000 --- a/lib/node/Status.js +++ /dev/null @@ -1,126 +0,0 @@ -const clone = require('../util/clone'); - -const ChangeTypes = require('../ChangeTypes'); -const Type = require('./Type'); - -// This represents the current (sub)expression we're simplifying. -// As we move step by step, a node might be updated. Functions return this -// status object to pass on the updated node and information on if/how it was -// changed. -// Status(node) creates a Status object that signals no change -class Status { - constructor(changeType, oldNode, newNode, substeps=[]) { - if (!newNode) { - throw Error('node is not defined'); - } - if (changeType === undefined || typeof(changeType) !== 'string') { - throw Error('changetype isn\'t valid'); - } - - this.changeType = changeType; - this.oldNode = oldNode; - this.newNode = newNode; - this.substeps = substeps; - } - - hasChanged() { - return this.changeType !== ChangeTypes.NO_CHANGE; - } -} - -Status.resetChangeGroups = function(node) { - node = clone(node); - node.filter(node => node.changeGroup).forEach(change => { - delete change.changeGroup; - }); - return node; -}; - -// A wrapper around the Status constructor for the case where node hasn't -// been changed. -Status.noChange = function(node) { - return new Status(ChangeTypes.NO_CHANGE, null, node); -}; - -// A wrapper around the Status constructor for the case of a change -// that is happening at the level of oldNode + newNode -// e.g. 2 + 2 --> 4 (an addition node becomes a constant node) -Status.nodeChanged = function( - changeType, oldNode, newNode, defaultChangeGroup=true, steps=[]) { - if (defaultChangeGroup) { - oldNode.changeGroup = 1; - newNode.changeGroup = 1; - } - - return new Status(changeType, oldNode, newNode, steps); -}; - -// A wrapper around the Status constructor for the case where there was -// a change that happened deeper `node`'s tree, and `node`'s children must be -// updated to have the newNode/oldNode metadata (changeGroups) -// e.g. (2 + 2) + x --> 4 + x has to update the left argument -Status.childChanged = function(node, childStatus, childArgIndex=null) { - const oldNode = clone(node); - const newNode = clone(node); - let substeps = childStatus.substeps; - - if (!childStatus.oldNode) { - throw Error ('Expected old node for changeType: ' + childStatus.changeType); - } - - function updateSubsteps(substeps, fn) { - substeps.map((step) => { - step = fn(step); - step.substeps = updateSubsteps(step.substeps, fn); - }); - return substeps; - } - - if (Type.isParenthesis(node)) { - oldNode.content = childStatus.oldNode; - newNode.content = childStatus.newNode; - substeps = updateSubsteps(substeps, (step) => { - const oldNode = clone(node); - const newNode = clone(node); - oldNode.content = step.oldNode; - newNode.content = step.newNode; - step.oldNode = oldNode; - step.newNode = newNode; - return step; - }); - } - else if ((Type.isOperator(node) || Type.isFunction(node) && - childArgIndex !== null)) { - oldNode.args[childArgIndex] = childStatus.oldNode; - newNode.args[childArgIndex] = childStatus.newNode; - substeps = updateSubsteps(substeps, (step) => { - const oldNode = clone(node); - const newNode = clone(node); - oldNode.args[childArgIndex] = step.oldNode; - newNode.args[childArgIndex] = step.newNode; - step.oldNode = oldNode; - step.newNode = newNode; - return step; - }); - } - else if (Type.isUnaryMinus(node)) { - oldNode.args[0] = childStatus.oldNode; - newNode.args[0] = childStatus.newNode; - substeps = updateSubsteps(substeps, (step) => { - const oldNode = clone(node); - const newNode = clone(node); - oldNode.args[0] = step.oldNode; - newNode.args[0] = step.newNode; - step.oldNode = oldNode; - step.newNode = newNode; - return step; - }); - } - else { - throw Error('Unexpected node type: ' + node.type); - } - - return new Status(childStatus.changeType, oldNode, newNode, substeps); -}; - -module.exports = Status; diff --git a/lib/node/Type.js b/lib/node/Type.js deleted file mode 100644 index 97f7406e..00000000 --- a/lib/node/Type.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - For determining the type of a mathJS node. - */ - -const NodeType = {}; - -NodeType.isOperator = function(node, operator=null) { - return node.type === 'OperatorNode' && - node.fn !== 'unaryMinus' && - '*+-/^'.includes(node.op) && - (operator ? node.op === operator : true); -}; - -NodeType.isParenthesis = function(node) { - return node.type === 'ParenthesisNode'; -}; - -NodeType.isUnaryMinus = function(node) { - return node.type === 'OperatorNode' && node.fn === 'unaryMinus'; -}; - -NodeType.isFunction = function(node, functionName=null) { - if (node.type !== 'FunctionNode') { - return false; - } - if (functionName && node.fn.name !== functionName) { - return false; - } - return true; -}; - -NodeType.isSymbol = function(node, allowUnaryMinus=true) { - if (node.type === 'SymbolNode') { - return true; - } - else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { - return NodeType.isSymbol(node.args[0], false); - } - else { - return false; - } -}; - -NodeType.isConstant = function(node, allowUnaryMinus=false) { - if (node.type === 'ConstantNode') { - return true; - } - else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { - if (NodeType.isConstant(node.args[0], false)) { - const value = parseFloat(node.args[0].value); - return value >= 0; - } - else { - return false; - } - } - else { - return false; - } -}; - -NodeType.isConstantFraction = function(node, allowUnaryMinus=false) { - if (NodeType.isOperator(node, '/')) { - return node.args.every(n => NodeType.isConstant(n, allowUnaryMinus)); - } - else { - return false; - } -}; - -NodeType.isConstantOrConstantFraction = function(node, allowUnaryMinus=false) { - if (NodeType.isConstant(node, allowUnaryMinus) || - NodeType.isConstantFraction(node, allowUnaryMinus)) { - return true; - } - else { - return false; - } -}; - -NodeType.isIntegerFraction = function(node, allowUnaryMinus=false) { - if (!NodeType.isConstantFraction(node, allowUnaryMinus)) { - return false; - } - let numerator = node.args[0]; - let denominator = node.args[1]; - if (allowUnaryMinus) { - if (NodeType.isUnaryMinus(numerator)) { - numerator = numerator.args[0]; - } - if (NodeType.isUnaryMinus(denominator)) { - denominator = denominator.args[0]; - } - } - return (Number.isInteger(parseFloat(numerator.value)) && - Number.isInteger(parseFloat(denominator.value))); -}; - - -module.exports = NodeType; diff --git a/lib/node/index.js b/lib/node/index.js deleted file mode 100644 index c40c0797..00000000 --- a/lib/node/index.js +++ /dev/null @@ -1,11 +0,0 @@ -const Creator = require('./Creator'); -const PolynomialTerm = require('./PolynomialTerm'); -const Status = require('./Status'); -const Type = require('./Type'); - -module.exports = { - Creator, - PolynomialTerm, - Status, - Type, -}; diff --git a/lib/simplifyExpression/arithmeticSearch/index.js b/lib/simplifyExpression/arithmeticSearch/index.js deleted file mode 100644 index b84c7f8f..00000000 --- a/lib/simplifyExpression/arithmeticSearch/index.js +++ /dev/null @@ -1,61 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const evaluate = require('../../util/evaluate'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -// Searches through the tree, prioritizing deeper nodes, and evaluates -// arithmetic (e.g. 2+2 or 3*5*2) on an operation node if possible. -// Returns a Node.Status object. -const search = TreeSearch.postOrder(arithmetic); - -// evaluates arithmetic (e.g. 2+2 or 3*5*2) on an operation node. -// Returns a Node.Status object. -function arithmetic(node) { - if (!Node.Type.isOperator(node)) { - return Node.Status.noChange(node); - } - if (!node.args.every(child => Node.Type.isConstant(child, true))) { - return Node.Status.noChange(node); - } - - // we want to eval each arg so unary minuses around constant nodes become - // constant nodes with negative values - node.args.forEach((arg, i) => { - node.args[i] = Node.Creator.constant(evaluate(arg)); - }); - - // Only resolve division of integers if we get an integer result. - // Note that a fraction of decimals will be divided out. - if (Node.Type.isIntegerFraction(node)) { - const numeratorValue = parseInt(node.args[0]); - const denominatorValue = parseInt(node.args[1]); - if (numeratorValue % denominatorValue === 0) { - const newNode = Node.Creator.constant(numeratorValue/denominatorValue); - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode); - } - else { - return Node.Status.noChange(node); - } - } - else { - const evaluatedValue = evaluateAndRound(node); - const newNode = Node.Creator.constant(evaluatedValue); - return Node.Status.nodeChanged(ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode); - } -} - -// Evaluates a math expression to a constant, e.g. 3+4 -> 7 and rounds if -// necessary -function evaluateAndRound(node) { - let result = evaluate(node); - if (result < 1) { - result = parseFloat(result.toPrecision(4)); - } - else { - result = parseFloat(result.toFixed(4)); - } - return result; -} - -module.exports = search; diff --git a/lib/simplifyExpression/basicsSearch/index.js b/lib/simplifyExpression/basicsSearch/index.js deleted file mode 100644 index d524cc68..00000000 --- a/lib/simplifyExpression/basicsSearch/index.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Performs simpifications that are more basic and overaching like (...)^0 => 1 - * These are always the first simplifications that are attempted. - */ - -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -const rearrangeCoefficient = require('./rearrangeCoefficient'); -const reduceExponentByZero = require('./reduceExponentByZero'); -const reduceMultiplicationByZero = require('./reduceMultiplicationByZero'); -const reduceZeroDividedByAnything = require('./reduceZeroDividedByAnything'); -const removeAdditionOfZero = require('./removeAdditionOfZero'); -const removeDivisionByOne = require('./removeDivisionByOne'); -const removeExponentBaseOne = require('./removeExponentBaseOne'); -const removeExponentByOne = require('./removeExponentByOne'); -const removeMultiplicationByNegativeOne = require('./removeMultiplicationByNegativeOne'); -const removeMultiplicationByOne = require('./removeMultiplicationByOne'); -const simplifyDoubleUnaryMinus = require('./simplifyDoubleUnaryMinus'); - -const SIMPLIFICATION_FUNCTIONS = [ - // multiplication by 0 yields 0 - reduceMultiplicationByZero, - // division of 0 by something yields 0 - reduceZeroDividedByAnything, - // ____^0 --> 1 - reduceExponentByZero, - // Check for x^1 which should be reduced to x - removeExponentByOne, - // Check for 1^x which should be reduced to 1 - // if x can be simplified to a constant - removeExponentBaseOne, - // - - becomes + - simplifyDoubleUnaryMinus, - // If this is a + node and one of the operands is 0, get rid of the 0 - removeAdditionOfZero, - // If this is a * node and one of the operands is 1, get rid of the 1 - removeMultiplicationByOne, - // In some cases, remove multiplying by -1 - removeMultiplicationByNegativeOne, - // If this is a / node and the denominator is 1 or -1, get rid of it - removeDivisionByOne, - // e.g. x*5 -> 5x - rearrangeCoefficient, -]; - -const search = TreeSearch.preOrder(basics); - -// Look for basic step(s) to perform on a node. Returns a Node.Status object. -function basics(node) { - for (let i = 0; i < SIMPLIFICATION_FUNCTIONS.length; i++) { - const nodeStatus = SIMPLIFICATION_FUNCTIONS[i](node); - if (nodeStatus.hasChanged()) { - return nodeStatus; - } - else { - node = nodeStatus.newNode; - } - } - return Node.Status.noChange(node); -} - -module.exports = search; diff --git a/lib/simplifyExpression/basicsSearch/rearrangeCoefficient.js b/lib/simplifyExpression/basicsSearch/rearrangeCoefficient.js deleted file mode 100644 index 03c148a9..00000000 --- a/lib/simplifyExpression/basicsSearch/rearrangeCoefficient.js +++ /dev/null @@ -1,27 +0,0 @@ -const checks = require('../../checks'); -const clone = require('../../util/clone'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// Rearranges something of the form x * 5 to be 5x, ie putting the coefficient -// in the right place. -// Returns a Node.Status object -function rearrangeCoefficient(node) { - if (!checks.canRearrangeCoefficient(node)) { - return Node.Status.noChange(node); - } - - let newNode = clone(node); - - const polyNode = new Node.PolynomialTerm(newNode.args[0]); - const constNode = newNode.args[1]; - const exponentNode = polyNode.getExponentNode(); - newNode = Node.Creator.polynomialTerm( - polyNode.getSymbolNode(), exponentNode, constNode); - - return Node.Status.nodeChanged( - ChangeTypes.REARRANGE_COEFF, node, newNode); -} - -module.exports = rearrangeCoefficient; diff --git a/lib/simplifyExpression/basicsSearch/reduceExponentByZero.js b/lib/simplifyExpression/basicsSearch/reduceExponentByZero.js deleted file mode 100644 index c24d43c6..00000000 --- a/lib/simplifyExpression/basicsSearch/reduceExponentByZero.js +++ /dev/null @@ -1,21 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is an exponent of something to 0, we can reduce that to just 1. -// Returns a Node.Status object. -function reduceExponentByZero(node) { - if (node.op !== '^') { - return Node.Status.noChange(node); - } - const exponent = node.args[1]; - if (Node.Type.isConstant(exponent) && exponent.value === '0') { - const newNode = Node.Creator.constant(1); - return Node.Status.nodeChanged( - ChangeTypes.REDUCE_EXPONENT_BY_ZERO, node, newNode); - } - else { - return Node.Status.noChange(node); - } -} - -module.exports = reduceExponentByZero; diff --git a/lib/simplifyExpression/basicsSearch/reduceMultiplicationByZero.js b/lib/simplifyExpression/basicsSearch/reduceMultiplicationByZero.js deleted file mode 100644 index b9bc4d57..00000000 --- a/lib/simplifyExpression/basicsSearch/reduceMultiplicationByZero.js +++ /dev/null @@ -1,31 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is a multiplication node with 0 as one of its operands, -// reduce the node to 0. Returns a Node.Status object. -function reduceMultiplicationByZero(node) { - if (node.op !== '*') { - return Node.Status.noChange(node); - } - const zeroIndex = node.args.findIndex(arg => { - if (Node.Type.isConstant(arg) && arg.value === '0') { - return true; - } - if (Node.PolynomialTerm.isPolynomialTerm(arg)) { - const polyTerm = new Node.PolynomialTerm(arg); - return polyTerm.getCoeffValue() === 0; - } - return false; - }); - if (zeroIndex >= 0) { - // reduce to just the 0 node - const newNode = Node.Creator.constant(0); - return Node.Status.nodeChanged( - ChangeTypes.MULTIPLY_BY_ZERO, node, newNode); - } - else { - return Node.Status.noChange(node); - } -} - -module.exports = reduceMultiplicationByZero; diff --git a/lib/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.js b/lib/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.js deleted file mode 100644 index b6c4aed3..00000000 --- a/lib/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.js +++ /dev/null @@ -1,20 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is a fraction with 0 as the numerator, reduce the node to 0. -// Returns a Node.Status object. -function reduceZeroDividedByAnything(node) { - if (node.op !== '/') { - return Node.Status.noChange(node); - } - if (node.args[0].value === '0') { - const newNode = Node.Creator.constant(0); - return Node.Status.nodeChanged( - ChangeTypes.REDUCE_ZERO_NUMERATOR, node, newNode); - } - else { - return Node.Status.noChange(node); - } -} - -module.exports = reduceZeroDividedByAnything; diff --git a/lib/simplifyExpression/basicsSearch/removeAdditionOfZero.js b/lib/simplifyExpression/basicsSearch/removeAdditionOfZero.js deleted file mode 100644 index 696c360b..00000000 --- a/lib/simplifyExpression/basicsSearch/removeAdditionOfZero.js +++ /dev/null @@ -1,30 +0,0 @@ -const clone = require('../../util/clone'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is an addition node with 0 as one of its operands, -// remove 0 from the operands list. Returns a Node.Status object. -function removeAdditionOfZero(node) { - if (node.op !== '+') { - return Node.Status.noChange(node); - } - const zeroIndex = node.args.findIndex(arg => { - return Node.Type.isConstant(arg) && arg.value === '0'; - }); - let newNode = clone(node); - if (zeroIndex >= 0) { - // remove the 0 node - newNode.args.splice(zeroIndex, 1); - // if there's only one operand left, there's nothing left to add it to, - // so move it up the tree - if (newNode.args.length === 1) { - newNode = newNode.args[0]; - } - return Node.Status.nodeChanged( - ChangeTypes.REMOVE_ADDING_ZERO, node, newNode); - } - return Node.Status.noChange(node); -} - -module.exports = removeAdditionOfZero; diff --git a/lib/simplifyExpression/basicsSearch/removeDivisionByOne.js b/lib/simplifyExpression/basicsSearch/removeDivisionByOne.js deleted file mode 100644 index 3cef9349..00000000 --- a/lib/simplifyExpression/basicsSearch/removeDivisionByOne.js +++ /dev/null @@ -1,42 +0,0 @@ -const clone = require('../../util/clone'); - -const ChangeTypes = require('../../ChangeTypes'); -const Negative = require('../../Negative'); -const Node = require('../../node'); - -// If `node` is a division operation of something by 1 or -1, we can remove the -// denominator. Returns a Node.Status object. -function removeDivisionByOne(node) { - if (node.op !== '/') { - return Node.Status.noChange(node); - } - const denominator = node.args[1]; - if (!Node.Type.isConstant(denominator)) { - return Node.Status.noChange(node); - } - let numerator = clone(node.args[0]); - - // if denominator is -1, we make the numerator negative - if (parseFloat(denominator.value) === -1) { - // If the numerator was an operation, wrap it in parens before adding - - // to the front. - // e.g. 2+3 / -1 ---> -(2+3) - if (Node.Type.isOperator(numerator)) { - numerator = Node.Creator.parenthesis(numerator); - } - const changeType = Negative.isNegative(numerator) ? - ChangeTypes.RESOLVE_DOUBLE_MINUS : - ChangeTypes.DIVISION_BY_NEGATIVE_ONE; - numerator = Negative.negate(numerator); - return Node.Status.nodeChanged(changeType, node, numerator); - } - else if (parseFloat(denominator.value) === 1) { - return Node.Status.nodeChanged( - ChangeTypes.DIVISION_BY_ONE, node, numerator); - } - else { - return Node.Status.noChange(node); - } -} - -module.exports = removeDivisionByOne; diff --git a/lib/simplifyExpression/basicsSearch/removeExponentBaseOne.js b/lib/simplifyExpression/basicsSearch/removeExponentBaseOne.js deleted file mode 100644 index e93595f2..00000000 --- a/lib/simplifyExpression/basicsSearch/removeExponentBaseOne.js +++ /dev/null @@ -1,21 +0,0 @@ -const checks = require('../../checks'); -const clone = require('../../util/clone'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is of the form 1^x, reduces it to a node of the form 1. -// Returns a Node.Status object. -function removeExponentBaseOne(node) { - if (node.op === '^' && // an exponent with - checks.resolvesToConstant(node.args[1]) && // a power not a symbol and - Node.Type.isConstant(node.args[0]) && // a constant base - node.args[0].value === '1') { // of value 1 - const newNode = clone(node.args[0]); - return Node.Status.nodeChanged( - ChangeTypes.REMOVE_EXPONENT_BASE_ONE, node, newNode); - } - return Node.Status.noChange(node); -} - -module.exports = removeExponentBaseOne; diff --git a/lib/simplifyExpression/basicsSearch/removeExponentByOne.js b/lib/simplifyExpression/basicsSearch/removeExponentByOne.js deleted file mode 100644 index 85a0400e..00000000 --- a/lib/simplifyExpression/basicsSearch/removeExponentByOne.js +++ /dev/null @@ -1,19 +0,0 @@ -const clone = require('../../util/clone'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is of the form x^1, reduces it to a node of the form x. -// Returns a Node.Status object. -function removeExponentByOne(node) { - if (node.op === '^' && // exponent of anything - Node.Type.isConstant(node.args[1]) && // to a constant - node.args[1].value === '1') { // of value 1 - const newNode = clone(node.args[0]); - return Node.Status.nodeChanged( - ChangeTypes.REMOVE_EXPONENT_BY_ONE, node, newNode); - } - return Node.Status.noChange(node); -} - -module.exports = removeExponentByOne; diff --git a/lib/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.js b/lib/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.js deleted file mode 100644 index 20d9df99..00000000 --- a/lib/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.js +++ /dev/null @@ -1,56 +0,0 @@ -const clone = require('../../util/clone'); - -const ChangeTypes = require('../../ChangeTypes'); -const Negative = require('../../Negative'); -const Node = require('../../node'); - -// If `node` is a multiplication node with -1 as one of its operands, -// and a non constant as the next operand, remove -1 from the operands -// list and make the next term have a unary minus. -// Returns a Node.Status object. -function removeMultiplicationByNegativeOne(node) { - if (node.op !== '*') { - return Node.Status.noChange(node); - } - const minusOneIndex = node.args.findIndex(arg => { - return Node.Type.isConstant(arg) && arg.value === '-1'; - }); - if (minusOneIndex < 0) { - return Node.Status.noChange(node); - } - - // We might merge/combine the negative one into another node. This stores - // the index of that other node in the arg list. - let nodeToCombineIndex; - // If minus one is the last term, maybe combine with the term before - if (minusOneIndex + 1 === node.args.length) { - nodeToCombineIndex = minusOneIndex - 1; - } - else { - nodeToCombineIndex = minusOneIndex + 1; - } - - let nodeToCombine = node.args[nodeToCombineIndex]; - // If it's a constant, the combining of those terms is handled elsewhere. - if (Node.Type.isConstant(nodeToCombine)) { - return Node.Status.noChange(node); - } - - let newNode = clone(node); - - // Get rid of the -1 - nodeToCombine = Negative.negate(clone(nodeToCombine)); - - // replace the node next to -1 and remove -1 - newNode.args[nodeToCombineIndex] = nodeToCombine; - newNode.args.splice(minusOneIndex, 1); - - // if there's only one operand left, move it up the tree - if (newNode.args.length === 1) { - newNode = newNode.args[0]; - } - return Node.Status.nodeChanged( - ChangeTypes.REMOVE_MULTIPLYING_BY_NEGATIVE_ONE, node, newNode); -} - -module.exports = removeMultiplicationByNegativeOne; diff --git a/lib/simplifyExpression/basicsSearch/removeMultiplicationByOne.js b/lib/simplifyExpression/basicsSearch/removeMultiplicationByOne.js deleted file mode 100644 index be05c435..00000000 --- a/lib/simplifyExpression/basicsSearch/removeMultiplicationByOne.js +++ /dev/null @@ -1,30 +0,0 @@ -const clone = require('../../util/clone'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is a multiplication node with 1 as one of its operands, -// remove 1 from the operands list. Returns a Node.Status object. -function removeMultiplicationByOne(node) { - if (node.op !== '*') { - return Node.Status.noChange(node); - } - const oneIndex = node.args.findIndex(arg => { - return Node.Type.isConstant(arg) && arg.value === '1'; - }); - if (oneIndex >= 0) { - let newNode = clone(node); - // remove the 1 node - newNode.args.splice(oneIndex, 1); - // if there's only one operand left, there's nothing left to multiply it - // to, so move it up the tree - if (newNode.args.length === 1) { - newNode = newNode.args[0]; - } - return Node.Status.nodeChanged( - ChangeTypes.REMOVE_MULTIPLYING_BY_ONE, node, newNode); - } - return Node.Status.noChange(node); -} - -module.exports = removeMultiplicationByOne; diff --git a/lib/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.js b/lib/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.js deleted file mode 100644 index 749d050a..00000000 --- a/lib/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.js +++ /dev/null @@ -1,38 +0,0 @@ -const clone = require('../../util/clone'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// Simplifies two unary minuses in a row by removing both of them. -// e.g. -(- 4) --> 4 -function simplifyDoubleUnaryMinus(node) { - if (!Node.Type.isUnaryMinus(node)) { - return Node.Status.noChange(node); - } - const unaryArg = node.args[0]; - // e.g. in - -x, -x is the unary arg, and we'd want to reduce to just x - if (Node.Type.isUnaryMinus(unaryArg)) { - const newNode = clone(unaryArg.args[0]); - return Node.Status.nodeChanged( - ChangeTypes.RESOLVE_DOUBLE_MINUS, node, newNode); - } - // e.g. - -4, -4 could be a constant with negative value - else if (Node.Type.isConstant(unaryArg) && parseFloat(unaryArg.value) < 0) { - const newNode = Node.Creator.constant(parseFloat(unaryArg.value) * -1); - return Node.Status.nodeChanged( - ChangeTypes.RESOLVE_DOUBLE_MINUS, node, newNode); - } - // e.g. -(-(5+2)) - else if (Node.Type.isParenthesis(unaryArg)) { - const parenthesisNode = unaryArg; - const parenthesisContent = parenthesisNode.content; - if (Node.Type.isUnaryMinus(parenthesisContent)) { - const newNode = Node.Creator.parenthesis(parenthesisContent.args[0]); - return Node.Status.nodeChanged( - ChangeTypes.RESOLVE_DOUBLE_MINUS, node, newNode); - } - } - return Node.Status.noChange(node); -} - -module.exports = simplifyDoubleUnaryMinus; diff --git a/lib/simplifyExpression/breakUpNumeratorSearch/index.js b/lib/simplifyExpression/breakUpNumeratorSearch/index.js deleted file mode 100644 index 2dcff7a2..00000000 --- a/lib/simplifyExpression/breakUpNumeratorSearch/index.js +++ /dev/null @@ -1,45 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -// Breaks up any fraction (deeper nodes getting priority) that has a numerator -// that is a sum. e.g. (2+x)/5 -> (2/5 + x/5) -// This step must happen after things have been collected and combined, or -// else things will infinite loop, so it's a tree search of its own. -// Returns a Node.Status object -const search = TreeSearch.postOrder(breakUpNumerator); - -// If `node` is a fraction with a numerator that is a sum, breaks up the -// fraction e.g. (2+x)/5 -> (2/5 + x/5) -// Returns a Node.Status object -function breakUpNumerator(node) { - if (!Node.Type.isOperator(node) || node.op !== '/') { - return Node.Status.noChange(node); - } - let numerator = node.args[0]; - if (Node.Type.isParenthesis(numerator)) { - numerator = numerator.content; - } - if (!Node.Type.isOperator(numerator) || numerator.op !== '+') { - return Node.Status.noChange(node); - } - - // At this point, we know that node is a fraction and its numerator is a sum - // of terms that can't be collected or combined, so we should break it up. - const fractionList = []; - const denominator = node.args[1]; - numerator.args.forEach(arg => { - const newFraction = Node.Creator.operator('/', [arg, denominator]); - newFraction.changeGroup = 1; - fractionList.push(newFraction); - }); - - let newNode = Node.Creator.operator('+', fractionList); - // Wrap in parens for cases like 2*(2+3)/5 => 2*(2/5 + 3/5) - newNode = Node.Creator.parenthesis(newNode); - node.changeGroup = 1; - return Node.Status.nodeChanged( - ChangeTypes.BREAK_UP_FRACTION, node, newNode, false); -} - -module.exports = search; diff --git a/lib/simplifyExpression/collectAndCombineSearch/LikeTermCollector.js b/lib/simplifyExpression/collectAndCombineSearch/LikeTermCollector.js deleted file mode 100644 index e9ec462f..00000000 --- a/lib/simplifyExpression/collectAndCombineSearch/LikeTermCollector.js +++ /dev/null @@ -1,274 +0,0 @@ -const clone = require('../../util/clone'); -const print = require('../../util/print'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); -const Util = require('../../util/Util'); - -const CONSTANT = 'constant'; -const CONSTANT_FRACTION = 'constantFraction'; -const OTHER = 'other'; - -const LikeTermCollector = {}; - -// Given an expression tree, returns true if there are terms that can be -// collected -LikeTermCollector.canCollectLikeTerms = function(node) { - // We can collect like terms through + or through * - // Note that we never collect like terms with - or /, those expressions will - // always be manipulated in flattenOperands so that the top level operation is - // + or *. - if (!(Node.Type.isOperator(node, '+') || Node.Type.isOperator(node, '*'))) { - return false; - } - - let terms; - if (node.op === '+') { - terms = getTermsForCollectingAddition(node); - } - else if (node.op === '*') { - terms = getTermsForCollectingMultiplication(node); - } - else { - throw Error('Operation not supported: ' + node.op); - } - - // Conditions we need to meet to decide to to reorganize (collect) the terms: - // - more than 1 term type - // - more than 1 of at least one type (not including other) - // (note that this means x^2 + x + x + 2 -> x^2 + (x + x) + 2, - // which will be recorded as a step, but doesn't change the order of terms) - const termTypes = Object.keys(terms); - const filteredTermTypes = termTypes.filter(x => x !== OTHER); - return (termTypes.length > 1 && - filteredTermTypes.some(x => terms[x].length > 1)); -}; - -// Collects like terms for an operation node and returns a Node.Status object. -LikeTermCollector.collectLikeTerms = function(node) { - if (!LikeTermCollector.canCollectLikeTerms(node)) { - return Node.Status.noChange(node); - } - - const op = node.op; - let terms = []; - if (op === '+') { - terms = getTermsForCollectingAddition(node); - } - else if (op === '*') { - terms = getTermsForCollectingMultiplication(node); - } - else { - throw Error('Operation not supported: ' + op); - } - - // List the symbols alphabetically - const termTypesSorted = Object.keys(terms) - .filter(x => (x !== CONSTANT && x !== CONSTANT_FRACTION && x !== OTHER)) - .sort(sortTerms); - - - // Then add const - if (terms[CONSTANT]) { - // at the end for addition (since we'd expect x^2 + (x + x) + 4) - if (op === '+') { - termTypesSorted.push(CONSTANT); - } - // for multipliation it should be at the front (e.g. (3*4) * x^2) - if (op === '*') { - termTypesSorted.unshift(CONSTANT); - } - } - if (terms[CONSTANT_FRACTION]) { - termTypesSorted.push(CONSTANT_FRACTION); - } - - // Collect the new operands under op. - let newOperands = []; - let changeGroup = 1; - termTypesSorted.forEach(termType => { - const termsOfType = terms[termType]; - if (termsOfType.length === 1) { - const singleTerm = clone(termsOfType[0]); - singleTerm.changeGroup = changeGroup; - newOperands.push(singleTerm); - } - // Any like terms should be wrapped in parens. - else { - const termList = clone(Node.Creator.parenthesis( - Node.Creator.operator(op, termsOfType))); - termList.changeGroup = changeGroup; - newOperands.push(termList); - } - termsOfType.forEach(term => { - term.changeGroup = changeGroup; - }); - changeGroup++; - }); - - // then stick anything else (paren nodes, operator nodes) at the end - if (terms[OTHER]) { - newOperands = newOperands.concat(terms[OTHER]); - } - - const newNode = clone(node); - newNode.args = newOperands; - return Node.Status.nodeChanged( - ChangeTypes.COLLECT_LIKE_TERMS, node, newNode, false); -}; - -// Polyonomial terms are collected by categorizing them by their 'name' -// which is used to separate them into groups that can be combined. getTermName -// returns this group 'name' -function getTermName(node, op) { - const polyNode = new Node.PolynomialTerm(node); - // we 'name' polynomial terms by their symbol name - let termName = polyNode.getSymbolName(); - // when adding terms, the exponent matters too (e.g. 2x^2 + 5x^3 can't be combined) - if (op === '+') { - const exponent = print(polyNode.getExponentNode(true)); - termName += '^' + exponent; - } - return termName; -} - -// Collects like terms in an addition expression tree into categories. -// Returns a dictionary of termname to lists of nodes with that name -// e.g. 2x + 4 + 5x would return {'x': [2x, 5x], CONSTANT: [4]} -// (where 2x, 5x, and 4 would actually be expression trees) -function getTermsForCollectingAddition(node) { - let terms = {}; - - for (let i = 0; i < node.args.length; i++) { - const child = node.args[i]; - - if (Node.PolynomialTerm.isPolynomialTerm(child)) { - const termName = getTermName(child, '+'); - terms = Util.appendToArrayInObject(terms, termName, child); - } - else if (Node.Type.isIntegerFraction(child)) { - terms = Util.appendToArrayInObject(terms, CONSTANT_FRACTION, child); - } - else if (Node.Type.isConstant(child)) { - terms = Util.appendToArrayInObject(terms, CONSTANT, child); - } - else if (Node.Type.isOperator(node) || - Node.Type.isFunction(node) || - Node.Type.isParenthesis(node) || - Node.Type.isUnaryMinus(node)) { - terms = Util.appendToArrayInObject(terms, OTHER, child); - } - else { - // Note that we shouldn't get any symbol nodes in the switch statement - // since they would have been handled by isPolynomialTerm - throw Error('Unsupported node type: ' + child.type); - } - } - // If there's exactly one constant and one fraction, we collect them - // to add them together. - // e.g. 2 + 1/3 + 5 would collect the constants (2+5) + 1/3 - // but 2 + 1/3 + x would collect (2 + 1/3) + x so we can add them together - if (terms[CONSTANT] && terms[CONSTANT].length === 1 && - terms[CONSTANT_FRACTION] && terms[CONSTANT_FRACTION].length === 1) { - const fraction = terms[CONSTANT_FRACTION][0]; - terms = Util.appendToArrayInObject(terms, CONSTANT, fraction); - delete terms[CONSTANT_FRACTION]; - } - - return terms; -} - -// Collects like terms in a multiplication expression tree into categories. -// For multiplication, polynomial terms with constants are separated into -// a symbolic term and a constant term. -// Returns a dictionary of termname to lists of nodes with that name -// e.g. 2x + 4 + 5x^2 would return {'x': [x, x^2], CONSTANT: [2, 4, 5]} -// (where x, x^2, 2, 4, and 5 would actually be expression trees) -function getTermsForCollectingMultiplication(node) { - let terms = {}; - - for (let i = 0; i < node.args.length; i++) { - let child = node.args[i]; - - if (Node.Type.isUnaryMinus(child)) { - terms = Util.appendToArrayInObject( - terms, CONSTANT, Node.Creator.constant(-1)); - child = child.args[0]; - } - if (Node.PolynomialTerm.isPolynomialTerm(child)) { - terms = addToTermsforPolynomialMultiplication(terms, child); - } - else if (Node.Type.isIntegerFraction(child)) { - terms = Util.appendToArrayInObject(terms, CONSTANT, child); - } - else if (Node.Type.isConstant(child)) { - terms = Util.appendToArrayInObject(terms, CONSTANT, child); - } - else if (Node.Type.isOperator(node) || - Node.Type.isFunction(node) || - Node.Type.isParenthesis(node) || - Node.Type.isUnaryMinus(node)) { - terms = Util.appendToArrayInObject(terms, OTHER, child); - } - else { - // Note that we shouldn't get any symbol nodes in the switch statement - // since they would have been handled by isPolynomialTerm - throw Error('Unsupported node type: ' + child.type); - } - } - return terms; -} - -// A helper function for getTermsForCollectingMultiplication -// Polynomial terms need to be divided into their coefficient + symbolic parts. -// e.g. 2x^4 -> 2 (coeffient) and x^4 (symbolic, named after the symbol node) -// Takes the terms list and the polynomial term node, and returns an updated -// terms list. -function addToTermsforPolynomialMultiplication(terms, node) { - const polyNode = new Node.PolynomialTerm(node); - let termName; - - if (!polyNode.hasCoeff()) { - termName = getTermName(node, '*'); - terms = Util.appendToArrayInObject(terms, termName, node); - } - else { - const coefficient = polyNode.getCoeffNode(); - let termWithoutCoefficient = polyNode.getSymbolNode(); - if (polyNode.getExponentNode()) { - termWithoutCoefficient = Node.Creator.operator( - '^', [termWithoutCoefficient, polyNode.getExponentNode()]); - } - - terms = Util.appendToArrayInObject(terms, CONSTANT, coefficient); - termName = getTermName(termWithoutCoefficient, '*'); - terms = Util.appendToArrayInObject(terms, termName, termWithoutCoefficient); - } - return terms; -} - -// Sort function for termnames. Sort first by symbol name, and then by exponent. -function sortTerms(a, b) { - if (a === b) { - return 0; - } - // if no exponent, sort alphabetically - if (a.indexOf('^') === -1) { - return a < b ? -1 : 1; - } - // if exponent: sort by symbol, but then exponent decreasing - else { - const symbA = a.split('^')[0]; - const expA = a.split('^')[1]; - const symbB = b.split('^')[0]; - const expB = b.split('^')[1]; - if (symbA !== symbB) { - return symbA < symbB ? -1 : 1; - } - else { - return expA > expB ? -1 : 1; - } - } -} - -module.exports = LikeTermCollector; diff --git a/lib/simplifyExpression/collectAndCombineSearch/addLikeTerms.js b/lib/simplifyExpression/collectAndCombineSearch/addLikeTerms.js deleted file mode 100644 index 1e1b39a3..00000000 --- a/lib/simplifyExpression/collectAndCombineSearch/addLikeTerms.js +++ /dev/null @@ -1,182 +0,0 @@ -const checks = require('../../checks'); -const clone = require('../../util/clone'); -const evaluateConstantSum = require('./evaluateConstantSum'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// Adds a list of nodes that are polynomial terms. Returns a Node.Status object. -function addLikeTerms(node, polynomialOnly=false) { - if (!Node.Type.isOperator(node)) { - return Node.Status.noChange(node); - } - let status; - - if (!polynomialOnly) { - status = evaluateConstantSum(node); - if (status.hasChanged()) { - return status; - } - } - - status = addLikePolynomialTerms(node); - if (status.hasChanged()) { - return status; - } - - return Node.Status.noChange(node); -} - -function addLikePolynomialTerms(node) { - if (!checks.canAddLikeTermPolynomialNodes(node)) { - return Node.Status.noChange(node); - } - - const substeps = []; - let newNode = clone(node); - - // STEP 1: If any nodes have no coefficient, make it have coefficient 1 - // (this step only happens under certain conditions and later steps might - // happen even if step 1 does not) - let status = addPositiveOneCoefficient(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // STEP 2: If any nodes have a unary minus, make it have coefficient -1 - // (this step only happens under certain conditions and later steps might - // happen even if step 2 does not) - status = addNegativeOneCoefficient(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // STEP 3: group the coefficients in a sum - status = groupCoefficientsForAdding(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // STEP 4: evaluate the sum (could include fractions) - status = evaluateCoefficientSum(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - return Node.Status.nodeChanged( - ChangeTypes.ADD_POLYNOMIAL_TERMS, - node, newNode, true, substeps); -} - -// Given a sum of like polynomial terms, changes any term with no coefficient -// into a term with an explicit coefficient of 1. This is for pedagogy, and -// makes the adding coefficients step clearer. -// e.g. 2x + x -> 2x + 1x -// Returns a Node.Status object. -function addPositiveOneCoefficient(node) { - const newNode = clone(node, false); - let change = false; - - let changeGroup = 1; - newNode.args.forEach((child, i) => { - const polyTerm = new Node.PolynomialTerm(child); - if (polyTerm.getCoeffValue() === 1) { - newNode.args[i] = Node.Creator.polynomialTerm( - polyTerm.getSymbolNode(), - polyTerm.getExponentNode(), - Node.Creator.constant(1), - true /* explicit coefficient */); - - newNode.args[i].changeGroup = changeGroup; - node.args[i].changeGroup = changeGroup; // note that this is the "oldNode" - - change = true; - changeGroup++; - } - }); - - if (change) { - return Node.Status.nodeChanged( - ChangeTypes.ADD_COEFFICIENT_OF_ONE, node, newNode, false); - } - else { - return Node.Status.noChange(node); - } -} - -// Given a sum of like polynomial terms, changes any term with a unary minus -// coefficient into a term with an explicit coefficient of -1. This is for -// pedagogy, and makes the adding coefficients step clearer. -// e.g. 2x - x -> 2x - 1x -// Returns a Node.Status object. -function addNegativeOneCoefficient(node) { - const newNode = clone(node); - let change = false; - - let changeGroup = 1; - newNode.args.forEach((child, i) => { - const polyTerm = new Node.PolynomialTerm(child); - if (polyTerm.getCoeffValue() === -1) { - newNode.args[i] = Node.Creator.polynomialTerm( - polyTerm.getSymbolNode(), - polyTerm.getExponentNode(), - polyTerm.getCoeffNode(), - true /* explicit -1 coefficient */); - - node.args[i].changeGroup = changeGroup; // note that this is the "oldNode" - newNode.args[i].changeGroup = changeGroup; - - change = true; - changeGroup++; - } - }); - - if (change) { - return Node.Status.nodeChanged( - ChangeTypes.UNARY_MINUS_TO_NEGATIVE_ONE, node, newNode, false); - } - else { - return Node.Status.noChange(node); - } -} - -// Given a sum of like polynomial terms, groups the coefficients -// e.g. 2x^2 + 3x^2 + 5x^2 -> (2+3+5)x^2 -// Returns a Node.Status object. -function groupCoefficientsForAdding(node) { - let newNode = clone(node); - - const polynomialTermList = newNode.args.map(n => new Node.PolynomialTerm(n)); - const coefficientList = polynomialTermList.map(p => p.getCoeffNode(true)); - const sumOfCoefficents = Node.Creator.parenthesis( - Node.Creator.operator('+', coefficientList)); - // TODO: changegroups should also be on the before node, on all the - // coefficients, but changegroups with polyTerm gets messy so let's tackle - // that later. - sumOfCoefficents.changeGroup = 1; - - // Polynomial terms that can be added together must share the same symbol - // name and exponent. Get that name and exponent from the first term - const firstTerm = polynomialTermList[0]; - const exponentNode = firstTerm.getExponentNode(); - const symbolNode = firstTerm.getSymbolNode(); - newNode = Node.Creator.polynomialTerm( - symbolNode, exponentNode, sumOfCoefficents); - - return Node.Status.nodeChanged( - ChangeTypes.GROUP_COEFFICIENTS, node, newNode); -} - -// Given a node of the form (2 + 4 + 5)x -- ie the coefficients have been -// grouped for adding -- add the coefficients together to make a new coeffient -// that is a constant or constant fraction. -function evaluateCoefficientSum(node) { - // the node is now always a * node with the left child the coefficent sum - // e.g. (2 + 4 + 5) and the right node the symbol part e.g. x or y^2 - // so we want to evaluate args[0] - const coefficientSum = clone(node).args[0]; - const childStatus = evaluateConstantSum(coefficientSum); - return Node.Status.childChanged(node, childStatus, 0); -} - -module.exports = addLikeTerms; diff --git a/lib/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.js b/lib/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.js deleted file mode 100644 index e2c517b9..00000000 --- a/lib/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.js +++ /dev/null @@ -1,132 +0,0 @@ -const addConstantAndFraction = require('../fractionsSearch/addConstantAndFraction'); -const addConstantFractions = require('../fractionsSearch/addConstantFractions'); -const arithmeticSearch = require('../arithmeticSearch'); -const clone = require('../../util/clone'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// Evaluates a sum of constant numbers and integer fractions to a single -// constant number or integer fraction. e.g. e.g. 2/3 + 5 + 5/2 => 49/6 -// Returns a Node.Status object. -function evaluateConstantSum(node) { - if (Node.Type.isParenthesis(node)) { - node = node.content; - } - if (!Node.Type.isOperator(node) || node.op !== '+') { - return Node.Status.noChange(node); - } - if (node.args.some(node => !Node.Type.isConstantOrConstantFraction(node))) { - return Node.Status.noChange(node); - } - - // functions needed to evaluate the sum - const summingFunctions = [ - arithmeticSearch, - addConstantFractions, - addConstantAndFraction, - ]; - for (let i = 0; i < summingFunctions.length; i++) { - const status = summingFunctions[i](node); - if (status.hasChanged()) { - if (Node.Type.isConstantOrConstantFraction(status.newNode)) { - return status; - } - } - } - - let newNode = clone(node); - const substeps = []; - let status; - - // STEP 1: group fractions and constants separately - status = groupConstantsAndFractions(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - const constants = newNode.args[0]; - const fractions = newNode.args[1]; - - // STEP 2A: evaluate arithmetic IF there's > 1 constant - // (which is the case if it's a list surrounded by parenthesis) - if (Node.Type.isParenthesis(constants)) { - const constantList = constants.content; - const evaluateStatus = arithmeticSearch(constantList); - status = Node.Status.childChanged(newNode, evaluateStatus, 0); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // STEP 2B: add fractions IF there's > 1 fraction - // (which is the case if it's a list surrounded by parenthesis) - if (Node.Type.isParenthesis(fractions)) { - const fractionList = fractions.content; - const evaluateStatus = addConstantFractions(fractionList); - status = Node.Status.childChanged(newNode, evaluateStatus, 1); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // STEP 3: combine the evaluated constant and fraction - // the fraction might have simplified to a constant (e.g. 1/3 + 2/3 -> 2) - // so we just call evaluateConstantSum again to cycle through - status = evaluateConstantSum(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode, true, substeps); -} - -// If we can't combine using one of those functions, there's a mix of > 2 -// fractions and constants. So we need to group them together so we can later -// add them. -// Expects a node that is a sum of integer fractions and constants. -// Returns a Node.Status object. -// e.g. 2/3 + 5 + 5/2 => (2/3 + 5/2) + 5 -function groupConstantsAndFractions(node) { - let fractions = node.args.filter(Node.Type.isIntegerFraction); - let constants = node.args.filter(Node.Type.isConstant); - - if (fractions.length === 0 || constants.length === 0) { - throw Error('expected both integer fractions and constants, got ' + node); - } - - if (fractions.length + constants.length !== node.args.length) { - throw Error('can only evaluate integer fractions and constants'); - } - - constants = constants.map(node => { - // set the changeGroup - this affects both the old and new node - node.changeGroup = 1; - // clone so that node and newNode aren't stored in the same memory - return clone(node); - }); - // wrap in parenthesis if there's more than one, to group them - if (constants.length > 1) { - constants = Node.Creator.parenthesis(Node.Creator.operator('+', constants)); - } - else { - constants = constants[0]; - } - - fractions = fractions.map(node => { - // set the changeGroup - this affects both the old and new node - node.changeGroup = 2; - // clone so that node and newNode aren't stored in the same memory - return clone(node); - }); - // wrap in parenthesis if there's more than one, to group them - if (fractions.length > 1) { - fractions = Node.Creator.parenthesis(Node.Creator.operator('+', fractions)); - } - else { - fractions = fractions[0]; - } - - const newNode = Node.Creator.operator('+', [constants, fractions]); - return Node.Status.nodeChanged( - ChangeTypes.COLLECT_LIKE_TERMS, node, newNode); -} - -module.exports = evaluateConstantSum; diff --git a/lib/simplifyExpression/collectAndCombineSearch/index.js b/lib/simplifyExpression/collectAndCombineSearch/index.js deleted file mode 100644 index 33ddb342..00000000 --- a/lib/simplifyExpression/collectAndCombineSearch/index.js +++ /dev/null @@ -1,106 +0,0 @@ -// Collects and combines like terms - -const addLikeTerms = require('./addLikeTerms'); -const clone = require('../../util/clone'); -const multiplyLikeTerms = require('./multiplyLikeTerms'); - -const ChangeTypes = require('../../ChangeTypes'); -const LikeTermCollector = require('./LikeTermCollector'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -const termCollectorFunctions = { - '+': addLikeTerms, - '*': multiplyLikeTerms -}; - -// Iterates through the tree looking for like terms to collect and combine. -// Will prioritize deeper expressions. Returns a Node.Status object. -const search = TreeSearch.postOrder(collectAndCombineLikeTerms); - -// Given an operator node, maybe collects and then combines if possible -// e.g. 2x + 4x + y => 6x + y -// e.g. 2x * x^2 * 5x => 10 x^4 -function collectAndCombineLikeTerms(node) { - if (node.op === '+') { - const status = collectAndCombineOperation(node); - if (status.hasChanged()) { - return status; - } - // we might also be able to just combine if they're all the same term - // e.g. 2x + 4x + x (doesn't need collecting) - return addLikeTerms(node, true); - } - else if (node.op === '*') { - // collect and combine involves there being coefficients pulled the front - // e.g. 2x * x^2 * 5x => (2*5) * (x * x^2 * x) => ... => 10 x^4 - const status = collectAndCombineOperation(node); - if (status.hasChanged()) { - // make sure there's no * between the coefficient and the symbol part - status.newNode.implicit = true; - return status; - } - // we might also be able to just combine polynomial terms - // e.g. x * x^2 * x => ... => x^4 - return multiplyLikeTerms(node, true); - } - else { - return Node.Status.noChange(node); - } -} - -// Collects and combines (if possible) the arguments of an addition or -// multiplication -function collectAndCombineOperation(node) { - let substeps = []; - - const status = LikeTermCollector.collectLikeTerms(clone(node)); - if (!status.hasChanged()) { - return status; - } - - // STEP 1: collect like terms, e.g. 2x + 4x^2 + 5x => 4x^2 + (2x + 5x) - substeps.push(status); - let newNode = Node.Status.resetChangeGroups(status.newNode); - - // STEP 2 onwards: combine like terms for each group that can be combined - // e.g. (x + 3x) + (2 + 2) has two groups - const combineSteps = combineLikeTerms(newNode); - if (combineSteps.length > 0) { - substeps = substeps.concat(combineSteps); - const lastStep = combineSteps[combineSteps.length - 1]; - newNode = Node.Status.resetChangeGroups(lastStep.newNode); - } - - return Node.Status.nodeChanged( - ChangeTypes.COLLECT_AND_COMBINE_LIKE_TERMS, - node, newNode, true, substeps); -} - -// step 2 onwards for collectAndCombineOperation -// combine like terms for each group that can be combined -// e.g. (x + 3x) + (2 + 2) has two groups -// returns a list of combine steps -function combineLikeTerms(node) { - const steps = []; - let newNode = clone(node); - - for (let i = 0; i < node.args.length; i++) { - let child = node.args[i]; - // All groups of terms will be surrounded by parenthesis - if (!Node.Type.isParenthesis(child)) { - continue; - } - child = child.content; - const childStatus = termCollectorFunctions[newNode.op](child); - if (childStatus.hasChanged()) { - const status = Node.Status.childChanged(newNode, childStatus, i); - steps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - } - - return steps; -} - -module.exports = search; diff --git a/lib/simplifyExpression/collectAndCombineSearch/multiplyLikeTerms.js b/lib/simplifyExpression/collectAndCombineSearch/multiplyLikeTerms.js deleted file mode 100644 index 079d3b17..00000000 --- a/lib/simplifyExpression/collectAndCombineSearch/multiplyLikeTerms.js +++ /dev/null @@ -1,143 +0,0 @@ -const arithmeticSearch = require('../arithmeticSearch'); -const checks = require('../../checks'); -const clone = require('../../util/clone'); -const multiplyFractionsSearch = require('../multiplyFractionsSearch'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// Multiplies a list of nodes that are polynomial like terms. Returns a node. -// The polynomial nodes should *not* have coefficients. (multiplying -// coefficients is handled in collecting like terms for multiplication) -function multiplyLikeTerms(node, polynomialOnly=false) { - if (!Node.Type.isOperator(node)) { - return Node.Status.noChange(node); - } - let status; - - if (!polynomialOnly) { - status = arithmeticSearch(node); - if (status.hasChanged()) { - status.changeType = ChangeTypes.MULTIPLY_COEFFICIENTS; - return status; - } - - status = multiplyFractionsSearch(node); - if (status.hasChanged()) { - status.changeType = ChangeTypes.MULTIPLY_COEFFICIENTS; - return status; - } - } - - status = multiplyPolynomialTerms(node); - if (status.hasChanged()) { - status.changeType = ChangeTypes.MULTIPLY_COEFFICIENTS; - return status; - } - - return Node.Status.noChange(node); -} - -function multiplyPolynomialTerms(node) { - if (!checks.canMultiplyLikeTermPolynomialNodes(node)) { - return Node.Status.noChange(node); - } - - const substeps = []; - let newNode = clone(node); - - // STEP 1: If any term has no exponent, make it have exponent 1 - // e.g. x -> x^1 (this is for pedagogy reasons) - // (this step only happens under certain conditions and later steps might - // happen even if step 1 does not) - let status = addOneExponent(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // STEP 2: collect exponents to a single exponent sum - // e.g. x^1 * x^3 -> x^(1+3) - status = collectExponents(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // STEP 3: add exponents together. - // NOTE: This might not be a step if the exponents aren't all constants, - // but this case isn't that common and can be caught in other steps. - // e.g. x^(2+4+z) - // TODO: handle fractions, combining and collecting like terms, etc, here - const exponentSum = newNode.args[1].content; - const sumStatus = arithmeticSearch(exponentSum); - if (sumStatus.hasChanged()) { - status = Node.Status.childChanged(newNode, sumStatus, 1); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - if (substeps.length === 1) { // possible if only step 2 happens - return substeps[0]; - } - else { - return Node.Status.nodeChanged( - ChangeTypes.MULTIPLY_POLYNOMIAL_TERMS, - node, newNode, true, substeps); - } -} - -// Given a product of polynomial terms, changes any term with no exponent -// into a term with an explicit exponent of 1. This is for pedagogy, and -// makes the adding coefficients step clearer. -// e.g. x^2 * x -> x^2 * x^1 -// Returns a Node.Status object. -function addOneExponent(node) { - const newNode = clone(node); - let change = false; - - let changeGroup = 1; - newNode.args.forEach((child, i) => { - const polyTerm = new Node.PolynomialTerm(child); - if (!polyTerm.getExponentNode()) { - newNode.args[i] = Node.Creator.polynomialTerm( - polyTerm.getSymbolNode(), - Node.Creator.constant(1), - polyTerm.getCoeffNode()); - - newNode.args[i].changeGroup = changeGroup; - node.args[i].changeGroup = changeGroup; // note that this is the "oldNode" - - change = true; - changeGroup++; - } - }); - - if (change) { - return Node.Status.nodeChanged( - ChangeTypes.ADD_EXPONENT_OF_ONE, node, newNode, false); - } - else { - return Node.Status.noChange(node); - } -} - -// Given a product of polynomial terms, groups the exponents into a sum -// e.g. x^2 * x^3 * x^1 -> x^(2 + 3 + 1) -// Returns a Node.Status object. -function collectExponents(node) { - const polynomialTermList = node.args.map(n => new Node.PolynomialTerm(n)); - - // If we're multiplying polynomial nodes together, they all share the same - // symbol. Get that from the first node. - const symbolNode = polynomialTermList[0].getSymbolNode(); - - // The new exponent will be a sum of exponents (an operation, wrapped in - // parens) e.g. x^(3+4+5) - const exponentNodeList = polynomialTermList.map(p => p.getExponentNode(true)); - const newExponent = Node.Creator.parenthesis( - Node.Creator.operator('+', exponentNodeList)); - const newNode = Node.Creator.polynomialTerm(symbolNode, newExponent, null); - return Node.Status.nodeChanged( - ChangeTypes.COLLECT_EXPONENTS, node, newNode); -} - -module.exports = multiplyLikeTerms; diff --git a/lib/simplifyExpression/distributeSearch/index.js b/lib/simplifyExpression/distributeSearch/index.js deleted file mode 100644 index c63a597f..00000000 --- a/lib/simplifyExpression/distributeSearch/index.js +++ /dev/null @@ -1,292 +0,0 @@ -const arithmeticSearch = require('../arithmeticSearch'); -const clone = require('../../util/clone'); -const collectAndCombineSearch = require('../collectAndCombineSearch'); -const rearrangeCoefficient = require('../basicsSearch/rearrangeCoefficient'); - -const ChangeTypes = require('../../ChangeTypes'); -const Negative = require('../../Negative'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -const search = TreeSearch.postOrder(distribute); - -// Distributes through parenthesis. -// e.g. 2(x+3) -> (2*x + 2*3) -// e.g. -(x+5) -> (-x + -5) -// Returns a Node.Status object. -function distribute(node) { - if (Node.Type.isUnaryMinus(node)) { - return distributeUnaryMinus(node); - } - else if (Node.Type.isOperator(node)) { - return distributeAndSimplifyOperationNode(node); - } - else { - return Node.Status.noChange(node); - } -} - -// Distributes unary minus into a parenthesis node. -// e.g. -(4*9*x^2) --> (-4 * 9 * x^2) -// e.g. -(x + y - 5) --> (-x + -y + 5) -// Returns a Node.Status object. -function distributeUnaryMinus(node) { - if (!Node.Type.isUnaryMinus(node)) { - return Node.Status.noChange(node); - } - const unaryContent = node.args[0]; - if (!Node.Type.isParenthesis(unaryContent)) { - return Node.Status.noChange(node); - } - const content = unaryContent.content; - if (!Node.Type.isOperator(content)) { - return Node.Status.noChange(node); - } - const newContent = clone(content); - node.changeGroup = 1; - // For multiplication and division, we can push the unary minus in to - // the first argument. - // e.g. -(2/3) -> (-2/3) -(4*9*x^2) --> (-4 * 9 * x^2) - if (content.op === '*' || content.op === '/') { - newContent.args[0] = Negative.negate(newContent.args[0]); - newContent.args[0].changeGroup = 1; - const newNode = Node.Creator.parenthesis(newContent); - return Node.Status.nodeChanged( - ChangeTypes.DISTRIBUTE_NEGATIVE_ONE, node, newNode, false); - } - else if (content.op === '+') { - // Now we know `node` is of the form -(x + y + ...). - // We want to now return (-x + -y + ....) - // If any term is negative, we make it positive it right away - // e.g. -(2-4) => -2 + 4 - const newArgs = newContent.args.map(arg => { - const newArg = Negative.negate(arg); - newArg.changeGroup = 1; - return newArg; - }); - newContent.args = newArgs; - const newNode = Node.Creator.parenthesis(newContent); - return Node.Status.nodeChanged( - ChangeTypes.DISTRIBUTE_NEGATIVE_ONE, node, newNode, false); - } - else { - return Node.Status.noChange(node); - } -} - -// Distributes a pair of terms in a multiplication operation, if a pair -// can be distributed. To be distributed, there must be two terms beside -// each other, and at least one of them must be a parenthesis node. -// e.g. 2*(3+x) or (4+x^2+x^3)*(x+3) -// Returns a Node.Status object with substeps -function distributeAndSimplifyOperationNode(node) { - if (!Node.Type.isOperator(node) || node.op !== '*') { - return Node.Status.noChange(node); - } - - // STEP 1: distribute with `distributeTwoNodes` - // e.g. x*(2+x) -> x*2 + x*x - // STEP 2: simplifications of each operand in the new sum with `simplify` - // e.g. x*2 + x*x -> ... -> 2x + x^2 - for (let i = 0; i+1 < node.args.length; i++) { - if (!isParenthesisOfAddition(node.args[i]) && - !isParenthesisOfAddition(node.args[i+1])) { - continue; - } - - let newNode = clone(node); - const substeps = []; - let status; - - const combinedNode = distributeTwoNodes(newNode.args[i], newNode.args[i+1]); - node.args[i].changeGroup = 1; - node.args[i+1].changeGroup = 1; - combinedNode.changeGroup = 1; - - if (newNode.args.length > 2) { - newNode.args.splice(i, 2, combinedNode); - newNode.args[i].changeGroup = 1; - } - else { - newNode = combinedNode; - newNode.changeGroup = 1; - } - - status = Node.Status.nodeChanged( - ChangeTypes.DISTRIBUTE, node, newNode, false); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // case 1: there were more than two operands in this multiplication - // e.g. 3*7*(2+x)*(3+x)*(4+x) is a multiplication node with 5 children - // and the new node will be 3*(14+7x)*(3+x)*(4+x) with 4 children. - if (Node.Type.isOperator(newNode, '*')) { - const childStatus = simplifyWithParens(newNode.args[i]); - if (childStatus.hasChanged()) { - status = Node.Status.childChanged(newNode, childStatus, i); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - } - // case 2: there were only two operands and we multiplied them together. - // e.g. 7*(2+x) -> (7*2 + 7*x) - // Now we can just simplify it. - else if (Node.Type.isParenthesis(newNode)){ - status = simplifyWithParens(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - } - else { - throw Error('Unsupported node type for distribution: ' + node); - } - - if (substeps.length === 1) { - return substeps[0]; - } - - return Node.Status.nodeChanged( - ChangeTypes.DISTRIBUTE, node, newNode, false, substeps); - } - return Node.Status.noChange(node); -} - -// Distributes two nodes together. At least one node must be parenthesis node -// e.g. 2*(x+3) -> (2*x + 2*3) (5+x)*x -> 5*x + x*x -// e.g. (5+x)*(x+3) -> (5*x + 5*3 + x*x + x*3) -// Returns a node. -function distributeTwoNodes(firstNode, secondNode) { - // lists of terms we'll be multiplying together from each node - let firstArgs, secondArgs; - if (isParenthesisOfAddition(firstNode)) { - firstArgs = firstNode.content.args; - } - else { - firstArgs = [firstNode]; - } - - if (isParenthesisOfAddition(secondNode)) { - secondArgs = secondNode.content.args; - } - else { - secondArgs = [secondNode]; - } - // the new operands under addition, now products of terms - const newArgs = []; - - // if exactly one group contains at least one fraction, multiply the - // non-fraction group into the numerators of the fraction group - if ([firstArgs, secondArgs].filter(hasFraction).length === 1) { - const firstArgsHasFraction = hasFraction(firstArgs); - const fractionNodes = firstArgsHasFraction ? firstArgs : secondArgs; - const nonFractionTerm = firstArgsHasFraction ? secondNode : firstNode; - fractionNodes.forEach((node) => { - let arg; - if (isFraction(node)) { - let numerator = Node.Creator.operator('*', [node.args[0], nonFractionTerm]); - numerator = Node.Creator.parenthesis(numerator); - arg = Node.Creator.operator('/', [numerator, node.args[1]]); - } - else { - arg = Node.Creator.operator('*', [node, nonFractionTerm]); - } - arg.changeGroup = 1; - newArgs.push(arg); - }); - } - // e.g. (4+x)(x+y+z) will become 4(x+y+z) + x(x+y+z) as an intermediate - // step. - else if (firstArgs.length > 1 && secondArgs.length > 1) { - firstArgs.forEach(leftArg => { - const arg = Node.Creator.operator('*', [leftArg, secondNode]); - arg.changeGroup = 1; - newArgs.push(arg); - }); - } - else { - // a list of all pairs of nodes between the two arg lists - firstArgs.forEach(leftArg => { - secondArgs.forEach(rightArg => { - const arg = Node.Creator.operator('*', [leftArg, rightArg]); - arg.changeGroup = 1; - newArgs.push(arg); - }); - }); - } - return Node.Creator.parenthesis(Node.Creator.operator('+', newArgs)); -} - -function hasFraction(args) { - return args.filter(isFraction).length > 0; -} - -function isFraction(node) { - return Node.Type.isOperator(node, '/'); -} - -// Simplifies a sum of terms (a result of distribution) that's in parens -// (note that all results of distribution are in parens) -// e.g. 2x*(4 + x) distributes to (2x*4 + 2x*x) -// This is a separate function from simplify to make the flow more readable, -// but this is literally just a wrapper around 'simplify'. -// Returns a Node.Status object -function simplifyWithParens(node) { - if (!Node.Type.isParenthesis(node)) { - throw Error('expected ' + node + ' to be a parenthesis node'); - } - - const status = simplify(node.content); - if (status.hasChanged()) { - return Node.Status.childChanged(node, status); - } - else { - return Node.Status.noChange(node); - } -} - -// Simplifies a sum of terms that are a result of distribution. -// e.g. (2x+3)*(4x+5) -distribute-> 2x*(4x+5) + 3*(4x+5) <- 2 terms to simplify -// e.g. 2x*(4x+5) --distribute--> 2x*4x + 2x*5 --simplify--> 8x^2 + 10x -// Returns a Node.Status object. -function simplify(node) { - const substeps = []; - const simplifyFunctions = [ - arithmeticSearch, // e.g. 2*9 -> 18 - rearrangeCoefficient, // e.g. x*5 -> 5x - collectAndCombineSearch, // e.g 2x*4x -> 8x^2 - distributeAndSimplifyOperationNode, // e.g. (2+x)(3+x) -> 2*(3+x) recurses - ]; - - let newNode = clone(node); - for (let i = 0; i < newNode.args.length; i++) { - for (let j = 0; j < simplifyFunctions.length; j++) { - const childStatus = simplifyFunctions[j](newNode.args[i]); - if (childStatus.hasChanged()) { - const status = Node.Status.childChanged(newNode, childStatus, i); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - } - } - - // possible in cases like 2(x + y) -> 2x + 2y -> doesn't need simplifying - if (substeps.length === 0) { - return Node.Status.noChange(node); - } - else { - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_TERMS, node, newNode, false, substeps); - } -} - -// returns true if `node` is of the type (node + node + ...) -function isParenthesisOfAddition(node) { - if (!Node.Type.isParenthesis(node)) { - return false; - } - const content = node.content; - return Node.Type.isOperator(content, '+'); -} - -module.exports = search; diff --git a/lib/simplifyExpression/divisionSearch/index.js b/lib/simplifyExpression/divisionSearch/index.js deleted file mode 100644 index ffd78b92..00000000 --- a/lib/simplifyExpression/divisionSearch/index.js +++ /dev/null @@ -1,86 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -// Searches for and simplifies any chains of division or nested division. -// Returns a Node.Status object -const search = TreeSearch.preOrder(division); - -function division(node) { - if (!Node.Type.isOperator(node) || node.op !== '/') { - return Node.Status.noChange(node); - } - // e.g. 2/(x/6) => 2 * 6/x - let nodeStatus = multiplyByInverse(node); - if (nodeStatus.hasChanged()) { - return nodeStatus; - } - // e.g. 2/x/6 -> 2/(x*6) - nodeStatus = simplifyDivisionChain(node); - if (nodeStatus.hasChanged()) { - return nodeStatus; - } - return Node.Status.noChange(node); -} - -// If `node` is a fraction with a denominator that is also a fraction, multiply -// by the inverse. -// e.g. x/(2/3) -> x * 3/2 -function multiplyByInverse(node) { - let denominator = node.args[1]; - if (Node.Type.isParenthesis(denominator)) { - denominator = denominator.content; - } - if (!Node.Type.isOperator(denominator) || denominator.op !== '/') { - return Node.Status.noChange(node); - } - // At this point, we know that node is a fraction and denonimator is the - // fraction we need to inverse. - const inverseNumerator = denominator.args[1]; - const inverseDenominator = denominator.args[0]; - const inverseFraction = Node.Creator.operator( - '/', [inverseNumerator, inverseDenominator]); - - const newNode = Node.Creator.operator('*', [node.args[0], inverseFraction]); - return Node.Status.nodeChanged( - ChangeTypes.MULTIPLY_BY_INVERSE, node, newNode); -} - -// Simplifies any chains of division into a single division operation. -// e.g. 2/x/6 -> 2/(x*6) -// Returns a Node.Status object -function simplifyDivisionChain(node) { - // check for a chain of division - const denominatorList = getDenominatorList(node); - // one for the numerator, and at least two terms in the denominator - if (denominatorList.length > 2) { - const numerator = denominatorList.shift(); - // the new single denominator is all the chained denominators - // multiplied together, in parentheses. - const denominator = Node.Creator.parenthesis( - Node.Creator.operator('*', denominatorList)); - const newNode = Node.Creator.operator('/', [numerator, denominator]); - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_DIVISION, node, newNode); - } - return Node.Status.noChange(node); -} - -// Given a the denominator of a division node, returns all the nested -// denominator nodess. e.g. 2/3/4/5 would return [2,3,4,5] -// (note: all the numbers in the example are actually constant nodes) -function getDenominatorList(denominator) { - let node = denominator; - const denominatorList = []; - while (node.op === '/') { - // unshift the denominator to the front of the list, and recurse on - // the numerator - denominatorList.unshift(node.args[1]); - node = node.args[0]; - } - // unshift the final node, which wasn't a / node - denominatorList.unshift(node); - return denominatorList; -} - -module.exports = search; diff --git a/lib/simplifyExpression/fractionsSearch/addConstantAndFraction.js b/lib/simplifyExpression/fractionsSearch/addConstantAndFraction.js deleted file mode 100644 index ed23c8dc..00000000 --- a/lib/simplifyExpression/fractionsSearch/addConstantAndFraction.js +++ /dev/null @@ -1,107 +0,0 @@ -const addConstantFractions = require('./addConstantFractions'); -const clone = require('../../util/clone'); - -const ChangeTypes = require('../../ChangeTypes'); -const evaluate = require('../../util/evaluate'); -const Node = require('../../node'); - -// Adds a constant to a fraction by: -// - collapsing the fraction to decimal if the constant is not an integer -// e.g. 5.3 + 1/2 -> 5.3 + 0.2 -// - turning the constant into a fraction with the same denominator if it is -// an integer, e.g. 5 + 1/2 -> 10/2 + 1/2 -function addConstantAndFraction(node) { - if (!Node.Type.isOperator(node) || node.op !== '+' || node.args.length !== 2) { - return Node.Status.noChange(node); - } - - const firstArg = node.args[0]; - const secondArg = node.args[1]; - let constNode, fractionNode; - if (Node.Type.isConstant(firstArg)) { - if (Node.Type.isIntegerFraction(secondArg)) { - constNode = firstArg; - fractionNode = secondArg; - } - else { - return Node.Status.noChange(node); - } - } - else if (Node.Type.isConstant(secondArg)) { - if (Node.Type.isIntegerFraction(firstArg)) { - constNode = secondArg; - fractionNode = firstArg; - } - else { - return Node.Status.noChange(node); - } - } - else { - return Node.Status.noChange(node); - } - - let newNode = clone(node); - let substeps = []; - // newConstNode and newFractionNode will end up both constants, or both - // fractions. I'm naming them based on their original form so we can keep - // track of which is which. - let newConstNode, newFractionNode; - let changeType; - if (Number.isInteger(parseFloat(constNode.value))) { - const denominatorNode = fractionNode.args[1]; - const denominatorValue = parseInt(denominatorNode); - const constNodeValue = parseInt(constNode.value); - const newNumeratorNode = Node.Creator.constant( - constNodeValue * denominatorValue); - newConstNode = Node.Creator.operator( - '/', [newNumeratorNode, denominatorNode]); - newFractionNode = fractionNode; - changeType = ChangeTypes.CONVERT_INTEGER_TO_FRACTION; - } - else { - // round to 4 decimal places - let dividedValue = evaluate(fractionNode); - if (dividedValue < 1) { - dividedValue = parseFloat(dividedValue.toPrecision(4)); - } - else { - dividedValue = parseFloat(dividedValue.toFixed(4)); - } - newFractionNode = Node.Creator.constant(dividedValue); - newConstNode = constNode; - changeType = ChangeTypes.DIVIDE_FRACTION_FOR_ADDITION; - } - - if (Node.Type.isConstant(firstArg)) { - newNode.args[0] = newConstNode; - newNode.args[1] = newFractionNode; - } - else { - newNode.args[0] = newFractionNode; - newNode.args[1] = newConstNode; - } - - substeps.push(Node.Status.nodeChanged(changeType, node, newNode)); - newNode = Node.Status.resetChangeGroups(newNode); - - // If we changed an integer to a fraction, we need to add the steps for - // adding the fractions. - if (changeType === ChangeTypes.CONVERT_INTEGER_TO_FRACTION) { - const addFractionStatus = addConstantFractions(newNode); - substeps = substeps.concat(addFractionStatus.substeps); - } - // Otherwise, add the two constants - else { - const evalNode = Node.Creator.constant(evaluate(newNode)); - substeps.push(Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_ARITHMETIC, newNode, evalNode)); - } - - const lastStep = substeps[substeps.length - 1]; - newNode = Node.Status.resetChangeGroups(lastStep.newNode); - - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode, true, substeps); -} - -module.exports = addConstantAndFraction; diff --git a/lib/simplifyExpression/fractionsSearch/addConstantFractions.js b/lib/simplifyExpression/fractionsSearch/addConstantFractions.js deleted file mode 100644 index 1179c366..00000000 --- a/lib/simplifyExpression/fractionsSearch/addConstantFractions.js +++ /dev/null @@ -1,173 +0,0 @@ -const clone = require('../../util/clone'); -const divideByGCD = require('./divideByGCD'); -const math = require('mathjs'); - -const ChangeTypes = require('../../ChangeTypes'); -const evaluate = require('../../util/evaluate'); -const Node = require('../../node'); - -// Adds constant fractions -- can start from either step 1 or 2 -// 1A. Find the LCD if denominators are different and multiplies to make -// denominators equal, e.g. 2/3 + 4/6 --> (2*2)/(3*2) + 4/6 -// 1B. Multiplies out to make constant fractions again -// e.g. (2*2)/(3*2) + 4/6 -> 4/6 + 4/6 -// 2A. Combines numerators, e.g. 4/6 + 4/6 -> e.g. 2/5 + 4/5 --> (2+4)/5 -// 2B. Adds numerators together, e.g. (2+4)/5 -> 6/5 -// Returns a Node.Status object with substeps -function addConstantFractions(node) { - let newNode = clone(node); - - if (!Node.Type.isOperator(node) || node.op !== '+') { - return Node.Status.noChange(node); - } - if (!node.args.every(n => Node.Type.isIntegerFraction(n, true))) { - return Node.Status.noChange(node); - } - const denominators = node.args.map(fraction => { - return parseFloat(evaluate(fraction.args[1])); - }); - - const substeps = []; - let status; - - // 1A. First create the common denominator if needed - // e.g. 2/6 + 1/4 -> (2*2)/(6*2) + (1*3)/(4*3) - if (!denominators.every(denominator => denominator === denominators[0])) { - status = makeCommonDenominator(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // 1B. Multiply out the denominators - status = evaluateDenominators(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // 1B. Multiply out the numerators - status = evaluateNumerators(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // 2A. Now that they all have the same denominator, combine the numerators - // e.g. 2/3 + 5/3 -> (2+5)/3 - status = combineNumeratorsAboveCommonDenominator(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // 2B. Finally, add the numerators together - status = addNumeratorsTogether(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // 2C. If the numerator is 0, simplify to just 0 - status = reduceNumerator(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // 2D. If we can simplify the fraction, do so - status = divideByGCD(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - return Node.Status.nodeChanged( - ChangeTypes.ADD_FRACTIONS, node, newNode, true, substeps); -} - -// Given a + operation node with a list of fraction nodes as args that all have -// the same denominator, add them together. e.g. 2/3 + 5/3 -> (2+5)/3 -// Returns the new node. -function combineNumeratorsAboveCommonDenominator(node) { - let newNode = clone(node); - - const commonDenominator = newNode.args[0].args[1]; - const numeratorArgs = []; - newNode.args.forEach(fraction => { - numeratorArgs.push(fraction.args[0]); - }); - const newNumerator = Node.Creator.parenthesis( - Node.Creator.operator('+', numeratorArgs)); - - newNode = Node.Creator.operator('/', [newNumerator, commonDenominator]); - return Node.Status.nodeChanged( - ChangeTypes.COMBINE_NUMERATORS, node, newNode); -} - -// Given a node with a numerator that is an addition node, will add -// all the numerators and return the result -function addNumeratorsTogether(node) { - const newNode = clone(node); - - newNode.args[0] = Node.Creator.constant(evaluate(newNode.args[0])); - return Node.Status.nodeChanged( - ChangeTypes.ADD_NUMERATORS, node, newNode); -} - -function reduceNumerator(node) { - let newNode = clone(node); - - if (newNode.args[0].value === '0') { - newNode = Node.Creator.constant(0); - return Node.Status.nodeChanged( - ChangeTypes.REDUCE_ZERO_NUMERATOR, node, newNode); - } - - return Node.Status.noChange(node); -} - -// Takes `node`, a sum of fractions, and returns a node that's a sum of -// fractions with denominators that evaluate to the same common denominator -// e.g. 2/6 + 1/4 -> (2*2)/(6*2) + (1*3)/(4*3) -// Returns the new node. -function makeCommonDenominator(node) { - const newNode = clone(node); - - const denominators = newNode.args.map(fraction => { - return parseFloat(fraction.args[1].value); - }); - const commonDenominator = math.lcm(...denominators); - - newNode.args.forEach((child, i) => { - // missingFactor is what we need to multiply the top and bottom by - // so that the denominator is the LCD - const missingFactor = commonDenominator / denominators[i]; - if (missingFactor !== 1) { - const missingFactorNode = Node.Creator.constant(missingFactor); - const newNumerator = Node.Creator.parenthesis( - Node.Creator.operator('*', [child.args[0], missingFactorNode])); - const newDeominator = Node.Creator.parenthesis( - Node.Creator.operator('*', [child.args[1], missingFactorNode])); - newNode.args[i] = Node.Creator.operator('/', [newNumerator, newDeominator]); - } - }); - - return Node.Status.nodeChanged( - ChangeTypes.COMMON_DENOMINATOR, node, newNode); -} - -function evaluateDenominators(node) { - const newNode = clone(node); - - newNode.args.map(fraction => { - fraction.args[1] = Node.Creator.constant(evaluate(fraction.args[1])); - }); - - return Node.Status.nodeChanged( - ChangeTypes.MULTIPLY_DENOMINATORS, node, newNode); -} - -function evaluateNumerators(node) { - const newNode = clone(node); - - newNode.args.map(fraction => { - fraction.args[0] = Node.Creator.constant(evaluate(fraction.args[0])); - }); - - return Node.Status.nodeChanged( - ChangeTypes.MULTIPLY_NUMERATORS, node, newNode); -} - -module.exports = addConstantFractions; diff --git a/lib/simplifyExpression/fractionsSearch/cancelLikeTerms.js b/lib/simplifyExpression/fractionsSearch/cancelLikeTerms.js deleted file mode 100644 index dd7818e6..00000000 --- a/lib/simplifyExpression/fractionsSearch/cancelLikeTerms.js +++ /dev/null @@ -1,306 +0,0 @@ -const clone = require('../../util/clone'); -const print = require('../../util/print'); - -const ChangeTypes = require('../../ChangeTypes'); -const Negative = require('../../Negative'); -const Node = require('../../node'); - -// Used for cancelTerms to return a (possibly updated) numerator and denominator -class CancelOutStatus { - constructor(numerator, denominator, hasChanged=false) { - this.numerator = numerator; - this.denominator = denominator; - this.hasChanged = hasChanged; - } -} - -// Cancels like terms in a fraction node -// e.g. (2x^2 * 5) / 2x^2 => 5 / 1 -// Returns a Node.Status object -function cancelLikeTerms(node) { - if (!Node.Type.isOperator(node) || node.op !== '/') { - return Node.Status.noChange(node); - } - let newNode = clone(node); - const numerator = newNode.args[0]; - const denominator = newNode.args[1]; - - // case 1: neither the numerator or denominator is a multiplication of terms - if (!isMultiplicationOfTerms(numerator) && - !isMultiplicationOfTerms(denominator)) { - const cancelStatus = cancelTerms(numerator, denominator); - if (cancelStatus.hasChanged) { - newNode.args[0] = cancelStatus.numerator || Node.Creator.constant(1); - if (cancelStatus.denominator) { - newNode.args[1] = cancelStatus.denominator; - } - else { - // If we cancelled out the denominator, the node is now its numerator - // e.g. (2x*y) / 2x => y (note y isn't a fraction) - newNode = newNode.args[0]; - } - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_TERMS, node, newNode); - } - else { - return Node.Status.noChange(node); - } - } - - // case 2: numerator is a multiplication of terms and denominator is not - // e.g. (2x^2 * 5) / 2x^2 => 5 / 1 - // e.g. (x^2*y) / x => x^(2 - 1) * y (<-- note that the denominator goes - // away because we always adjust the exponent in the numerator) - else if (isMultiplicationOfTerms(numerator) && - !isMultiplicationOfTerms(denominator)) { - const numeratorArgs = Node.Type.isParenthesis(numerator) ? - numerator.content.args : numerator.args; - for (let i = 0; i < numeratorArgs.length; i++) { - const cancelStatus = cancelTerms(numeratorArgs[i], denominator); - if (cancelStatus.hasChanged) { - if (cancelStatus.numerator) { - numeratorArgs[i] = cancelStatus.numerator; - } - // if the cancelling out got rid of the numerator node, we remove it from - // the list - else { - numeratorArgs.splice(i, 1); - // if the numerator is now a "multiplication" of only one term, - // change it to just that term - if (numeratorArgs.length === 1) { - newNode.args[0] = numeratorArgs[0]; - } - } - if (cancelStatus.denominator) { - newNode.args[1] = cancelStatus.denominator; - } - else { - // If we cancelled out the denominator, the node is now its numerator - // e.g. (2x*y) / 2x => y (note y isn't a fraction) - newNode = newNode.args[0]; - } - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_TERMS, node, newNode); - } - } - return Node.Status.noChange(node); - } - - // case 3: denominator is a multiplication of terms and numerator is not - // e.g. 2x^2 / (2x^2 * 5) => 1 / 5 - // e.g. x / (x^2*y) => x^(1-2) / y - else if (isMultiplicationOfTerms(denominator) && - !isMultiplicationOfTerms(numerator)) { - const denominatorArgs = Node.Type.isParenthesis(denominator) ? - denominator.content.args : denominator.args; - for (let i = 0; i < denominatorArgs.length; i++) { - const cancelStatus = cancelTerms(numerator, denominatorArgs[i]); - if (cancelStatus.hasChanged) { - newNode.args[0] = cancelStatus.numerator || Node.Creator.constant(1); - if (cancelStatus.denominator) { - denominatorArgs[i] = cancelStatus.denominator; - } - // if the cancelling out got rid of the denominator node, we remove it - // from the list - else { - denominatorArgs.splice(i, 1); - // if the denominator is now a "multiplication" of only one term, - // change it to just that term - if (denominatorArgs.length === 1) { - newNode.args[1] = denominatorArgs[0]; - } - } - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_TERMS, node, newNode); - } - } - return Node.Status.noChange(node); - } - - // case 4: the numerator and denominator are both multiplications of terms - else { - const numeratorArgs = Node.Type.isParenthesis(numerator) ? - numerator.content.args : numerator.args; - const denominatorArgs = Node.Type.isParenthesis(denominator) ? - denominator.content.args : denominator.args; - for (let i = 0; i < numeratorArgs.length; i++) { - for (let j = 0; j < denominatorArgs.length; j++) { - const cancelStatus = cancelTerms(numeratorArgs[i], denominatorArgs[j]); - if (cancelStatus.hasChanged) { - if (cancelStatus.numerator) { - numeratorArgs[i] = cancelStatus.numerator; - } - // if the cancelling out got rid of the numerator node, we remove it - // from the list - else { - numeratorArgs.splice(i, 1); - // if the numerator is now a "multiplication" of only one term, - // change it to just that term - if (numeratorArgs.length === 1) { - newNode.args[0] = numeratorArgs[0]; - } - } - if (cancelStatus.denominator) { - denominatorArgs[j] = cancelStatus.denominator; - } - // if the cancelling out got rid of the denominator node, we remove it - // from the list - else { - denominatorArgs.splice(j, 1); - // if the denominator is now a "multiplication" of only one term, - // change it to just that term - if (denominatorArgs.length === 1) { - newNode.args[1] = denominatorArgs[0]; - } - } - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_TERMS, node, newNode); - } - } - } - return Node.Status.noChange(node); - } -} - -// Given a term in the numerator and a term in the denominator, cancels out -// like terms if possible. See the cases below for possible things that can -// be cancelled out and how they are cancelled out. -// Returns the new nodes for numerator and denominator with the common terms -// removed. If the entire numerator or denominator is cancelled out, it is -// returned as null. e.g. 4, 4x => null, x -function cancelTerms(numerator, denominator) { - // Deal with unary minuses by recursing on the argument - if (Node.Type.isUnaryMinus(numerator)) { - const cancelStatus = cancelTerms(numerator.args[0], denominator); - if (!cancelStatus.numerator) { - numerator = Node.Creator.constant(-1); - } - else if (Negative.isNegative(cancelStatus.numerator)) { - numerator = Negative.negate(cancelStatus.numerator); - } - else { - numerator.args[0] = cancelStatus.numerator; - } - denominator = cancelTerms.denominator; - return new CancelOutStatus(numerator, denominator, cancelStatus.hasChanged); - } - if (Node.Type.isUnaryMinus(denominator)) { - const cancelStatus = cancelTerms(numerator, denominator.args[0]); - numerator = cancelStatus.numerator; - if (cancelStatus.denominator) { - denominator.args[0] = cancelStatus.denominator; - } - else { - denominator = cancelStatus.denominator; - if (numerator) { - numerator = Negative.negate(numerator); - } - else { - numerator = Node.Creator.constant(-1); - } - } - return new CancelOutStatus(numerator, denominator, cancelStatus.hasChanged); - } - - // Deal with parens similarily - if (Node.Type.isParenthesis(numerator)) { - const cancelStatus = cancelTerms(numerator.content, denominator); - if (cancelStatus.numerator) { - numerator.content = cancelStatus.numerator; - } - else { - // if the numerator was cancelled out, the numerator should be null - // and not null in parens. - numerator = cancelStatus.numerator; - } - denominator = cancelStatus.denominator; - return new CancelOutStatus(numerator, denominator, cancelStatus.hasChanged); - } - if (Node.Type.isParenthesis(denominator)) { - const cancelStatus = cancelTerms(numerator, denominator.content); - if (cancelStatus.denominator) { - denominator.content = cancelStatus.denominator; - } - else { - // if the denominator was cancelled out, the denominator should be null - // and not null in parens. - denominator = cancelStatus.denominator; - } - numerator = cancelStatus.numerator; - return new CancelOutStatus(numerator, denominator, cancelStatus.hasChanged); - } - - // Now for the term cancelling ---- - - // case 1: the numerator term and denominator term are the same, so we cancel - // them out. e.g. (x+5)^100 / (x+5)^100 => null / null - if (print(numerator) === print(denominator)) { - return new CancelOutStatus(null, null, true); - } - - // case 2: they're both exponent nodes with the same base - // e.g. (2x+5)^8 and (2x+5)^2 - if (Node.Type.isOperator(numerator, '^') && - Node.Type.isOperator(denominator, '^') && - print(numerator.args[0]) === print(denominator.args[0])) { - const numeratorExponent = numerator.args[1]; - let denominatorExponent = denominator.args[1]; - // wrap the denominatorExponent in parens, in case it's complicated. - // If the parens aren't needed, they'll be removed with - // removeUnnecessaryParens at the end of this step. - denominatorExponent = Node.Creator.parenthesis(denominatorExponent); - const newExponent = Node.Creator.parenthesis( - Node.Creator.operator('-', [numeratorExponent, denominatorExponent])); - numerator.args[1] = newExponent; - return new CancelOutStatus(numerator, null, true); - } - - // case 3: they're both polynomial terms, check if they have the same symbol - // e.g. 4x^2 / 5x^2 => 4 / 5 - // e.g. 4x^3 / 5x^2 => 4x^(3-2) / 5 - if (Node.PolynomialTerm.isPolynomialTerm(numerator) && - Node.PolynomialTerm.isPolynomialTerm(denominator)) { - const numeratorTerm = new Node.PolynomialTerm(numerator); - const denominatorTerm = new Node.PolynomialTerm(denominator); - if (numeratorTerm.getSymbolName() !== denominatorTerm.getSymbolName()) { - return new CancelOutStatus(numerator, denominator); - } - const numeratorExponent = numeratorTerm.getExponentNode(true); - let denominatorExponent = denominatorTerm.getExponentNode(true); - if (print(numeratorExponent) === print(denominatorExponent)) { - // note this returns null if there's no coefficient (ie it's 1) - numerator = numeratorTerm.getCoeffNode(); - } - else { - // wrap the denominatorExponent in parens, in case it's complicated. - // If the parens aren't needed, they'll be removed with - // removeUnnecessaryParens at the end of this step. - denominatorExponent = Node.Creator.parenthesis(denominatorExponent); - const newExponent = Node.Creator.parenthesis( - Node.Creator.operator('-', [numeratorExponent, denominatorExponent])); - numerator = Node.Creator.polynomialTerm( - numeratorTerm.getSymbolNode(), - newExponent, - numeratorTerm.getCoeffNode()); - } - denominator = denominatorTerm.getCoeffNode(); - return new CancelOutStatus(numerator, denominator, true); - } - - return new CancelOutStatus(numerator, denominator); -} - -// Returns true if node is a multiplication of terms that can be cancelled out -// e.g. 2 * 6^y => true -// e.g. 2 + 6 => false -// e.g. (2 * 6^y) => true -// e.g. 2x^2 => false (polynomial terms are considered as one single term) -function isMultiplicationOfTerms(node) { - if (Node.Type.isParenthesis(node)) { - return isMultiplicationOfTerms(node.content); - } - return (Node.Type.isOperator(node, '*') && - !Node.PolynomialTerm.isPolynomialTerm(node)); -} - -module.exports = cancelLikeTerms; diff --git a/lib/simplifyExpression/fractionsSearch/divideByGCD.js b/lib/simplifyExpression/fractionsSearch/divideByGCD.js deleted file mode 100644 index c225f68d..00000000 --- a/lib/simplifyExpression/fractionsSearch/divideByGCD.js +++ /dev/null @@ -1,58 +0,0 @@ -const math = require('mathjs'); - -const ChangeTypes = require('../../ChangeTypes'); -const evaluate = require('../../util/evaluate'); -const Node = require('../../node'); - -// Simplifies a fraction (with constant numerator and denominator) by dividing -// the top and bottom by the GCD, if possible. -// e.g. 2/4 --> 1/2 10/5 --> 2x -// Also simplified negative signs -// e.g. -1/-3 --> 1/3 4/-5 --> -4/5 -// Note that -4/5 doesn't need to be simplified. -// Note that our goal is for the denominator to always be positive. If it -// isn't, we can simplify signs. -// Returns a Node.Status object -function divideByGCD(fraction) { - if (!Node.Type.isOperator(fraction) || fraction.op !== '/') { - return Node.Status.noChange(fraction); - } - // If it's not an integer fraction, all we can do is simplify signs - if (!Node.Type.isIntegerFraction(fraction, true)) { - return Node.Status.noChange(fraction); - } - - const numeratorValue = parseInt(evaluate(fraction.args[0])); - const denominatorValue = parseInt(evaluate(fraction.args[1])); - - // The gcd is what we're dividing the numerator and denominator by. - let gcd = math.gcd(numeratorValue, denominatorValue); - // A greatest common denominator is technically defined as always positive, - // but since our goal is to reduce negative signs or move them to the - // numerator, a negative denominator always means we want to flip signs - // of both numerator and denominator. - // e.g. -1/-3 --> 1/3 4/-5 --> -4/5 - if (denominatorValue < 0) { - gcd *= -1; - } - - if (gcd === 1) { - return Node.Status.noChange(fraction); - } - - const newNumeratorNode = Node.Creator.constant(numeratorValue/gcd); - const newDenominatorNode = Node.Creator.constant(denominatorValue/gcd); - let newFraction; - if (parseFloat(newDenominatorNode.value) === 1) { - newFraction = newNumeratorNode; - } - else { - newFraction = Node.Creator.operator( - '/', [newNumeratorNode, newDenominatorNode]); - } - - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_FRACTION, fraction, newFraction); -} - -module.exports = divideByGCD; diff --git a/lib/simplifyExpression/fractionsSearch/index.js b/lib/simplifyExpression/fractionsSearch/index.js deleted file mode 100644 index 6650cd55..00000000 --- a/lib/simplifyExpression/fractionsSearch/index.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Performs simpifications on fractions: adding and cancelling out. - * - * Note: division is represented in mathjs as an operator node with op '/' - * and two args, where arg[0] is the numerator and arg[1] is the denominator - -// This module manipulates fractions with constants in the numerator and -// denominator. For more complex/general fractions, see Fraction.js - - */ - -const addConstantAndFraction = require('./addConstantAndFraction'); -const addConstantFractions = require('./addConstantFractions'); -const cancelLikeTerms = require('./cancelLikeTerms'); -const divideByGCD = require('./divideByGCD'); -const simplifyFractionSigns = require('./simplifyFractionSigns'); -const simplifyPolynomialFraction = require('./simplifyPolynomialFraction'); - -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -const SIMPLIFICATION_FUNCTIONS = [ - // e.g. 2/3 + 5/6 - addConstantFractions, - // e.g. 4 + 5/6 or 4.5 + 6/8 - addConstantAndFraction, - // e.g. 2/-9 -> -2/9 e.g. -2/-9 -> 2/9 - simplifyFractionSigns, - // e.g. 8/12 -> 2/3 (divide by GCD 4) - divideByGCD, - // e.g. 2x/4 -> x/2 (divideByGCD but for coefficients of polynomial terms) - simplifyPolynomialFraction, - // e.g. (2x * 5) / 2x -> 5 - cancelLikeTerms, -]; - -const search = TreeSearch.preOrder(simplifyFractions); - -// Look for step(s) to perform on a node. Returns a Node.Status object. -function simplifyFractions(node) { - for (let i = 0; i < SIMPLIFICATION_FUNCTIONS.length; i++) { - const nodeStatus = SIMPLIFICATION_FUNCTIONS[i](node); - if (nodeStatus.hasChanged()) { - return nodeStatus; - } - else { - node = nodeStatus.newNode; - } - } - return Node.Status.noChange(node); -} - - -module.exports = search; diff --git a/lib/simplifyExpression/fractionsSearch/simplifyFractionSigns.js b/lib/simplifyExpression/fractionsSearch/simplifyFractionSigns.js deleted file mode 100644 index 41d52bcf..00000000 --- a/lib/simplifyExpression/fractionsSearch/simplifyFractionSigns.js +++ /dev/null @@ -1,35 +0,0 @@ -const clone = require('../../util/clone'); - -const ChangeTypes = require('../../ChangeTypes'); -const Negative = require('../../Negative'); -const Node = require('../../node'); - -// Simplifies negative signs if possible -// e.g. -1/-3 --> 1/3 4/-5 --> -4/5 -// Note that -4/5 doesn't need to be simplified. -// Note that our goal is for the denominator to always be positive. If it -// isn't, we can simplify signs. -// Returns a Node.Status object -function simplifySigns(fraction) { - if (!Node.Type.isOperator(fraction) || fraction.op !== '/') { - return Node.Status.noChange(fraction); - } - const oldFraction = clone(fraction); - let numerator = fraction.args[0]; - let denominator = fraction.args[1]; - // The denominator should never be negative. - if (Negative.isNegative(denominator)) { - denominator = Negative.negate(denominator); - const changeType = Negative.isNegative(numerator) ? - ChangeTypes.CANCEL_MINUSES : - ChangeTypes.SIMPLIFY_SIGNS; - numerator = Negative.negate(numerator); - const newFraction = Node.Creator.operator('/', [numerator, denominator]); - return Node.Status.nodeChanged(changeType, oldFraction, newFraction); - } - else { - return Node.Status.noChange(fraction); - } -} - -module.exports = simplifySigns; diff --git a/lib/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.js b/lib/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.js deleted file mode 100644 index a130a4f9..00000000 --- a/lib/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.js +++ /dev/null @@ -1,44 +0,0 @@ -const arithmeticSearch = require('../arithmeticSearch'); -const clone = require('../../util/clone'); -const divideByGCD = require('./divideByGCD'); -const Node = require('../../node'); - -// Simplifies a polynomial term with a fraction as its coefficients. -// e.g. 2x/4 --> x/2 10x/5 --> 2x -// Also simplified negative signs -// e.g. -y/-3 --> y/3 4x/-5 --> -4x/5 -// returns the new simplified node in a Node.Status object -function simplifyPolynomialFraction(node) { - if (!Node.PolynomialTerm.isPolynomialTerm(node)) { - return Node.Status.noChange(node); - } - - const polyNode = new Node.PolynomialTerm(clone(node)); - if (!polyNode.hasFractionCoeff()) { - return Node.Status.noChange(node); - } - - const coefficientSimplifications = [ - divideByGCD, // for integer fractions - arithmeticSearch, // for decimal fractions - ]; - - for (let i = 0; i < coefficientSimplifications.length; i++) { - const coefficientFraction = polyNode.getCoeffNode(); // a division node - const newCoeffStatus = coefficientSimplifications[i](coefficientFraction); - if (newCoeffStatus.hasChanged()) { - let newCoeff = newCoeffStatus.newNode; - if (newCoeff.value === '1') { - newCoeff = null; - } - const exponentNode = polyNode.getExponentNode(); - const newNode = Node.Creator.polynomialTerm( - polyNode.getSymbolNode(), exponentNode, newCoeff); - return Node.Status.nodeChanged(newCoeffStatus.changeType, node, newNode); - } - } - - return Node.Status.noChange(node); -} - -module.exports = simplifyPolynomialFraction; diff --git a/lib/simplifyExpression/functionsSearch/absoluteValue.js b/lib/simplifyExpression/functionsSearch/absoluteValue.js deleted file mode 100644 index aa4ab9b7..00000000 --- a/lib/simplifyExpression/functionsSearch/absoluteValue.js +++ /dev/null @@ -1,38 +0,0 @@ -const clone = require('../../util/clone'); -const math = require('mathjs'); - -const ChangeTypes = require('../../ChangeTypes'); -const evaluate = require('../../util/evaluate'); -const Node = require('../../node'); - -// Evaluates abs() function if it's on a single constant value. -// Returns a Node.Status object. -function absoluteValue(node) { - if (!Node.Type.isFunction(node, 'abs')) { - return Node.Status.noChange(node); - } - if (node.args.length > 1) { - return Node.Status.noChange(node); - } - let newNode = clone(node); - const argument = newNode.args[0]; - if (Node.Type.isConstant(argument, true)) { - newNode = Node.Creator.constant(math.abs(evaluate(argument))); - return Node.Status.nodeChanged( - ChangeTypes.ABSOLUTE_VALUE, node, newNode); - } - else if (Node.Type.isConstantFraction(argument, true)) { - const newNumerator = Node.Creator.constant( - math.abs(evaluate(argument.args[0]))); - const newDenominator = Node.Creator.constant( - math.abs(evaluate(argument.args[1]))); - newNode = Node.Creator.operator('/', [newNumerator, newDenominator]); - return Node.Status.nodeChanged( - ChangeTypes.ABSOLUTE_VALUE, node, newNode); - } - else { - return Node.Status.noChange(node); - } -} - -module.exports = absoluteValue; diff --git a/lib/simplifyExpression/functionsSearch/index.js b/lib/simplifyExpression/functionsSearch/index.js deleted file mode 100644 index 95d11b25..00000000 --- a/lib/simplifyExpression/functionsSearch/index.js +++ /dev/null @@ -1,32 +0,0 @@ -const absoluteValue = require('./absoluteValue'); -const nthRoot = require('./nthRoot'); - -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -const FUNCTIONS = [ - nthRoot, - absoluteValue -]; - -// Searches through the tree, prioritizing deeper nodes, and evaluates -// functions (e.g. abs(-4)) if possible. -// Returns a Node.Status object. -const search = TreeSearch.postOrder(functions); - -// Evaluates a function call if possible. Returns a Node.Status object. -function functions(node) { - if (!Node.Type.isFunction(node)) { - return Node.Status.noChange(node); - } - - for (let i = 0; i < FUNCTIONS.length; i++) { - const nodeStatus = FUNCTIONS[i](node); - if (nodeStatus.hasChanged()) { - return nodeStatus; - } - } - return Node.Status.noChange(node); -} - -module.exports = search; diff --git a/lib/simplifyExpression/functionsSearch/nthRoot.js b/lib/simplifyExpression/functionsSearch/nthRoot.js deleted file mode 100644 index bb747746..00000000 --- a/lib/simplifyExpression/functionsSearch/nthRoot.js +++ /dev/null @@ -1,473 +0,0 @@ -const clone = require('../../util/clone'); -const math = require('mathjs'); - -const ChangeTypes = require('../../ChangeTypes'); -const ConstantFactors = require('../../factor/ConstantFactors'); -const Negative = require('../../Negative'); -const Node = require('../../node'); - -// Evaluate nthRoot() function. -// Returns a Node.Status object. -function nthRoot(node) { - if (!Node.Type.isFunction(node, 'nthRoot')) { - return Node.Status.noChange(node); - } - - const radicandNode = getRadicandNode(node); - if (Node.Type.isOperator(radicandNode)) { - if (radicandNode.op === '^') { - return nthRootExponent(node); - } - else if (radicandNode.op === '*') { - return nthRootMultiplication(node); - } - } - else if (Node.Type.isConstant(radicandNode)) { - return nthRootConstant(node); - } - - return Node.Status.noChange(node); -} - -// Returns the nthRoot evaluated for an exponent node. Expects an exponent under -// the radicand. Cancels the root and the exponent if possible. Three cases: -// equal: nthRoot(2^x, x) = 2 -// root > exponent: nthRoot(x^2, 4) = nthRoot(x, 2) -// exponent > root: nthRoot(x^4, 2) = x^2 -function nthRootExponent(node) { - let newNode = clone(node); - - const radicandNode = getRadicandNode(node); - const rootNode = getRootNode(node); - const baseNode = radicandNode.args[0]; - const exponentNode = Node.Type.isParenthesis(radicandNode.args[1]) ? - radicandNode.args[1].content : radicandNode.args[1]; - if (rootNode.equals(exponentNode)) { - newNode = baseNode; - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_EXPONENT_AND_ROOT, node, newNode); - } - else if (Node.Type.isConstant(rootNode) && Node.Type.isConstant(exponentNode)) { - const rootValue = parseFloat(rootNode.value); - const exponentValue = parseFloat(exponentNode.value); - if (rootValue % exponentValue === 0) { - const newRootValue = rootValue/exponentValue; - const newRootNode = Node.Creator.constant(newRootValue); - - newNode = Node.Creator.nthRoot(baseNode, newRootNode); - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_EXPONENT, node, newNode); - } - else if (exponentValue % rootValue === 0) { - const newExponentValue = exponentValue/rootValue; - const newExponentNode = Node.Creator.constant(newExponentValue); - - newNode = Node.Creator.operator('^', [baseNode, newExponentNode]); - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_ROOT, node, newNode); - } - } - - return Node.Status.noChange(node); -} - -// Returns the nthRoot evaluated for a multiplication node. -// Expects a multiplication node uder the radicand. -// If the root is a positive constant, it: -// 1A: factors the multiplicands -// 1B: groups them into groups whose length is the root value -// 1C: converts the multiplications into exponents. -// If it's possible to simplify further, it: -// 2A: Distributes the nthRoot into the children nodes, -// 2B: evaluates those nthRoots -// 2C: combines them -function nthRootMultiplication(node) { - let newNode = clone(node); - const rootNode = getRootNode(node); - - const substeps = []; - let status; - if (Node.Type.isConstant(rootNode) && !Negative.isNegative(rootNode)) { - // Step 1A - status = factorMultiplicands(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // Step 1B - status = groupTermsByRoot(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // Step 1C - status = convertMultiplicationToExponent(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - if (newNode.args[0].op === '^') { - status = nthRootExponent(newNode); - substeps.push(status); - return Node.Status.nodeChanged( - ChangeTypes.NTH_ROOT_VALUE, node, status.newNode, true, substeps); - } - } - } - - // Step 2A - status = distributeNthRoot(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // Step 2B - status = evaluateNthRootForChildren(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // Step 2C - status = combineRoots(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - return Node.Status.nodeChanged( - ChangeTypes.NTH_ROOT_VALUE, node, newNode, true, substeps); - } - - return Node.Status.noChange(node); -} - -// Given an nthRoot node with a constant positive root, will do the step of -// factoring all the multiplicands under the radicand -// e.g. nthRoot(2 * 9 * 5 * 12) = nthRoot(2 * 3 * 3 * 5 * 2 * 2 * 3) -function factorMultiplicands(node) { - const newNode = clone(node); - const radicandNode = getRadicandNode(node); - - let children = []; - let factored = false; - radicandNode.args.forEach(child => { - if (Node.PolynomialTerm.isPolynomialTerm(child)) { - const polyTerm = new Node.PolynomialTerm(child); - const coeffNode = polyTerm.getCoeffNode(); - const polyTermNoCoeff = Node.Creator.polynomialTerm( - polyTerm.getSymbolNode(), polyTerm.getExponentNode(), null); - if (coeffNode) { - const factorNodes = getFactorNodes(coeffNode); - if (factorNodes.length > 1) { - factored = true; - } - children = children.concat(factorNodes); - } - children.push(polyTermNoCoeff); - } - else { - const factorNodes = getFactorNodes(child); - if (factorNodes.length > 1) { - factored = true; - } - children = children.concat(factorNodes); - } - }); - - if (factored) { - newNode.args[0] = Node.Creator.operator('*', children); - return Node.Status.nodeChanged( - ChangeTypes.FACTOR_INTO_PRIMES, node, newNode); - } - - return Node.Status.noChange(node); -} - -function getFactorNodes(node) { - if (Node.Type.isConstant(node) && !Negative.isNegative(node)) { - const value = parseFloat(node.value); - const factors = ConstantFactors.getPrimeFactors(value); - const factorNodes = factors.map(Node.Creator.constant); - return factorNodes; - } - return [node]; -} - -// Given an nthRoot node with a constant positive root, will group the arguments -// into groups of the root as a step -// e.g. nthRoot(2 * 2 * 2, 2) -> nthRoot((2 * 2) * 2, 2) -function groupTermsByRoot(node) { - const newNode = clone(node); - const radicandNode = getRadicandNode(node); - const rootNode = getRootNode(node); - const rootValue = parseFloat(rootNode.value); - - radicandNode.args.sort(sortNodes); - - // We want to go through the sorted nodes, and try to find any groups of the - // same node that are the size of the root value - let children = [], hasGroups = false; - for (let i = 0; i < radicandNode.args.length;) { - let j = i; - const initialNode = radicandNode.args[i]; - while (j < radicandNode.args.length && j - i < rootValue) { - const siblingNode = radicandNode.args[j]; - if (!initialNode.equals(siblingNode)) { - break; - } - j++; - } - if (j - i === rootValue) { - hasGroups = true; - const groupedNode = Node.Creator.parenthesis( - Node.Creator.operator('*', radicandNode.args.slice(i, j))); - children.push(groupedNode); - } - else { - children = children.concat(radicandNode.args.slice(i, j)); - } - i = j; - } - - if (hasGroups) { - newNode.args[0] = children.length === 1 ? - children[0] : Node.Creator.operator('*', children); - return Node.Status.nodeChanged( - ChangeTypes.GROUP_TERMS_BY_ROOT, node, newNode); - } - // if we don't group any factors, then we can't simplify it any more - return Node.Status.noChange(node); -} - -// Given an nthRoot node with a constant positive root, -// will convert any grouped factors into exponent nodes as a step -// e.g. nthRoot((2 * 2) * 2, 2) -> nthRoot(2^2 * 2, 2) -function convertMultiplicationToExponent(node) { - const newNode = clone(node); - - const radicandNode = getRadicandNode(node); - - if (Node.Type.isParenthesis(radicandNode)) { - const child = radicandNode.content; - if (isMultiplicationOfEqualNodes(child)) { - const baseNode = child.args[0]; - const exponentNode = Node.Creator.constant(child.args.length); - newNode.args[0] = Node.Creator.operator('^', [baseNode, exponentNode]); - return Node.Status.nodeChanged( - ChangeTypes.CONVERT_MULTIPLICATION_TO_EXPONENT, node, newNode); - } - } - else if (Node.Type.isOperator(radicandNode, '*')) { - const children = []; - radicandNode.args.forEach(child => { - if (Node.Type.isParenthesis(child)) { - const grandChild = child.content; - if (isMultiplicationOfEqualNodes(grandChild)) { - const baseNode = grandChild.args[0]; - const exponentNode = Node.Creator.constant(grandChild.args.length); - children.push(Node.Creator.operator('^', [baseNode, exponentNode])); - return; - } - } - children.push(child); - }); - - newNode.args[0] = Node.Creator.operator('*', children); - return Node.Status.nodeChanged( - ChangeTypes.CONVERT_MULTIPLICATION_TO_EXPONENT, node, newNode); - } - - return Node.Status.noChange(node); -} - -// Given an nthRoot node with a multiplication under the radicand, will -// distribute the nthRoot to all the arguments under the radicand as a step -// e.g. nthRoot(2 * x^2, 2) -> nthRoot(2) * nthRoot(x^2) -function distributeNthRoot(node) { - let newNode = clone(node); - const radicandNode = getRadicandNode(node); - const rootNode = getRootNode(node); - - const children = []; - for (let i = 0; i < radicandNode.args.length; i++) { - const child = radicandNode.args[i]; - children.push(Node.Creator.nthRoot(child, rootNode)); - } - - newNode = Node.Creator.operator('*', children); - return Node.Status.nodeChanged( - ChangeTypes.DISTRIBUTE_NTH_ROOT, node, newNode); -} - -// Given a multiplication node of nthRoots (with the same root) -// will evaluate the nthRoot of each child as a substep -// e.g. nthRoot(2) * nthRoot(x^2) -> nthRoot(2) * x -function evaluateNthRootForChildren(node) { - const newNode = clone(node); - - const substeps = []; - for (let i = 0; i < newNode.args.length; i++) { - const child = newNode.args[i]; - const childNodeStatus = nthRoot(child); - if (childNodeStatus.hasChanged()) { - newNode.args[i] = childNodeStatus.newNode; - substeps.push(Node.Status.childChanged(newNode, childNodeStatus, i)); - } - } - - if (substeps.length === 0) { - return Node.Status.noChange(node); - } - else if (substeps.length === 1) { - return substeps[0]; - } - else { - return Node.Status.nodeChanged( - ChangeTypes.EVALUATE_DISTRIBUTED_NTH_ROOT, node, newNode, true, substeps); - } -} - -// Given a multiplication node, with children including nthRoots, will combine -// the nodes with the same radicand as a step -// e.g. 2 * nthRoot(2) * nthRoot(x) -> 2 * nthRoot(2 * x) -// Assumes that all the roots are the same (that this is occuring right -// after distributeNthRoot and evaluateNthRootForChildren) -function combineRoots(node) { - let newNode = clone(node); - - let rootNode; - const children = []; - const radicandArgs = []; - for (let i = 0; i < newNode.args.length; i++) { - const child = newNode.args[i]; - if (Node.Type.isFunction(child, 'nthRoot')) { - radicandArgs.push(child.args[0]); - rootNode = getRootNode(child); - } - else { - children.push(child); - } - } - - if (children.length > 0) { - if (radicandArgs.length > 0) { - const radicandNode = radicandArgs.length === 1 ? - radicandArgs[0] : Node.Creator.operator('*', radicandArgs); - children.push(Node.Creator.nthRoot(radicandNode, rootNode)); - } - - newNode = Node.Creator.operator('*', children); - if (!newNode.equals(node)) { - return Node.Status.nodeChanged( - ChangeTypes.COMBINE_UNDER_ROOT, node, newNode); - } - } - - // if there are no items moved out of the root, then nothing has changed - return Node.Status.noChange(node); -} - -// Returns the nthRoot evaluated on a constant node -// Potentially factors the constant node into primes, and calls -// nthRootMultiplication on the new nthRoot -function nthRootConstant(node) { - let newNode = clone(node); - const radicandNode = getRadicandNode(node); - const rootNode = getRootNode(node); - - if (Negative.isNegative(radicandNode)) { - return Node.Status.noChange(node); - } - else if (!Node.Type.isConstant(rootNode) || Negative.isNegative(rootNode)) { - return Node.Status.noChange(node); - } - - const radicandValue = parseFloat(radicandNode.value); - const rootValue = parseFloat(rootNode.value); - const nthRootValue = math.nthRoot(radicandValue, rootValue); - // Perfect root e.g. nthRoot(4, 2) = 2 - if (nthRootValue % 1 === 0) { - newNode = Node.Creator.constant(nthRootValue); - return Node.Status.nodeChanged( - ChangeTypes.NTH_ROOT_VALUE, node, newNode); - } - // Try to find if we can simplify by finding factors that can be - // pulled out of the radical - else { - // convert the number into the product of its prime factors - const factors = ConstantFactors.getPrimeFactors(radicandValue); - if (factors.length > 1) { - let substeps = []; - const factorNodes = factors.map(Node.Creator.constant); - - newNode.args[0] = Node.Creator.operator('*', factorNodes); - substeps.push(Node.Status.nodeChanged( - ChangeTypes.FACTOR_INTO_PRIMES, node, newNode)); - - // run nthRoot on the new node - const nodeStatus = nthRootMultiplication(newNode); - if (nodeStatus.hasChanged()) { - substeps = substeps.concat(nodeStatus.substeps); - newNode = nodeStatus.newNode; - - return Node.Status.nodeChanged( - ChangeTypes.NTH_ROOT_VALUE, node, newNode, true, substeps); - } - } - } - - return Node.Status.noChange(node); -} - -// Helpers - -// Given an nthRoot node, will return the root node. -// The root node is the second child of the nthRoot node, but if one doesn't -// exist, we assume it's a square root and return 2. -function getRootNode(node) { - if (!Node.Type.isFunction(node, 'nthRoot')) { - throw Error('Expected nthRoot'); - } - - return node.args.length === 2 ? node.args[1] : Node.Creator.constant(2); -} - -// Given an nthRoot node, will return the radicand node. -function getRadicandNode(node) { - if (!Node.Type.isFunction(node, 'nthRoot')) { - throw Error('Expected nthRoot'); - } - - return node.args[0]; -} - -// Sorts nodes, ordering constants nodes from smallest to largest and symbol -// nodes after -function sortNodes(a, b) { - if (Node.Type.isConstant(a) && Node.Type.isConstant(b)) { - return parseFloat(a.value) - parseFloat(b.value); - } - else if (Node.Type.isConstant(a)) { - return -1; - } - else if (Node.Type.isConstant(b)) { - return 1; - } - return 0; -} - -// Simple helper function which determines a node is a multiplication node -// of all equal nodes -function isMultiplicationOfEqualNodes(node) { - if (!Node.Type.isOperator(node) || node.op !== '*') { - return false; - } - - // return if they are all equal nodes - return node.args.reduce((a, b) => { - return a.equals(b); - }); - -} - -module.exports = nthRoot; diff --git a/lib/simplifyExpression/index.js b/lib/simplifyExpression/index.js deleted file mode 100644 index ab919afb..00000000 --- a/lib/simplifyExpression/index.js +++ /dev/null @@ -1,18 +0,0 @@ -const math = require('mathjs'); -const stepThrough = require('./stepThrough'); - -function simplifyExpressionString(expressionString, debug=false) { - let exprNode; - try { - exprNode = math.parse(expressionString); - } - catch (err) { - return []; - } - if (exprNode) { - return stepThrough(exprNode, debug); - } - return []; -} - -module.exports = simplifyExpressionString; diff --git a/lib/simplifyExpression/multiplyFractionsSearch/index.js b/lib/simplifyExpression/multiplyFractionsSearch/index.js deleted file mode 100644 index e19165d9..00000000 --- a/lib/simplifyExpression/multiplyFractionsSearch/index.js +++ /dev/null @@ -1,56 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -// If `node` is a product of terms where some are fractions (but none are -// polynomial terms), multiplies them together. -// e.g. 2 * 5/x -> (2*5)/x -// e.g. 3 * 1/5 * 5/9 = (3*1*5)/(5*9) -// TODO: add a step somewhere to remove common terms in numerator and -// denominator (so the 5s would cancel out on the next step after this) -// This step must happen after things have been distributed, or else the answer -// will be formatted badly, so it's a tree search of its own. -// Returns a Node.Status object. -const search = TreeSearch.postOrder(multiplyFractions); - -// If `node` is a product of terms where some are fractions (but none are -// polynomial terms), multiplies them together. -// e.g. 2 * 5/x -> (2*5)/x -// e.g. 3 * 1/5 * 5/9 = (3*1*5)/(5*9) -// Returns a Node.Status object. -function multiplyFractions(node) { - if (!Node.Type.isOperator(node) || node.op !== '*') { - return Node.Status.noChange(node); - } - const atLeastOneFraction = node.args.some( - arg => Node.Type.isOperator(arg, '/')); - const hasPolynomialTerms = node.args.some( - arg => Node.PolynomialTerm.isPolynomialTerm(arg)); - if (!atLeastOneFraction || hasPolynomialTerms) { - return Node.Status.noChange(node); - } - - const numeratorArgs = []; - const denominatorArgs = []; - node.args.forEach(operand => { - if (Node.Type.isOperator(operand, '/')) { - numeratorArgs.push(operand.args[0]); - denominatorArgs.push(operand.args[1]); - } - else { - numeratorArgs.push(operand); - } - }); - - const newNumerator = Node.Creator.parenthesis( - Node.Creator.operator('*', numeratorArgs)); - const newDenominator = denominatorArgs.length === 1 - ? denominatorArgs[0] - : Node.Creator.parenthesis(Node.Creator.operator('*', denominatorArgs)); - - const newNode = Node.Creator.operator('/', [newNumerator, newDenominator]); - return Node.Status.nodeChanged( - ChangeTypes.MULTIPLY_FRACTIONS, node, newNode); -} - -module.exports = search; diff --git a/lib/simplifyExpression/simplify.js b/lib/simplifyExpression/simplify.js deleted file mode 100644 index 37090a16..00000000 --- a/lib/simplifyExpression/simplify.js +++ /dev/null @@ -1,37 +0,0 @@ -const math = require('mathjs'); - -const checks = require('../checks'); -const flattenOperands = require('../util/flattenOperands'); -const print = require('../util/print'); -const removeUnnecessaryParens = require('../util/removeUnnecessaryParens'); -const stepThrough = require('./stepThrough'); - - -// Given a mathjs expression node, steps through simplifying the expression. -// Returns the simplified expression node. -function simplify(node, debug=false) { - if (checks.hasUnsupportedNodes(node)) { - return node; - } - - const steps = stepThrough(node, debug); - let simplifiedNode; - if (steps.length > 0) { - simplifiedNode = steps.pop().newNode; - } - else { - // removing parens isn't counted as a step, so try it here - simplifiedNode = removeUnnecessaryParens(flattenOperands(node), true); - } - // unflatten the node. - return unflatten(simplifiedNode); -} - -// Unflattens a node so it is in the math.js style, by printing and parsing it -// again -function unflatten(node) { - return math.parse(print(node)); -} - - -module.exports = simplify; diff --git a/lib/simplifyExpression/stepThrough.js b/lib/simplifyExpression/stepThrough.js deleted file mode 100644 index 9a5dad53..00000000 --- a/lib/simplifyExpression/stepThrough.js +++ /dev/null @@ -1,133 +0,0 @@ -const checks = require('../checks'); -const Node = require('../node'); -const Status = require('../node/Status'); - -const arithmeticSearch = require('./arithmeticSearch'); -const basicsSearch = require('./basicsSearch'); -const breakUpNumeratorSearch = require('./breakUpNumeratorSearch'); -const collectAndCombineSearch = require('./collectAndCombineSearch'); -const distributeSearch = require('./distributeSearch'); -const divisionSearch = require('./divisionSearch'); -const fractionsSearch = require('./fractionsSearch'); -const functionsSearch = require('./functionsSearch'); -const multiplyFractionsSearch = require('./multiplyFractionsSearch'); - -const clone = require('../util/clone'); -const flattenOperands = require('../util/flattenOperands'); -const print = require('../util/print'); -const removeUnnecessaryParens = require('../util/removeUnnecessaryParens'); - -// Given a mathjs expression node, steps through simplifying the expression. -// Returns a list of details about each step. -function stepThrough(node, debug=false) { - if (debug) { - // eslint-disable-next-line - console.log('\n\nSimplifying: ' + print(node, false, true)); - } - - if (checks.hasUnsupportedNodes(node)) { - return []; - } - - let nodeStatus; - const steps = []; - - const originalExpressionStr = print(node); - const MAX_STEP_COUNT = 20; - let iters = 0; - - // Now, step through the math expression until nothing changes - nodeStatus = step(node); - while (nodeStatus.hasChanged()) { - if (debug) { - logSteps(nodeStatus); - } - steps.push(removeUnnecessaryParensInStep(nodeStatus)); - const nextNode = Status.resetChangeGroups(nodeStatus.newNode); - nodeStatus = step(nextNode); - if (iters++ === MAX_STEP_COUNT) { - // eslint-disable-next-line - console.error('Math error: Potential infinite loop for expression: ' + - originalExpressionStr + ', returning no steps'); - return []; - } - } - - return steps; -} - -// Given a mathjs expression node, performs a single step to simplify the -// expression. Returns a Node.Status object. -function step(node) { - let nodeStatus; - - node = flattenOperands(node); - node = removeUnnecessaryParens(node, true); - - const simplificationTreeSearches = [ - // Basic simplifications that we always try first e.g. (...)^0 => 1 - basicsSearch, - // Simplify any division chains so there's at most one division operation. - // e.g. 2/x/6 -> 2/(x*6) e.g. 2/(x/6) => 2 * 6/x - divisionSearch, - // Adding fractions, cancelling out things in fractions - fractionsSearch, - // e.g. 2 + 2 => 4 - arithmeticSearch, - // e.g. addition: 2x + 4x^2 + x => 4x^2 + 3x - // e.g. multiplication: 2x * x * x^2 => 2x^3 - collectAndCombineSearch, - // e.g. (2 + x) / 4 => 2/4 + x/4 - breakUpNumeratorSearch, - // e.g. 3/x * 2x/5 => (3 * 2x) / (x * 5) - multiplyFractionsSearch, - // e.g. (2x + 3)(x + 4) => 2x^2 + 11x + 12 - distributeSearch, - // e.g. abs(-4) => 4 - functionsSearch, - ]; - - for (let i = 0; i < simplificationTreeSearches.length; i++) { - nodeStatus = simplificationTreeSearches[i](node); - // Always update node, since there might be changes that didn't count as - // a step. Remove unnecessary parens, in case one a step results in more - // parens than needed. - node = removeUnnecessaryParens(nodeStatus.newNode, true); - if (nodeStatus.hasChanged()) { - node = flattenOperands(node); - nodeStatus.newNode = clone(node); - return nodeStatus; - } - else { - node = flattenOperands(node); - } - } - return Node.Status.noChange(node); -} - -// Removes unnecessary parens throughout the steps. -// TODO: Ideally this would happen in NodeStatus instead. -function removeUnnecessaryParensInStep(nodeStatus) { - if (nodeStatus.substeps.length > 0) { - nodeStatus.substeps.map(removeUnnecessaryParensInStep); - } - - nodeStatus.oldNode = removeUnnecessaryParens(nodeStatus.oldNode, true); - nodeStatus.newNode = removeUnnecessaryParens(nodeStatus.newNode, true); - return nodeStatus; -} - -function logSteps(nodeStatus) { - // eslint-disable-next-line - console.log(nodeStatus.changeType); - // eslint-disable-next-line - console.log(print(nodeStatus.newNode) + '\n'); - - if (nodeStatus.substeps.length > 0) { - // eslint-disable-next-line - console.log('\nsubsteps: '); - nodeStatus.substeps.forEach(substep => substep); - } -} - -module.exports = stepThrough; diff --git a/lib/solveEquation/EquationOperations.js b/lib/solveEquation/EquationOperations.js deleted file mode 100644 index 78d29a19..00000000 --- a/lib/solveEquation/EquationOperations.js +++ /dev/null @@ -1,232 +0,0 @@ -// Operations on equation nodes - -const ChangeTypes = require('../ChangeTypes'); -const clone = require('../util/clone'); -const Equation = require('../equation/Equation'); -const EquationStatus = require('../equation/Status'); -const Negative = require('../Negative'); -const Node = require('../node'); -const Symbols = require('../Symbols'); - -const COMPARATOR_TO_INVERSE = { - '>': '<', - '>=': '<=', - '<': '>', - '<=': '>=', - '=': '=' -}; - -const EquationOperations = {}; - -// Ensures that the given equation has the given symbolName on the left side, -// by swapping the right and left sides if it is only in the right side. -// So 3 = x would become x = 3. -EquationOperations.ensureSymbolInLeftNode = function(equation, symbolName) { - const leftSideSymbolTerm = Symbols.getLastSymbolTerm( - equation.leftNode, symbolName); - const rightSideSymbolTerm = Symbols.getLastSymbolTerm( - equation.rightNode, symbolName); - - if (!leftSideSymbolTerm) { - if (rightSideSymbolTerm) { - const comparator = COMPARATOR_TO_INVERSE[equation.comparator]; - const oldEquation = equation; - const newEquation = new Equation( - equation.rightNode, equation.leftNode, comparator); - // no change groups are set for this step because everything changes, so - // they wouldn't be pedagogically helpful. - return new EquationStatus( - ChangeTypes.SWAP_SIDES, oldEquation, newEquation); - } - else { - throw Error('No term with symbol: ' + symbolName); - } - } - return EquationStatus.noChange(equation); -}; - -// TODO: Ensures that a symbol is not in the denominator by multiplying -// both sides by the whatever order of the symbol necessary. -// This is blocked on the simplifying functionality of canceling symbols in -// fractions (needs factoring for full canceling support) -EquationOperations.removeSymbolFromDenominator = function(equation) { - // pass for now - return EquationStatus.noChange(equation); -}; - -// Removes the given symbolName from the right side by adding or subtracting -// it from both sides as appropriate. -// e.g. 2x = 3x + 5 --> 2x - 3x = 5 -// There are actually no cases where we'd remove symbols from the right side -// by multiplying or dividing by a symbol term. -// TODO: support inverting functions e.g. sqrt, ^, log etc. -EquationOperations.removeSymbolFromRightSide = function(equation, symbolName) { - const rightNode = equation.rightNode; - let symbolTerm = Symbols.getLastSymbolTerm(rightNode, symbolName); - - let inverseOp, inverseTerm, changeType; - if (!symbolTerm){ - return EquationStatus.noChange(equation); - } - - // Clone it so that any operations on it don't affect the node already - // in the equation - symbolTerm = clone(symbolTerm); - - if (Node.PolynomialTerm.isPolynomialTerm(rightNode)) { - if (Negative.isNegative(symbolTerm)) { - inverseOp = '+'; - changeType = ChangeTypes.ADD_TO_BOTH_SIDES; - inverseTerm = Negative.negate(symbolTerm); - } - else { - inverseOp = '-'; - changeType = ChangeTypes.SUBTRACT_FROM_BOTH_SIDES; - inverseTerm = symbolTerm; - } - } - else if (Node.Type.isOperator(rightNode)) { - if (rightNode.op === '+') { - if (Negative.isNegative(symbolTerm)) { - inverseOp = '+'; - changeType = ChangeTypes.ADD_TO_BOTH_SIDES; - inverseTerm = Negative.negate(symbolTerm); - } - else { - inverseOp = '-'; - changeType = ChangeTypes.SUBTRACT_FROM_BOTH_SIDES; - inverseTerm = symbolTerm; - } - } - else { - // Note that operator '-' won't show up here because subtraction is - // flattened into adding the negative. See 'TRICKY catch' in the README - // for more details. - throw Error('Unsupported operation: ' + symbolTerm.op); - } - } - else if (Node.Type.isUnaryMinus(rightNode)) { - inverseOp = '+'; - changeType = ChangeTypes.ADD_TO_BOTH_SIDES; - inverseTerm = symbolTerm.args[0]; - } - else { - throw Error('Unsupported node type: ' + rightNode.type); - } - return performTermOperationOnEquation( - equation, inverseOp, inverseTerm, changeType); -}; - -// Isolates the given symbolName to the left side by adding, multiplying, subtracting -// or dividing all other symbols and constants from both sides appropriately -// TODO: support inverting functions e.g. sqrt, ^, log etc. -EquationOperations.isolateSymbolOnLeftSide = function(equation, symbolName) { - const leftNode = equation.leftNode; - let nonSymbolTerm = Symbols.getLastNonSymbolTerm(leftNode, symbolName); - - let inverseOp, inverseTerm, changeType; - if (!nonSymbolTerm) { - return EquationStatus.noChange(equation); - } - - // Clone it so that any operations on it don't affect the node already - // in the equation - nonSymbolTerm = clone(nonSymbolTerm); - - if (Node.Type.isOperator(leftNode)) { - if (leftNode.op === '+') { - if (Negative.isNegative(nonSymbolTerm)) { - inverseOp = '+'; - changeType = ChangeTypes.ADD_TO_BOTH_SIDES; - inverseTerm = Negative.negate(nonSymbolTerm); - } - else { - inverseOp = '-'; - changeType = ChangeTypes.SUBTRACT_FROM_BOTH_SIDES; - inverseTerm = nonSymbolTerm; - } - } - else if (leftNode.op === '*') { - if (Node.Type.isConstantFraction(nonSymbolTerm)) { - inverseOp = '*'; - changeType = ChangeTypes.MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION; - inverseTerm = Node.Creator.operator( - '/', [nonSymbolTerm.args[1], nonSymbolTerm.args[0]]); - } - else { - inverseOp = '/'; - changeType = ChangeTypes.DIVIDE_FROM_BOTH_SIDES; - inverseTerm = nonSymbolTerm; - } - } - else if (leftNode.op === '/') { - // The non symbol term is always a fraction because it's the - // coefficient of our symbol term. - // If the numerator is 1, we multiply both sides by the denominator, - // otherwise we multiply by the inverse - if (['1', '-1'].indexOf(nonSymbolTerm.args[0].value) !== -1) { - inverseOp = '*'; - changeType = ChangeTypes.MULTIPLY_TO_BOTH_SIDES; - inverseTerm = nonSymbolTerm.args[1]; - } - else { - inverseOp = '*'; - changeType = ChangeTypes.MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION; - inverseTerm = Node.Creator.operator( - '/', [nonSymbolTerm.args[1], nonSymbolTerm.args[0]]); - } - } - else if (leftNode.op === '^') { - // TODO: support roots - return EquationStatus.noChange(equation); - } - else { - throw Error('Unsupported operation: ' + leftNode.op); - } - } - else if (Node.Type.isUnaryMinus(leftNode)) { - inverseOp = '*'; - changeType = ChangeTypes.MULTIPLY_BOTH_SIDES_BY_NEGATIVE_ONE; - inverseTerm = Node.Creator.constant(-1); - } - else { - throw Error('Unsupported node type: ' + leftNode.type); - } - - return performTermOperationOnEquation( - equation, inverseOp, inverseTerm, changeType); -}; - -// Modifies the left and right sides of an equation by `op`-ing `term` -// to both sides. Returns an Status object. -function performTermOperationOnEquation(equation, op, term, changeType) { - const oldEquation = equation.clone(); - - const leftTerm = clone(term); - const rightTerm = clone(term); - const leftNode = performTermOperationOnExpression( - equation.leftNode, op, leftTerm); - const rightNode = performTermOperationOnExpression( - equation.rightNode, op, rightTerm); - - let comparator = equation.comparator; - if (Negative.isNegative(term) && (op === '*' || op === '/')) { - comparator = COMPARATOR_TO_INVERSE[comparator]; - } - - const newEquation = new Equation(leftNode, rightNode, comparator); - return new EquationStatus(changeType, oldEquation, newEquation); -} - -// Performs an operation of a term on an entire given expression -function performTermOperationOnExpression(expression, op, term) { - const node = (Node.Type.isOperator(expression) ? - Node.Creator.parenthesis(expression) : expression); - - term.changeGroup = 1; - const newNode = Node.Creator.operator(op, [node, term]); - - return newNode; -} - -module.exports = EquationOperations; diff --git a/lib/solveEquation/stepThrough.js b/lib/solveEquation/stepThrough.js deleted file mode 100644 index 969836c2..00000000 --- a/lib/solveEquation/stepThrough.js +++ /dev/null @@ -1,263 +0,0 @@ -const ChangeTypes = require('../ChangeTypes'); -const checks = require('../checks'); -const Equation = require('../equation/Equation'); -const EquationOperations = require('./EquationOperations'); -const EquationStatus = require('../equation/Status'); -const evaluate = require('../util/evaluate'); -const flattenOperands = require('../util/flattenOperands'); -const Node = require('../node'); -const removeUnnecessaryParens = require('../util/removeUnnecessaryParens'); -const simplifyExpressionNode = require('../simplifyExpression/stepThrough'); -const Symbols = require('../Symbols'); - -const COMPARATOR_TO_FUNCTION = { - '=': function(left, right) { return left === right; }, - '>': function(left, right) { return left > right; }, - '>=': function(left, right) { return left >= right; }, - '<': function(left, right) { return left < right; }, - '<=': function(left, right) { return left <= right; }, -}; - -// Given a leftNode, rightNode and a comparator, will return the steps to get -// the solution. Possible solutions include: -// - solving for a variable (e.g. 'x=3' for '3x=4+5') -// - the result of comparing values (e.g. 'true' for 3 = 3, 'false' for 3 < 2) -function stepThrough(leftNode, rightNode, comparator, debug=false) { - let equation = new Equation(leftNode, rightNode, comparator); - - if (debug) { - // eslint-disable-next-line - console.log('\n\nSolving: ' + equation.print(false, true)); - } - - // we can't solve/find steps if there are any unsupported nodes - if (checks.hasUnsupportedNodes(equation.leftNode) || - checks.hasUnsupportedNodes(equation.rightNode)) { - return []; - } - - const symbolSet = Symbols.getSymbolsInEquation(equation); - - if (symbolSet.size === 0) { - return solveConstantEquation(equation, debug); - } - const symbolName = symbolSet.values().next().value; - - let equationStatus; - let steps = []; - - const originalEquationStr = equation.print(); - const MAX_STEP_COUNT = 20; - let iters = 0; - - // Step through the math equation until nothing changes - do { - steps = addSimplificationSteps(steps, equation, debug); - if (steps.length > 0) { - const lastStep = steps[steps.length - 1]; - equation = Equation.createEquationFromString( - lastStep.newEquation.print(), equation.comparator); - } - - equation.leftNode = flattenOperands(equation.leftNode); - equation.rightNode = flattenOperands(equation.rightNode); - - // at this point, the symbols might have cancelled out. - if (Symbols.getSymbolsInEquation(equation).size === 0) { - return solveConstantEquation(equation, debug, steps); - } - - try { - equationStatus = step(equation, symbolName); - } - catch (e) { - // This error happens for some math that we don't support - if (e.message.startsWith('No term with symbol: ')) { - // eslint-disable-next-line - console.error('Math error: ' + e.message + ', returning no steps'); - return []; - } - else { - throw e; // bubble up - } - } - - if (equationStatus.hasChanged()) { - if (equationStatus.newEquation.print().length > 300) { - // eslint-disable-next-line - throw Error('Math error: Potential infinite loop for equation ' + - originalEquationStr + '. It reached over 300 characters '+ - ' long, so returning no steps'); - } - if (debug) { - logSteps(equationStatus); - } - steps.push(equationStatus); - } - - equation = EquationStatus.resetChangeGroups(equationStatus.newEquation); - if (iters++ === MAX_STEP_COUNT) { - // eslint-disable-next-line - console.error('Math error: Potential infinite loop for equation: ' + - originalEquationStr + ', returning no steps'); - return []; - } - } while (equationStatus.hasChanged()); - - return steps; -} - -// Given an equation of constants, will simplify both sides, returning -// the steps and the result of the equation e.g. 'True' or 'False' -function solveConstantEquation(equation, debug, steps=[]) { - const compareFunction = COMPARATOR_TO_FUNCTION[equation.comparator]; - - if (!compareFunction) { - throw Error('Unexpected comparator'); - } - - steps = addSimplificationSteps(steps, equation, true, debug); - if (steps.length > 0) { - const lastStep = steps[steps.length - 1]; - equation = Equation.createEquationFromString( - lastStep.newEquation.print(), equation.comparator); - } - - // If the left or right side didn't have any steps, unnecessary parens - // might not have been removed, so do that now. - equation.leftNode = removeUnnecessaryParens(equation.leftNode); - equation.rightNode = removeUnnecessaryParens(equation.rightNode); - - if (!Node.Type.isConstantOrConstantFraction(equation.leftNode, true) || - !Node.Type.isConstantOrConstantFraction(equation.rightNode, true)) { - throw Error('Expected both nodes to be constants, instead got: ' + - equation.print()); - } - - const leftValue = evaluate(equation.leftNode); - const rightValue = evaluate(equation.rightNode); - let changeType; - if (compareFunction(leftValue, rightValue)) { - changeType = ChangeTypes.STATEMENT_IS_TRUE; - } - else { - changeType = ChangeTypes.STATEMENT_IS_FALSE; - } - - // there's no oldEquation or change groups because nothing actually changes - // here, it's just a final step that states the solution - const equationStatus = new EquationStatus(changeType, null, equation); - if (debug) { - logSteps(equationStatus); - } - steps.push(equationStatus); - return steps; -} - -// Given a symbol and an equation, performs a single step to -// solve for the symbol. Returns an Status object. -function step(equation, symbolName) { - const solveFunctions = [ - // ensure the symbol is always on the left node - EquationOperations.ensureSymbolInLeftNode, - // get rid of denominators that have the symbol - EquationOperations.removeSymbolFromDenominator, - // remove the symbol from the right side - EquationOperations.removeSymbolFromRightSide, - // isolate the symbol on the left side - EquationOperations.isolateSymbolOnLeftSide, - ]; - - for (let i = 0; i < solveFunctions.length; i++) { - const equationStatus = solveFunctions[i](equation, symbolName); - - if (equationStatus.hasChanged()) { - return equationStatus; - } - } - return EquationStatus.noChange(equation); -} - -// Simplifies the equation and returns the simplification steps -function addSimplificationSteps(steps, equation, debug=false) { - let oldEquation = equation.clone(); - - const leftSteps = simplifyExpressionNode(equation.leftNode, false); - const leftSubSteps = []; - for (let i = 0; i < leftSteps.length; i++) { - const step = leftSteps[i]; - leftSubSteps.push(EquationStatus.addLeftStep(equation, step)); - } - if (leftSubSteps.length === 1) { - const step = leftSubSteps[0]; - if (debug) { - logSteps(step); - } - steps.push(step); - } - else if (leftSubSteps.length > 1) { - const lastStep = leftSubSteps[leftSubSteps.length - 1]; - const finalEquation = EquationStatus.resetChangeGroups(lastStep.newEquation); - // no change groups are set here - too much is changing for it to be useful - const simplifyStatus = new EquationStatus( - ChangeTypes.SIMPLIFY_LEFT_SIDE, - oldEquation, finalEquation, leftSubSteps); - if (debug) { - logSteps(simplifyStatus); - } - steps.push(simplifyStatus); - } - - // update `equation` to have the new simplified left node - if (steps.length > 0) { - equation = EquationStatus.resetChangeGroups( - steps[steps.length - 1 ].newEquation); - } - - // the updated equation from simplifing the left side is the old equation - // (ie the "before" of the before and after) for simplifying the right side. - oldEquation = equation.clone(); - - const rightSteps = simplifyExpressionNode(equation.rightNode, false); - const rightSubSteps = []; - for (let i = 0; i < rightSteps.length; i++) { - const step = rightSteps[i]; - rightSubSteps.push(EquationStatus.addRightStep(equation, step)); - } - if (rightSubSteps.length === 1) { - const step = rightSubSteps[0]; - if (debug) { - logSteps(step); - } - steps.push(step); - } - else if (rightSubSteps.length > 1) { - const lastStep = rightSubSteps[rightSubSteps.length - 1]; - const finalEquation = EquationStatus.resetChangeGroups(lastStep.newEquation); - // no change groups are set here - too much is changing for it to be useful - const simplifyStatus = new EquationStatus( - ChangeTypes.SIMPLIFY_RIGHT_SIDE, - oldEquation, finalEquation, rightSubSteps); - if (debug) { - logSteps(simplifyStatus); - } - steps.push(simplifyStatus); - } - - return steps; -} - -function logSteps(equationStatus) { - // eslint-disable-next-line - console.log('\n' + equationStatus.changeType); - // eslint-disable-next-line - console.log(equationStatus.newEquation.print()); - if (equationStatus.substeps.length > 0) { - // eslint-disable-next-line - console.log('\n substeps: '); - equationStatus.substeps.forEach(logSteps); - } -} - - -module.exports = stepThrough; diff --git a/lib/util/Util.js b/lib/util/Util.js deleted file mode 100644 index 033395ab..00000000 --- a/lib/util/Util.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - Various utility functions used in the math stepper - */ -const Util = {}; - -// Adds `value` to a list in `dict`, creating a new list if the key isn't in -// the dictionary yet. Returns the updated dictionary. -Util.appendToArrayInObject = function(dict, key, value) { - if (dict[key]) { - dict[key].push(value); - } - else { - dict[key] = [value]; - } - return dict; -}; - -module.exports = Util; diff --git a/lib/util/removeUnnecessaryParens.js b/lib/util/removeUnnecessaryParens.js deleted file mode 100644 index 3bbf796d..00000000 --- a/lib/util/removeUnnecessaryParens.js +++ /dev/null @@ -1,174 +0,0 @@ -const checks = require('../checks'); -const LikeTermCollector = require('../simplifyExpression/collectAndCombineSearch/LikeTermCollector'); -const Node = require('../node'); - -// Removes any parenthesis around nodes that can't be resolved further. -// Input must be a top level expression. -// Returns a node. -function removeUnnecessaryParens(node, rootNode=false) { - // Parens that wrap everything are redundant. - // NOTE: removeUnnecessaryParensSearch recursively removes parens that aren't - // needed, while this step only applies to the very top level expression. - // e.g. (2 + 3) * 4 can't become 2 + 3 * 4, but if (2 + 3) as a top level - // expression can become 2 + 3 - if (rootNode) { - while (Node.Type.isParenthesis(node)) { - node = node.content; - } - } - return removeUnnecessaryParensSearch(node); -} - -// Recursively moves parenthesis around nodes that can't be resolved further if -// it doesn't change the value of the expression. Returns a node. -// NOTE: after this function is called, every parenthesis node in the -// tree should always have an operator node or unary minus as its child. -function removeUnnecessaryParensSearch(node) { - if (Node.Type.isOperator(node)) { - return removeUnnecessaryParensInOperatorNode(node); - } - else if (Node.Type.isFunction(node)) { - return removeUnnecessaryParensInFunctionNode(node); - } - else if (Node.Type.isParenthesis(node)) { - return removeUnnecessaryParensInParenthesisNode(node); - } - else if (Node.Type.isConstant(node, true) || Node.Type.isSymbol(node)) { - return node; - } - else if (Node.Type.isUnaryMinus(node)) { - const content = node.args[0]; - node.args[0] = removeUnnecessaryParensSearch(content); - return node; - } - else { - throw Error('Unsupported node type: ' + node.type); - } -} - -// Removes unncessary parens for each operator in an operator node, and removes -// unncessary parens around operators that can't be simplified further. -// Returns a node. -function removeUnnecessaryParensInOperatorNode(node) { - // Special case: if the node is an exponent node and the base - // is an operator, we should keep the parentheses for the base. - // e.g. (2x)^2 -> (2x)^2 instead of 2x^2 - if (node.op === '^' && Node.Type.isParenthesis(node.args[0])) { - const base = node.args[0]; - if (Node.Type.isOperator(base.content)) { - base.content = removeUnnecessaryParensSearch(base.content); - node.args[1] = removeUnnecessaryParensSearch(node.args[1]); - - return node; - } - } - - node.args.forEach((child, i) => { - node.args[i] = removeUnnecessaryParensSearch(child); - }); - - // Sometimes, parens are around expressions that have been simplified - // all they can be. If that expression is part of an addition or subtraction - // operation, we can remove the parenthesis. - // e.g. (x+4) + 12 -> x+4 + 12 - if (node.op === '+') { - node.args.forEach((child, i) => { - if (Node.Type.isParenthesis(child) && - !canCollectOrCombine(child.content)) { - // remove the parens by replacing the child node (in its args list) - // with its content - node.args[i] = child.content; - } - }); - } - // This is different from addition because when subtracting a group of terms - //in parenthesis, we want to distribute the subtraction. - // e.g. `(2 + x) - (1 + x)` => `2 + x - (1 + x)` not `2 + x - 1 + x` - else if (node.op === '-') { - if (Node.Type.isParenthesis(node.args[0]) && - !canCollectOrCombine(node.args[0].content)) { - node.args[0] = node.args[0].content; - } - } - - return node; -} - -// Removes unncessary parens for each argument in a function node. -// Returns a node. -function removeUnnecessaryParensInFunctionNode(node) { - node.args.forEach((child, i) => { - if (Node.Type.isParenthesis(child)) { - child = child.content; - } - node.args[i] = removeUnnecessaryParensSearch(child); - }); - - return node; -} - - -// Parentheses are unnecessary when their content is a constant e.g. (2) -// or also a parenthesis node, e.g. ((2+3)) - this removes those parentheses. -// Note that this means that the type of the content of a ParenthesisNode after -// this step should now always be an OperatorNode (including unary minus). -// Returns a node. -function removeUnnecessaryParensInParenthesisNode(node) { - // polynomials terms can be complex trees (e.g. 3x^2/5) but don't need parens - // around them - if (Node.PolynomialTerm.isPolynomialTerm(node.content)) { - // also recurse to remove any unnecessary parens within the term - // (e.g. the exponent might have parens around it) - if (node.content.args) { - node.content.args.forEach((arg, i) => { - node.content.args[i] = removeUnnecessaryParensSearch(arg); - }); - } - node = node.content; - } - // If the content is just one symbol or constant, the parens are not - // needed. - else if (Node.Type.isConstant(node.content, true) || - Node.Type.isIntegerFraction(node.content) || - Node.Type.isSymbol(node.content)) { - node = node.content; - } - // If the content is just one function call, the parens are not needed. - else if (Node.Type.isFunction(node.content)) { - node = node.content; - node = removeUnnecessaryParensSearch(node); - } - // If there is an operation within the parens, then the parens are - // likely needed. So, recurse. - else if (Node.Type.isOperator(node.content)) { - node.content = removeUnnecessaryParensSearch(node.content); - // exponent nodes don't need parens around them - if (node.content.op === '^') { - node = node.content; - } - } - // If the content is also parens, we have doubly nested parens. First - // recurse on the child node, then set the current node equal to its child - // to get rid of the extra parens. - else if (Node.Type.isParenthesis(node.content)) { - node = removeUnnecessaryParensSearch(node.content); - } - else if (Node.Type.isUnaryMinus(node.content)) { - node.content = removeUnnecessaryParensSearch(node.content); - } - else { - throw Error('Unsupported node type: ' + node.content.type); - } - - return node; -} - -// Returns true if any of the collect or combine steps can be applied to the -// expression tree `node`. -function canCollectOrCombine(node) { - return LikeTermCollector.canCollectLikeTerms(node) || - checks.resolvesToConstant(node) || - checks.canSimplifyPolynomialTerms(node); -} - -module.exports = removeUnnecessaryParens; diff --git a/package.json b/package.json index d54df282..494a4256 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Step by step math solutions", "main": "index.js", "dependencies": { + "@types/mathjs": "0.0.34", "mathjs": "3.11.2" }, "engines": { @@ -13,7 +14,8 @@ "eslint": "^3.10.2", "eslint-config-google": "^0.7.0", "eslint-plugin-sort-requires": "^2.1.0", - "mocha": "2.4.5" + "mocha": "2.4.5", + "typescript": "^2.2.2" }, "scripts": { "lint": "node_modules/.bin/eslint .", diff --git a/src/ChangeTypes.ts b/src/ChangeTypes.ts new file mode 100644 index 00000000..20d8bf65 --- /dev/null +++ b/src/ChangeTypes.ts @@ -0,0 +1,184 @@ +// The text to identify rules for each possible step that can be taken + +export enum ChangeTypes{ + NO_CHANGE, + + // ARITHMETIC + + // e.g. 2 + 2 -> 4 or 2 * 2 -> 4 + SIMPLIFY_ARITHMETIC, + + // BASICS + + // e.g. 2/-1 -> -2 + DIVISION_BY_NEGATIVE_ONE, + // e.g. 2/1 -> 2 + DIVISION_BY_ONE, + // e.g. x * 0 -> 0 + MULTIPLY_BY_ZERO, + // e.g. x * 2 -> 2x + REARRANGE_COEFF, + // e.g. x ^ 0 -> 1 + REDUCE_EXPONENT_BY_ZERO, + // e.g. 0/1 -> 0 + REDUCE_ZERO_NUMERATOR, + // e.g. 2 + 0 -> 2 + REMOVE_ADDING_ZERO, + // e.g. x ^ 1 -> x + REMOVE_EXPONENT_BY_ONE, + // e.g. 1 ^ x -> 1 + REMOVE_EXPONENT_BASE_ONE, + // e.g. x * -1 -> -x + REMOVE_MULTIPLYING_BY_NEGATIVE_ONE, + // e.g. x * 1 -> x + REMOVE_MULTIPLYING_BY_ONE, + // e.g. 2 - - 3 -> 2 + 3 + RESOLVE_DOUBLE_MINUS, + + // COLLECT AND COMBINE + + // e.g. 2 + x + 3 + x -> 5 + 2x + COLLECT_AND_COMBINE_LIKE_TERMS, + // e.g. x + 2 + x^2 + x + 4 -> x^2 + (x + x) + (4 + 2) + COLLECT_LIKE_TERMS, + + // ADDING POLYNOMIALS + + // e.g. 2x + x -> 2x + 1x + ADD_COEFFICIENT_OF_ONE, + // e.g. x^2 + x^2 -> 2x^2 + ADD_POLYNOMIAL_TERMS, + // e.g. 2x^2 + 3x^2 + 5x^2 -> (2+3+5)x^2 + GROUP_COEFFICIENTS, + // e.g. -x + 2x => -1*x + 2x + UNARY_MINUS_TO_NEGATIVE_ONE, + + // MULTIPLYING POLYNOMIALS + + // e.g. x^2 * x -> x^2 * x^1 + ADD_EXPONENT_OF_ONE, + // e.g. x^2 * x^3 * x^1 -> x^(2 + 3 + 1) + COLLECT_EXPONENTS, + // e.g. 2x * 3x -> (2 * 3)(x * x) + MULTIPLY_COEFFICIENTS, + // e.g. 2x * x -> 2x ^ 2 + MULTIPLY_POLYNOMIAL_TERMS, + + // FRACTIONS + + // e.g. (x + 2)/2 -> x/2 + 2/2 + BREAK_UP_FRACTION, + // e.g. -2/-3 => 2/3 + CANCEL_MINUSES, + // e.g. 2x/2 -> x + CANCEL_TERMS, + // e.g. 2/6 -> 1/3 + SIMPLIFY_FRACTION, + // e.g. 2/-3 -> -2/3 + SIMPLIFY_SIGNS, + + // ADDING FRACTIONS + + // e.g. 1/2 + 1/3 -> 5/6 + ADD_FRACTIONS, + // e.g. (1 + 2)/3 -> 3/3 + ADD_NUMERATORS, + // e.g. (2+1)/5 + COMBINE_NUMERATORS, + // e.g. 2/6 + 1/4 -> (2*2)/(6*2) + (1*3)/(4*3) + COMMON_DENOMINATOR, + // e.g. 3 + 1/2 -> 6/2 + 1/2 (for addition) + CONVERT_INTEGER_TO_FRACTION, + // e.g. 1.2 + 1/2 -> 1.2 + 0.5 + DIVIDE_FRACTION_FOR_ADDITION, + // e.g. (2*2)/(6*2) + (1*3)/(4*3) -> (2*2)/12 + (1*3)/12 + MULTIPLY_DENOMINATORS, + // e.g. (2*2)/12 + (1*3)/12 -> 4/12 + 3/12 + MULTIPLY_NUMERATORS, + + // MULTIPLYING FRACTIONS + + // e.g. 1/2 * 2/3 -> 2/6 + MULTIPLY_FRACTIONS, + + // DIVISION + + // e.g. 2/3/4 -> 2/(3*4) + SIMPLIFY_DIVISION, + // e.g. x/(2/3) -> x * 3/2 + MULTIPLY_BY_INVERSE, + + // DISTRIBUTION + + // e.g. 2(x + y) -> 2x + 2y + DISTRIBUTE, + // e.g. -(2 + x) -> -2 - x + DISTRIBUTE_NEGATIVE_ONE, + // e.g. 2 * 4x + 2*5 --> 8x + 10 (as part of distribution) + SIMPLIFY_TERMS, + + // ABSOLUTE + // e.g. |-3| -> 3 + ABSOLUTE_VALUE, + + // ROOTS + // e.g. nthRoot(x ^ 2, 4) -> nthRoot(x, 2) + CANCEL_EXPONENT, + // e.g. nthRoot(x ^ 2, 2) -> x + CANCEL_EXPONENT_AND_ROOT, + // e.g. nthRoot(x ^ 4, 2) -> x ^ 2 + CANCEL_ROOT, + // e.g. nthRoot(2, 2) * nthRoot(3, 2) -> nthRoot(2 * 3, 2) + COMBINE_UNDER_ROOT, + // e.g. 2 * 2 * 2 -> 2 ^ 3 + CONVERT_MULTIPLICATION_TO_EXPONENT, + // e.g. nthRoot(2 * x) -> nthRoot(2) * nthRoot(x) + DISTRIBUTE_NTH_ROOT, + // e.g. nthRoot(4) * nthRoot(x^2) -> 2 * x + EVALUATE_DISTRIBUTED_NTH_ROOT, + // e.g. 12 -> 2 * 2 * 3 + FACTOR_INTO_PRIMES, + // e.g. nthRoot(2 * 2 * 2, 2) -> nthRoot((2 * 2) * 2) + GROUP_TERMS_BY_ROOT, + // e.g. nthRoot(4) -> 2 + NTH_ROOT_VALUE, + + // SOLVING FOR A VARIABLE + + // e.g. x - 3 = 2 -> x - 3 + 3 = 2 + 3 + ADD_TO_BOTH_SIDES, + // e.g. 2x = 1 -> (2x)/2 = 1/2 + DIVIDE_FROM_BOTH_SIDES, + // e.g. (2/3)x = 1 -> (2/3)x * (3/2) = 1 * (3/2) + MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION, + // e.g. -x = 2 -> -1 * -x = -1 * 2 + MULTIPLY_BOTH_SIDES_BY_NEGATIVE_ONE, + // e.g. x/2 = 1 -> (x/2) * 2 = 1 * 2 + MULTIPLY_TO_BOTH_SIDES, + // e.g. x + 2 - 1 = 3 -> x + 1 = 3 + SIMPLIFY_LEFT_SIDE, + // e.g. x = 3 - 1 -> x = 2 + SIMPLIFY_RIGHT_SIDE, + // e.g. x + 3 = 2 -> x + 3 - 3 = 2 - 3 + SUBTRACT_FROM_BOTH_SIDES, + // e.g. 2 = x -> x = 2 + SWAP_SIDES, + + // CONSTANT EQUATION + + // e.g. 2 = 2 + STATEMENT_IS_TRUE, + // e.g. 2 = 3 + STATEMENT_IS_FALSE, + + // FACTORING + + // e.g. x^2 - 4x -> x(x - 4) + FACTOR_SYMBOL, + // e.g. x^2 - 4 -> (x - 2)(x + 2) + FACTOR_DIFFERENCE_OF_SQUARES, + // e.g. x^2 + 2x + 1 -> (x + 1)^2 + FACTOR_PERFECT_SQUARE, + // e.g. x^2 + 3x + 2 -> (x + 1)(x + 2) + FACTOR_SUM_PRODUCT_RULE, +}; \ No newline at end of file diff --git a/src/Equation.ts b/src/Equation.ts new file mode 100644 index 00000000..79bb21ee --- /dev/null +++ b/src/Equation.ts @@ -0,0 +1,114 @@ +import * as nodeHelper from "./nodeHelper"; +// This represents an equation, made up of the leftNode (LHS), the +// rightNode (RHS) and a comparator (=, <, >, <=, or >=) +export default class Equation { + leftNode: mNode; + rightNode: mNode; + comparator: string; + constructor(leftNode: mNode, rightNode: mNode, comparator: string) { + this.leftNode = leftNode; + this.rightNode = rightNode; + this.comparator = comparator; + } + + // Prints an Equation properly using the print module + print(showPlusMinus=false) { + const leftSide = printNode(this.leftNode, showPlusMinus); + const rightSide = printNode(this.rightNode, showPlusMinus); + const comparator = this.comparator; + + return `${leftSide} ${comparator} ${rightSide}`; + } + + clone() { + const newLeft = clone(this.leftNode); + const newRight = clone(this.rightNode); + return new Equation(newLeft, newRight, this.comparator); + } + +// Splits a string on the given comparator and returns a new Equation object +// from the left and right hand sides +static createEquationFromString = function(str: string, comparator:string) { + const sides = str.split(comparator); + if (sides.length !== 2) { + throw Error('Expected two sides of an equation using comparator: ' + + comparator); + } + const leftNode = math.parse(sides[0]); + const rightNode = math.parse(sides[1]); + + return new Equation(leftNode, rightNode, comparator); +}; +} + +// This represents the current equation we're solving. +// As we move step by step, an equation might be updated. Functions return this +// status object to pass on the updated equation and information on if/how it was +// changed. +class Status { + changeType; + oldEquation: Equation; + newEquation: Equation; + substeps; + constructor(changeType, oldEquation: Equation, newEquation: Equation, substeps=[]) { + if (!newEquation) { + throw Error('new equation isn\'t defined'); + } + if (changeType === undefined || typeof(changeType) !== 'string') { + throw Error('changetype isn\'t valid'); + } + + this.changeType = changeType; + this.oldEquation = oldEquation; + this.newEquation = newEquation; + this.substeps = substeps; + } + + hasChanged() { + return this.changeType !== ChangeTypes.NO_CHANGE; + } + +// A wrapper around the Status constructor for the case where equation +// hasn't been changed. +noChange(equation) { + return new Status(ChangeTypes.NO_CHANGE, null, equation); +}; + +addLeftStep(equation, leftStep) { + const substeps = []; + leftStep.substeps.forEach(substep => { + substeps.push(Status.addLeftStep(equation, substep)); + }); + let oldEquation = null; + if (leftStep.oldNode) { + oldEquation = equation.clone(); + oldEquation.leftNode = leftStep.oldNode; + } + const newEquation = equation.clone(); + newEquation.leftNode = leftStep.newNode; + return new Status( + leftStep.changeType, oldEquation, newEquation, substeps); +}; + +addRightStep(equation: Equation, rightStep) { + const substeps = []; + rightStep.substeps.forEach(substep => { + substeps.push(Status.addRightStep(equation, substep)); + }); + let oldEquation = null; + if (rightStep.oldNode) { + oldEquation = equation.clone(); + oldEquation.rightNode = rightStep.oldNode; + } + const newEquation = equation.clone(); + newEquation.rightNode = rightStep.newNode; + return new Status( + rightStep.changeType, oldEquation, newEquation, substeps); +}; + +resetChangeGroups(equation) { + const leftNode = nodeHelper.Status.resetChangeGroups(equation.leftNode); + const rightNode = nodeHelper.Status.resetChangeGroups(equation.rightNode); + return new Equation(leftNode, rightNode, equation.comparator); +}; +} diff --git a/lib/Negative.js b/src/Negative.ts similarity index 63% rename from lib/Negative.js rename to src/Negative.ts index 6efb0afc..6b4b88cf 100644 --- a/lib/Negative.js +++ b/src/Negative.ts @@ -1,26 +1,24 @@ -const Node = require('./node'); - -const Negative = {}; - +import * as nodeHelper from "./nodeHelper" +export default class Negative{ // Returns if the given node is negative. Treats a unary minus as a negative, // as well as a negative constant value or a constant fraction that would // evaluate to a negative number -Negative.isNegative = function(node) { - if (Node.Type.isUnaryMinus(node)) { +static isNegative(node: mNode): boolean { + if (nodeHelper.Type.isUnaryMinus(node)) { return !Negative.isNegative(node.args[0]); } - else if (Node.Type.isConstant(node)) { + else if (nodeHelper.Type.isConstant(node)) { return parseFloat(node.value) < 0; } - else if (Node.Type.isConstantFraction(node)) { + else if (nodeHelper.Type.isConstantFraction(node)) { const numeratorValue = parseFloat(node.args[0].value); const denominatorValue = parseFloat(node.args[1].value); if (numeratorValue < 0 || denominatorValue < 0) { return !(numeratorValue < 0 && denominatorValue < 0); } } - else if (Node.PolynomialTerm.isPolynomialTerm(node)) { - const polyNode = new Node.PolynomialTerm(node); + else if (nodeHelper.PolynomialTerm.isPolynomialTerm(node)) { + const polyNode = new nodeHelper.PolynomialTerm(node); return Negative.isNegative(polyNode.getCoeffNode(true)); } @@ -33,23 +31,23 @@ Negative.isNegative = function(node) { // E.g. // not naive: -3 -> 3, x -> -x // naive: -3 -> --3, x -> -x -Negative.negate = function(node, naive=false) { - if (Node.Type.isConstantFraction(node)) { +static negate(node: mNode, naive=false): mNode { + if (nodeHelper.Type.isConstantFraction(node)) { node.args[0] = Negative.negate(node.args[0], naive); return node; } - else if (Node.PolynomialTerm.isPolynomialTerm(node)) { + else if (nodeHelper.PolynomialTerm.isPolynomialTerm(node)) { return Negative.negatePolynomialTerm(node, naive); } else if (!naive) { - if (Node.Type.isUnaryMinus(node)) { + if (nodeHelper.Type.isUnaryMinus(node)) { return node.args[0]; } - else if (Node.Type.isConstant(node)) { - return Node.Creator.constant(0 - parseFloat(node.value)); + else if (nodeHelper.Type.isConstant(node)) { + return nodeHelper.Creator.constant(0 - parseFloat(node.value)); } } - return Node.Creator.unaryMinus(node); + return nodeHelper.Creator.unaryMinus(node); }; // Multiplies a polynomial term by -1 and returns the new node @@ -58,15 +56,15 @@ Negative.negate = function(node, naive=false) { // E.g. // not naive: -3x -> 3x, x -> -x // naive: -3x -> --3x, x -> -x -Negative.negatePolynomialTerm = function(node, naive=false) { - if (!Node.PolynomialTerm.isPolynomialTerm(node)) { +static negatePolynomialTerm(node: mNode, naive=false): mNode { + if (!nodeHelper.PolynomialTerm.isPolynomialTerm(node)) { throw Error('node is not a polynomial term'); } - const polyNode = new Node.PolynomialTerm(node); + const polyNode = new nodeHelper.PolynomialTerm(node); let newCoeff; if (!polyNode.hasCoeff()) { - newCoeff = Node.Creator.constant(-1); + newCoeff = nodeHelper.Creator.constant(-1); } else { const oldCoeff = polyNode.getCoeffNode(); @@ -78,7 +76,7 @@ Negative.negatePolynomialTerm = function(node, naive=false) { numerator = Negative.negate(numerator, naive); const denominator = oldCoeff.args[1]; - newCoeff = Node.Creator.operator('/', [numerator, denominator]); + newCoeff = nodeHelper.Creator.operator('/', [numerator, denominator]); } else { newCoeff = Negative.negate(oldCoeff, naive); @@ -87,8 +85,7 @@ Negative.negatePolynomialTerm = function(node, naive=false) { } } } - return Node.Creator.polynomialTerm( + return nodeHelper.Creator.polynomialTerm( polyNode.getSymbolNode(), polyNode.getExponentNode(), newCoeff); }; - -module.exports = Negative; +} \ No newline at end of file diff --git a/lib/Symbols.js b/src/Symbols.ts similarity index 55% rename from lib/Symbols.js rename to src/Symbols.ts index 6978a8f9..edcf215e 100644 --- a/lib/Symbols.js +++ b/src/Symbols.ts @@ -1,9 +1,9 @@ -const Node = require('./node'); - -const Symbols = {}; - +import * as nodeHelper from "./nodeHelper"; +import Equation from "./Equation"; +class Symbols{ + // returns the set of all the symbols in an equation -Symbols.getSymbolsInEquation = function(equation) { +static getSymbolsInEquation(equation: Equation) { const leftSymbols = Symbols.getSymbolsInExpression(equation.leftNode); const rightSymbols = Symbols.getSymbolsInExpression(equation.rightNode); const symbols = new Set([...leftSymbols, ...rightSymbols]); @@ -11,7 +11,7 @@ Symbols.getSymbolsInEquation = function(equation) { }; // return the set of symbols in the expression tree -Symbols.getSymbolsInExpression = function(expression) { +static getSymbolsInExpression(expression) { const symbolNodes = expression.filter(node => node.isSymbolNode); // all the symbol nodes const symbols = symbolNodes.map(node => node.name); // all the symbol nodes' names const symbolSet = new Set(symbols); // to get rid of duplicates @@ -21,17 +21,17 @@ Symbols.getSymbolsInExpression = function(expression) { // Iterates through a node and returns the polynomial term with the symbol name // Returns null if no terms with the symbol name are in the node. // e.g. 4x^2 + 2x + y + 2 with `symbolName=x` would return 2x -Symbols.getLastSymbolTerm = function(node, symbolName) { +static getLastSymbolTerm(node: mNode, symbolName: string) { // First check if the node itself is a polyomial term with symbolName - if (isSymbolTerm(node, symbolName)) { + if (Symbols.isSymbolTerm(node, symbolName)) { return node; } // Otherwise, it's a sum of terms. Look through the operands for a term // with `symbolName` - else if (Node.Type.isOperator(node, '+')) { + else if (nodeHelper.Type.isOperator(node, '+')) { for (let i = node.args.length - 1; i >= 0 ; i--) { const child = node.args[i]; - if (isSymbolTerm(child, symbolName)) { + if (Symbols.isSymbolTerm(child, symbolName)) { return child; } } @@ -45,14 +45,14 @@ Symbols.getLastSymbolTerm = function(node, symbolName) { // e.g. 4x^2 with `symbolName=x` would return 4 // e.g. 4x^2 + 2x + 2/4 with `symbolName=x` would return 2/4 // e.g. 4x^2 + 2x + y with `symbolName=x` would return y -Symbols.getLastNonSymbolTerm = function(node, symbolName) { - if (isSymbolTerm(node, symbolName)) { - return new Node.PolynomialTerm(node).getCoeffNode(); +static getLastNonSymbolTerm(node: mNode, symbolName: string) { + if (Symbols.isSymbolTerm(node, symbolName)) { + return new nodeHelper.PolynomialTerm(node).getCoeffNode(); } - else if (Node.Type.isOperator(node)) { + else if (nodeHelper.Type.isOperator(node)) { for (let i = node.args.length - 1; i >= 0 ; i--) { const child = node.args[i]; - if (!isSymbolTerm(child, symbolName)) { + if (!Symbols.isSymbolTerm(child, symbolName)) { return child; } } @@ -62,14 +62,34 @@ Symbols.getLastNonSymbolTerm = function(node, symbolName) { }; // Returns if `node` is a polynomial term with symbol `symbolName` -function isSymbolTerm(node, symbolName) { - if (Node.PolynomialTerm.isPolynomialTerm(node)) { - const polyTerm = new Node.PolynomialTerm(node); +static isSymbolTerm(node: mNode, symbolName: string) { + if (nodeHelper.PolynomialTerm.isPolynomialTerm(node)) { + const polyTerm = new nodeHelper.PolynomialTerm(node); if (polyTerm.getSymbolName() === symbolName) { return true; } } return false; } +} +//Probably shouldn't do this but whatever +interface Set { + add(value: T): Set; + clear(): void; + delete(value: T): boolean; + entries(): IterableIterator<[T, T]>; + forEach(callbackfn: (value: T, index: T, set: Set) => void, thisArg?: any): void; + has(value: T): boolean; + keys(): IterableIterator; + size: number; + values(): IterableIterator; + [Symbol.iterator]():IterableIterator; + [Symbol.toStringTag]: string; +} -module.exports = Symbols; +interface SetConstructor { + new (): Set; + new (iterable: Iterable): Set; + prototype: Set; +} +declare var Set: SetConstructor; \ No newline at end of file diff --git a/src/TreeSearch.ts b/src/TreeSearch.ts new file mode 100644 index 00000000..0c47f85a --- /dev/null +++ b/src/TreeSearch.ts @@ -0,0 +1,66 @@ +import * as nodeHelper from "./nodeHelper"; +class TreeSearch { + +// Returns a function that performs a preorder search on the tree for the given +// simplifcation function +static preOrder(simplificationFunction) { + return function (node: mNode) { + return TreeSearch.search(simplificationFunction, node, true); + }; +}; + +// Returns a function that performs a postorder search on the tree for the given +// simplifcation function +static postOrder = function(simplificationFunction) { + return function (node: mNode) { + return TreeSearch.search(simplificationFunction, node, false); + }; +}; + +// A helper function for performing a tree search with a function +static search(simplificationFunction, node: mNode, preOrder: boolean) { + let status; + + if (preOrder) { + status = simplificationFunction(node); + if (status.hasChanged()) { + return status; + } + } + + if (nodeHelper.Type.isConstant(node) || nodeHelper.Type.isSymbol(node)) { + return nodeHelper.Status.noChange(node); + } + else if (nodeHelper.Type.isUnaryMinus(node)) { + status = TreeSearch.search(simplificationFunction, node.args[0], preOrder); + if (status.hasChanged()) { + return nodeHelper.Status.childChanged(node, status); + } + } + else if (nodeHelper.Type.isOperator(node) || nodeHelper.Type.isFunction(node)) { + for (let i = 0; i < node.args.length; i++) { + const child = node.args[i]; + const childNodeStatus = TreeSearch.search(simplificationFunction, child, preOrder); + if (childNodeStatus.hasChanged()) { + return nodeHelper.Status.childChanged(node, childNodeStatus, i); + } + } + } + else if (nodeHelper.Type.isParenthesis(node)) { + status = TreeSearch.search(simplificationFunction, node.content, preOrder); + if (status.hasChanged()) { + return nodeHelper.Status.childChanged(node, status); + } + } + else { + throw Error('Unsupported node type: ' + node); + } + + if (!preOrder) { + return simplificationFunction(node); + } + else { + return nodeHelper.Status.noChange(node); + } +} +} diff --git a/src/classes.ts b/src/classes.ts new file mode 100644 index 00000000..dead85db --- /dev/null +++ b/src/classes.ts @@ -0,0 +1,5 @@ + //extend utility for MathNode class + interface mNode extends mathjs.MathNode{ + changeGroup?: number; + content?: mathjs.MathNode; + } \ No newline at end of file diff --git a/src/factor.ts b/src/factor.ts new file mode 100644 index 00000000..eda5c639 --- /dev/null +++ b/src/factor.ts @@ -0,0 +1,254 @@ +import Negative from "./Negative"; +import * as nodeHelper from "./nodeHelper"; + //returns all prime factors of a number + export function getPrimeFactors(number: number): number[] { + let factors = []; + if (number < 0) { + factors = [-1]; + factors = factors.concat(getPrimeFactors(-1 * number)); + return factors; + } + + const root = Math.sqrt(number); + let candidate = 2; + if (number % 2) { + candidate = 3; // assign first odd + while (number % candidate && candidate <= root) { + candidate = candidate + 2; + } + } + + // if no factor found then the number is prime + if (candidate > root) { + factors.push(number); + } + // if we find a factor, make a recursive call on the quotient of the number and + // our newly found prime factor in order to find more factors + else { + factors.push(candidate); + factors = factors.concat(getPrimeFactors(number / candidate)); + } + + return factors; + } + // Given a number, will return all the factor pairs for that number as a list + // of 2-item lists + export function getFactorPairs(number: number): number[][] { + const factors = []; + + 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]); + } + } + + return factors; + } + // functions for factoring quadratic equations + 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 +]; + export function factorQuadratic(node: mNode) { + node = flatten(node); + if (!checks.isQuadratic(node)) { + return nodeHelper.Status.noChange(node); + } + // get a, b and c + let symbol, aValue = 0, bValue = 0, cValue = 0; + for (const term of node.args) { + if (nodeHelper.Type.isConstant(term)) { + cValue = evaluate(term); + } + else if (nodeHelper.PolynomialTerm.isPolynomialTerm(term)) { + const polyTerm = new nodeHelper.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 nodeHelper.Status.noChange(node); + } + } + else { + return nodeHelper.Status.noChange(node); + } + } + + if (!symbol || !aValue) { + return nodeHelper.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 nodeHelper.Status.noChange(node); + } + // Will factor the node if it's in the form of ax^2 + bx + function factorSymbol(node: mNode, symbol, aValue: number, bValue: number, cValue: number, negate: boolean) { + if (!bValue || cValue) { + return nodeHelper.Status.noChange(node); + } + + const gcd = math.gcd(aValue, bValue); + const gcdNode = nodeHelper.Creator.constant(gcd); + const aNode = nodeHelper.Creator.constant(aValue / gcd); + const bNode = nodeHelper.Creator.constant(bValue / gcd); + + const factoredNode = nodeHelper.Creator.polynomialTerm(symbol, null, gcdNode); + const polyTerm = nodeHelper.Creator.polynomialTerm(symbol, null, aNode); + const paren = nodeHelper.Creator.parenthesis( + nodeHelper.Creator.operator('+', [polyTerm, bNode])); + + let newNode = nodeHelper.Creator.operator('*', [factoredNode, paren], true); + if (negate) { + newNode = Negative.negate(newNode); + } + + return nodeHelper.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: mNode, symbol, aValue: number, bValue: number, cValue: number, negate: boolean) { + // check if difference of squares: (i) abs(a) and abs(c) are squares, (ii) b = 0, + // (iii) c is negative + if (bValue || !cValue) { + return nodeHelper.Status.noChange(node); + } + + const aRootValue = Math.sqrt(Math.abs(aValue)); + const cRootValue = Math.sqrt(Math.abs(cValue)); + + // must be a difference of squares + if (isInteger(aRootValue) && + isInteger(cRootValue) && + cValue < 0) { + + const aRootNode = nodeHelper.Creator.constant(aRootValue); + const cRootNode = nodeHelper.Creator.constant(cRootValue); + + const polyTerm = nodeHelper.Creator.polynomialTerm(symbol, null, aRootNode); + const firstParen = nodeHelper.Creator.parenthesis( + nodeHelper.Creator.operator('+', [polyTerm, cRootNode])); + const secondParen = nodeHelper.Creator.parenthesis( + nodeHelper.Creator.operator('-', [polyTerm, cRootNode])); + + // create node in difference of squares form + let newNode = nodeHelper.Creator.operator('*', [firstParen, secondParen], true); + if (negate) { + newNode = Negative.negate(newNode); + } + + return nodeHelper.Status.nodeChanged( + ChangeTypes.FACTOR_DIFFERENCE_OF_SQUARES, node, newNode); + } + + return nodeHelper.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: mNode, symbol, aValue: number, bValue: number, cValue: number, negate: boolean) { + // check if perfect square: (i) a and c squares, (ii) b = 2*sqrt(a)*sqrt(c) + if (!bValue || !cValue) { + return nodeHelper.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 (isInteger(aRootValue) && + isInteger(cRootValue) && + bValue === perfectProduct) { + + const aRootNode = nodeHelper.Creator.constant(aRootValue); + const cRootNode = nodeHelper.Creator.constant(cRootValue); + + const polyTerm = nodeHelper.Creator.polynomialTerm(symbol, null, aRootNode); + const paren = nodeHelper.Creator.parenthesis( + nodeHelper.Creator.operator('+', [polyTerm, cRootNode])); + const exponent = nodeHelper.Creator.constant(2); + + // create node in perfect square form + let newNode = nodeHelper.Creator.operator('^', [paren, exponent]); + if (negate) { + newNode = Negative.negate(newNode); + } + + return nodeHelper.Status.nodeChanged( + ChangeTypes.FACTOR_PERFECT_SQUARE, node, newNode); + } + + return nodeHelper.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: mNode, symbol, aValue: number, bValue: number, cValue: number, negate: boolean) { + if (aValue === 1 && bValue && cValue) { + // try sum/product rule: find a factor pair of c that adds up to b + const factorPairs = getFactorPairs(cValue); + for (const pair of factorPairs) { + if (pair[0] + pair[1] === bValue) { + const firstParen = nodeHelper.Creator.parenthesis( + nodeHelper.Creator.operator('+', [symbol, nodeHelper.Creator.constant(pair[0])])); + const secondParen = nodeHelper.Creator.parenthesis( + nodeHelper.Creator.operator('+', [symbol, nodeHelper.Creator.constant(pair[1])])); + + // create a node in the general factored form for expression + let newNode = nodeHelper.Creator.operator('*', [firstParen, secondParen], true); + if (negate) { + newNode = Negative.negate(newNode); + } + + return nodeHelper.Status.nodeChanged( + ChangeTypes.FACTOR_SUM_PRODUCT_RULE, node, newNode); + } + } + } + + return nodeHelper.Status.noChange(node); + } + function isInteger(a: number) + { + return a % 1 === 0; + } diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/nodeHelper.ts b/src/nodeHelper.ts new file mode 100644 index 00000000..e98e093b --- /dev/null +++ b/src/nodeHelper.ts @@ -0,0 +1,499 @@ +/// +/// + +export class Status { + changeType: ChangeTypes; + oldNode: mNode; + newNode: mNode; + substeps: any; + constructor(changeType: ChangeTypes, oldNode: mNode, newNode: mNode, substeps = []) { + if (!newNode) { + throw Error('node is not defined'); + } + if (changeType === undefined || typeof (changeType) !== 'string') { + throw Error('changetype isn\'t valid'); + } + + this.changeType = changeType; + this.oldNode = oldNode; + this.newNode = newNode; + this.substeps = substeps; + } + hasChanged() { + return this.changeType !== ChangeTypes.NO_CHANGE; + } + static resetChangeGroups = function (node: mNode) { + node = clone(node); + node.filter(node => node.changeGroup).forEach(change => { + delete change.changeGroup; + }); + return node; + }; + + // A wrapper around the Status constructor for the case where node hasn't + // been changed. + static noChange(node: mNode) { + return new Status(ChangeTypes.NO_CHANGE, null, node); + }; + + // A wrapper around the Status constructor for the case of a change + // that is happening at the level of oldNode + newNode + // e.g. 2 + 2 --> 4 (an addition node becomes a constant node) + static nodeChanged( + changeType: ChangeTypes, oldNode: mNode, newNode: mNode, defaultChangeGroup = true, steps = []) { + if (defaultChangeGroup) { + oldNode.changeGroup = 1; + newNode.changeGroup = 1; + } + + return new Status(changeType, oldNode, newNode, steps); + }; + + // A wrapper around the Status constructor for the case where there was + // a change that happened deeper `node`'s tree, and `node`'s children must be + // updated to have the newNode/oldNode metadata (changeGroups) + // e.g. (2 + 2) + x --> 4 + x has to update the left argument + static childChanged(node: mNode, childStatus, childArgIndex = null) { + const oldNode = clone(node); + const newNode = clone(node); + let substeps = childStatus.substeps; + + if (!childStatus.oldNode) { + throw Error('Expected old node for changeType: ' + childStatus.changeType); + } + + function updateSubsteps(substeps, fn) { + substeps.map((step) => { + step = fn(step); + step.substeps = updateSubsteps(step.substeps, fn); + }); + return substeps; + } + + if (Type.isParenthesis(node)) { + oldNode.content = childStatus.oldNode; + newNode.content = childStatus.newNode; + substeps = updateSubsteps(substeps, (step) => { + const oldNode = clone(node); + const newNode = clone(node); + oldNode.content = step.oldNode; + newNode.content = step.newNode; + step.oldNode = oldNode; + step.newNode = newNode; + return step; + }); + } + else if ((Type.isOperator(node) || Type.isFunction(node) && + childArgIndex !== null)) { + oldNode.args[childArgIndex] = childStatus.oldNode; + newNode.args[childArgIndex] = childStatus.newNode; + substeps = updateSubsteps(substeps, (step) => { + const oldNode = clone(node); + const newNode = clone(node); + oldNode.args[childArgIndex] = step.oldNode; + newNode.args[childArgIndex] = step.newNode; + step.oldNode = oldNode; + step.newNode = newNode; + return step; + }); + } + else if (Type.isUnaryMinus(node)) { + oldNode.args[0] = childStatus.oldNode; + newNode.args[0] = childStatus.newNode; + substeps = updateSubsteps(substeps, (step) => { + const oldNode = clone(node); + const newNode = clone(node); + oldNode.args[0] = step.oldNode; + newNode.args[0] = step.newNode; + step.oldNode = oldNode; + step.newNode = newNode; + return step; + }); + } + else { + throw Error('Unexpected node type: ' + node.type); + } + + return new Status(childStatus.changeType, oldNode, newNode, substeps); + }; +} +/* +Functions to generate any mathJS node supported by the stepper +see http://mathjs.org/docs/expressions/expression_trees.html#nodes for more +information on nodes in mathJS +*/ +export class Creator { + static operator(op: string, args: mNode[], implicit = false): mNode { + switch (op) { + case '+': + return new math.expression.node.OperatorNode('+', 'add', args); + case '-': + return new math.expression.node.OperatorNode('-', 'subtract', args); + case '/': + return new math.expression.node.OperatorNode('/', 'divide', args); + case '*': + return new math.expression.node.OperatorNode( + '*', 'multiply', args, implicit); + case '^': + return new math.expression.node.OperatorNode('^', 'pow', args); + default: + throw Error('Unsupported operation: ' + op); + } + } + + // In almost all cases, use Negative.negate (with naive = true) to add a + // unary minus to your node, rather than calling this constructor directly + static unaryMinus(content) { + return new math.expression.node.OperatorNode( + '-', 'unaryMinus', [content]); + } + + static constant(val) { + return new math.expression.node.ConstantNode(val); + } + + static symbol(name) { + return new math.expression.node.SymbolNode(name); + } + + static parenthesis(content) { + return new math.expression.node.ParenthesisNode(content); + } + + // 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. + static polynomialTerm(symbol, exponent, coeff, explicitCoeff = false) { + let polyTerm = symbol; + if (exponent) { + polyTerm = this.operator('^', [polyTerm, exponent]); + } + if (coeff && (explicitCoeff || parseFloat(coeff.value) !== 1)) { + if (Type.isConstant(coeff) && + parseFloat(coeff.value) === -1 && + !explicitCoeff) { + // if you actually want -1 as the coefficient, set explicitCoeff to true + polyTerm = this.unaryMinus(polyTerm); + } + else { + polyTerm = this.operator('*', [coeff, polyTerm], true); + } + } + return polyTerm; + } + + // Given a root value and a radicand (what is under the radical) + static nthRoot(radicandNode: mNode, rootNode: mNode) { + const symbol = Creator.symbol('nthRoot'); + return new math.expression.node.FunctionNode(symbol, [radicandNode, rootNode]); + } +}; +//For determining the type of a mathJS node. +export class Type { + + static isOperator(node: mNode, operator: string = null) { + return node.type === 'OperatorNode' && + node.fn !== 'unaryMinus' && + "*+-/^".indexOf(node.op) >= 0 && + (operator ? node.op === operator : true); + }; + + static isParenthesis(node: mNode) { + return node.type === 'ParenthesisNode'; + }; + + static isUnaryMinus = function (node: mNode) { + return node.type === 'OperatorNode' && node.fn === 'unaryMinus'; + }; + + static isFunction = function (node: mNode, functionName = null) { + if (node.type !== 'FunctionNode') { + return false; + } + if (functionName && node.fn.name !== functionName) { + return false; + } + return true; + }; + + static isSymbol(node: mNode, allowUnaryMinus = true): boolean { + if (node.type === 'SymbolNode') { + return true; + } + else if (allowUnaryMinus && Type.isUnaryMinus(node)) { + return Type.isSymbol(node.args[0], false); + } + else { + return false; + } + }; + + static isConstant(node: mNode, allowUnaryMinus = false) { + if (node.type === 'ConstantNode') { + return true; + } + else if (allowUnaryMinus && Type.isUnaryMinus(node)) { + if (Type.isConstant(node.args[0], false)) { + const value = parseFloat(node.args[0].value); + return value >= 0; + } + else { + return false; + } + } + else { + return false; + } + }; + + static isConstantFraction(node: mNode, allowUnaryMinus = false) { + if (Type.isOperator(node, '/')) { + return node.args.every(n => Type.isConstant(n, allowUnaryMinus)); + } + else { + return false; + } + }; + + static isConstantOrConstantFraction(node: mNode, allowUnaryMinus = false) { + if (Type.isConstant(node, allowUnaryMinus) || + Type.isConstantFraction(node, allowUnaryMinus)) { + return true; + } + else { + return false; + } + }; + + static isIntegerFraction(node: mNode, allowUnaryMinus = false) { + if (!NodeType.isConstantFraction(node, allowUnaryMinus)) { + return false; + } + let numerator = node.args[0]; + let denominator = node.args[1]; + if (allowUnaryMinus) { + if (NodeType.isUnaryMinus(numerator)) { + numerator = numerator.args[0]; + } + if (NodeType.isUnaryMinus(denominator)) { + denominator = denominator.args[0]; + } + } + return (Number.isInteger(parseFloat(numerator.value)) && + Number.isInteger(parseFloat(denominator.value))); + }; + +} +export class PolynomialTerm { + // For storing polynomial terms. + // Has a symbol (e.g. x), maybe an exponent, and maybe a coefficient. + // These expressions are of the form of a PolynomialTerm: x^2, 2y, z, 3x/5 + // These expressions are not: 4, x^(3+4), 2+x, 3*7, x-z + /* Fields: + - coeff: either a constant node or a fraction of two constant nodes + (might be null if no coefficient) + - symbol: the node with the symbol (e.g. in x^2, the node x) + - exponent: a node that can take any form, e.g. x^(2+x^2) + (might be null if no exponent) + */ + // if onlyImplicitMultiplication is true, an error will be thrown if `node` + // is a polynomial term without implicit multiplication + // (i.e. 2*x instead of 2x) and therefore isPolynomialTerm will return false. + //TODO: Add type annotation + symbol; + exponent; + coeff; + constructor(node: mNode, onlyImplicitMultiplication = false) { + if (NodeType.isOperator(node)) { + if (node.op === '^') { + const symbolNode = node.args[0]; + if (!NodeType.isSymbol(symbolNode)) { + throw Error('Expected symbol term, got ' + symbolNode); + } + this.symbol = symbolNode; + this.exponent = node.args[1]; + } + // it's '*' ie it has a coefficient + else if (node.op === '*') { + if (onlyImplicitMultiplication && !node.implicit) { + throw Error('Expected implicit multiplication'); + } + if (node.args.length !== 2) { + throw Error('Expected two arguments to *'); + } + const coeffNode = node.args[0]; + if (!NodeType.isConstantOrConstantFraction(coeffNode)) { + throw Error('Expected coefficient to be constant or fraction of ' + + 'constants term, got ' + coeffNode); + } + this.coeff = coeffNode; + const nonCoefficientTerm = new PolynomialTerm( + node.args[1], onlyImplicitMultiplication); + if (nonCoefficientTerm.hasCoeff()) { + throw Error('Cannot have two coefficients ' + coeffNode + + ' and ' + nonCoefficientTerm.getCoeffNode()); + } + this.symbol = nonCoefficientTerm.getSymbolNode(); + this.exponent = nonCoefficientTerm.getExponentNode(); + } + // this means there's a fraction coefficient + else if (node.op === '/') { + const denominatorNode = node.args[1]; + if (!NodeType.isConstant(denominatorNode)) { + throw Error('denominator must be constant node, instead of ' + + denominatorNode); + } + const numeratorNode = new PolynomialTerm( + node.args[0], onlyImplicitMultiplication); + if (numeratorNode.hasFractionCoeff()) { + throw Error('Polynomial terms cannot have nested fractions'); + } + this.exponent = numeratorNode.getExponentNode(); + this.symbol = numeratorNode.getSymbolNode(); + const numeratorConstantNode = numeratorNode.getCoeffNode(true); + this.coeff = NodeCreator.operator( + '/', [numeratorConstantNode, denominatorNode]); + } + else { + throw Error('Unsupported operatation for polynomial node: ' + node.op); + } + } + else if (NodeType.isUnaryMinus(node)) { + var arg = node.args[0] as mNode; + if (NodeType.isParenthesis(arg)) { + arg = arg.content; + } + const polyNode = new PolynomialTerm( + arg, onlyImplicitMultiplication); + this.exponent = polyNode.getExponentNode(); + this.symbol = polyNode.getSymbolNode(); + if (!polyNode.hasCoeff()) { + this.coeff = NodeCreator.constant(-1); + } + else { + this.coeff = negativeCoefficient(polyNode.getCoeffNode()); + } + } + else if (NodeType.isSymbol(node)) { + this.symbol = node; + } + else { + throw Error('Unsupported node type: ' + node.type); + } + } + + /* GETTER FUNCTIONS */ + getSymbolNode() { + return this.symbol; + } + + getSymbolName() { + return this.symbol.name; + } + + getCoeffNode(defaultOne = false) { + if (!this.coeff && defaultOne) { + return Creator.constant(1); + } + else { + return this.coeff; + } + } + + getCoeffValue() { + if (this.coeff) { + return evaluate(this.coeff); + } + else { + return 1; // no coefficient is like a coeff of 1 + } + } + + getExponentNode(defaultOne = false) { + if (!this.exponent && defaultOne) { + return Creator.constant(1); + } + else { + return this.exponent; + } + } + + getRootNode() { + return Creator.polynomialTerm( + this.symbol, this.exponent, this.coeff); + } + + // note: there is no exponent value getter function because the exponent + // can be any expresion and not necessarily a number. + + /* CHECKER FUNCTIONS (returns true / false for certain conditions) */ + + // Returns true if the coefficient is a fraction + hasFractionCoeff() { + // coeffNode is either a constant or a division operation. + return this.coeff && Type.isOperator(this.coeff); + } + + hasCoeff() { + return !!this.coeff; + } + + + // Returns if the node represents an expression that can be considered a term. + // e.g. x^2, 2y, z, 3x/5 are all terms. 4, 2+x, 3*7, x-z are all not terms. + // See the tests for some more thorough examples of exactly what counts and + // what does not. + static isPolynomialTerm(node: mNode, onlyImplicitMultiplication = false) { + try { + // will throw error if node isn't poly term + new PolynomialTerm(node, onlyImplicitMultiplication); + return true; + } + catch (err) { + return false; + } + }; + + // Multiplies `node`, a constant or fraction of two constant nodes, by -1 + // Returns a node + static negativeCoefficient(node: mNode) { + if (Type.isConstant(node)) { + node = Creator.constant(0 - parseFloat(node.value)); + } + else { + const numeratorValue = 0 - parseFloat(node.args[0].value); + node.args[0] = Creator.constant(numeratorValue); + } + return node; + } +} +/* +None of this works :( +// create node constructors because we can't use the ones from mathjs :( +function OperatorNode(op: string, fn: string, args: mNode[]): mNode { + let node: mNode = { + isNode: true, + type: "OperatorNode", + isOperatorNode: true, + op: op, + fn: fn, + args: args as mathjs.MathNode[] || [], + //don't know how to implement the rest of the things + } + return node; +} +function Constant(value: string, valueType:string): mNode { + let node: mNode = { + value: value, + valueType: valueType, + isNode: true, + type: "OperatorNode", + isOperatorNode: true, + op: op, + fn: fn, + args: args as mathjs.MathNode[] || [], + //don't know how to implement the rest of the things + } + return node; +} +*/ \ No newline at end of file diff --git a/src/simplifyExpression/arithmeticSearch.ts b/src/simplifyExpression/arithmeticSearch.ts new file mode 100644 index 00000000..2b9c4f46 --- /dev/null +++ b/src/simplifyExpression/arithmeticSearch.ts @@ -0,0 +1,2 @@ +import * as ChangeTypes from "../ChangeTypes"; +import * as evaluate from "../util/evaluate"; \ No newline at end of file diff --git a/src/simplifyExpression/basicsSearch.ts b/src/simplifyExpression/basicsSearch.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/simplifyExpression/breakUpNumeratorSearch.ts b/src/simplifyExpression/breakUpNumeratorSearch.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/simplifyExpression/collectAndCombineSearch.ts b/src/simplifyExpression/collectAndCombineSearch.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/simplifyExpression/distributeSearch.ts b/src/simplifyExpression/distributeSearch.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/simplifyExpression/divisionSearch.ts b/src/simplifyExpression/divisionSearch.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/simplifyExpression/fractionsSearch.ts b/src/simplifyExpression/fractionsSearch.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/simplifyExpression/functionsSearch.ts b/src/simplifyExpression/functionsSearch.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/simplifyExpression/multiplyFractionsSearch.ts b/src/simplifyExpression/multiplyFractionsSearch.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/solveEquation/EquationOperators.ts b/src/solveEquation/EquationOperators.ts new file mode 100644 index 00000000..e69de29b diff --git a/lib/solveEquation/index.js b/src/solveEquation/solveEquation.ts similarity index 53% rename from lib/solveEquation/index.js rename to src/solveEquation/solveEquation.ts index c990bcd8..6abfa070 100644 --- a/lib/solveEquation/index.js +++ b/src/solveEquation/solveEquation.ts @@ -1,17 +1,13 @@ -const math = require('mathjs'); +module solveEquation{ +export function solveEquationString(equationString: string, debug=false) { + const comparators = ['<=', '>=', '=', '<', '>']; -const stepThrough = require('./stepThrough'); - -function solveEquationString(equationString, debug=false) { - const comparators = ['<=', '>=', '=', '<', '>']; - - for (let i = 0; i < comparators.length; i++) { - const comparator = comparators[i]; - const sides = equationString.split(comparator); + for (let i of comparators) { + const sides = equationString.split(i); if (sides.length !== 2) { continue; } - let leftNode, rightNode; + let leftNode: mathjs.MathNode, rightNode: mathjs.MathNode; const leftSide = sides[0].trim(); const rightSide = sides[1].trim(); @@ -33,5 +29,4 @@ function solveEquationString(equationString, debug=false) { return []; } - -module.exports = solveEquationString; +} \ No newline at end of file diff --git a/lib/util/clone.js b/src/util/clone.ts similarity index 80% rename from lib/util/clone.js rename to src/util/clone.ts index 7debbad4..a3c50e62 100644 --- a/lib/util/clone.js +++ b/src/util/clone.ts @@ -1,7 +1,9 @@ + // Simple clone function, which creates a deep copy of the given node // And recurses on the children (due to the shallow nature of the mathjs node // clone) -function clone(node) { +//need to somehow make this actually work in TS +export default function clone(node: mNode): mNode { const copy = node.clone(); copy.changeGroup = node.changeGroup; if (node.args) { @@ -13,6 +15,4 @@ function clone(node) { copy.content = clone(node.content); } return copy; -} - -module.exports = clone; +} \ No newline at end of file diff --git a/lib/util/evaluate.js b/src/util/evaluate.ts similarity index 83% rename from lib/util/evaluate.js rename to src/util/evaluate.ts index 53d79728..e5828556 100644 --- a/lib/util/evaluate.js +++ b/src/util/evaluate.ts @@ -2,9 +2,7 @@ // e.g. the tree representing (2 + 2) * 5 would be evaluated to the number 20 // it's important that `node` does not contain any symbol nodes -function evaluate(node) { +export function evaluate(node: mNode) { // TODO: once we swap in math-parser, call its evaluate function instead return node.eval(); } - -module.exports = evaluate; diff --git a/lib/util/flattenOperands.js b/src/util/flattenOperands.ts similarity index 87% rename from lib/util/flattenOperands.js rename to src/util/flattenOperands.ts index b85c81d4..343ae0bb 100644 --- a/lib/util/flattenOperands.js +++ b/src/util/flattenOperands.ts @@ -1,7 +1,3 @@ -const evaluate = require('./evaluate'); -const Negative = require('../Negative'); -const Node = require('../node'); - /* Background: @@ -34,18 +30,21 @@ interested in how that works // 2+2+2, ie one + node that has three children. // Input: an expression tree // Output: the expression tree updated with flattened operations -function flattenOperands(node) { - if (Node.Type.isConstant(node, true)) { +import * as nodeHelper from "../nodeHelper"; +import * as Negative from "../Negative"; + +function flattenOperands(node: mNode): mNode { + if (nodeHelper.Type.isConstant(node, true)) { // the evaluate() changes unary minuses around constant nodes to constant nodes // with negative values. - const constNode = Node.Creator.constant(evaluate(node)); + const constNode = nodeHelper.Creator.constant(evaluate(node)); if (node.changeGroup) { constNode.changeGroup = node.changeGroup; } return constNode; } - else if (Node.Type.isOperator(node)) { - if ('+-/*'.includes(node.op)) { + else if (nodeHelper.Type.isOperator(node)) { + if ('+-/*'.indexOf(node.op)>= 0) { let parentOp; if (node.op === '/') { // Division is flattened in partner with multiplication. This means @@ -70,11 +69,11 @@ function flattenOperands(node) { } return node; } - else if (Node.Type.isParenthesis(node)) { + else if (nodeHelper.Type.isParenthesis(node)) { node.content = flattenOperands(node.content); return node; } - else if (Node.Type.isUnaryMinus(node)) { + else if (nodeHelper.Type.isUnaryMinus(node)) { const arg = flattenOperands(node.args[0]); const flattenedNode = Negative.negate(arg, true); if (node.changeGroup) { @@ -82,11 +81,11 @@ function flattenOperands(node) { } return flattenedNode; } - else if (Node.Type.isFunction(node, 'abs')) { + else if (nodeHelper.Type.isFunction(node, 'abs')) { node.args[0] = flattenOperands(node.args[0]); return node; } - else if (Node.Type.isFunction(node, 'nthRoot')) { + else if (nodeHelper.Type.isFunction(node, 'nthRoot')) { node.args[0] = flattenOperands(node.args[0]); if (node.args[1]) { node.args[1] = flattenOperands(node.args[1]); @@ -104,7 +103,7 @@ function flattenOperands(node) { // NOTE: the returned node will be of operation type `parentOp`, regardless of // the operation type of `node`, unless `node` wasn't changed // e.g. 2 * 3 / 4 would be * of 2 and 3/4, but 2/3 would stay 2/3 and division -function flattenSupportedOperation(node, parentOp) { +function flattenSupportedOperation(node: mNode, parentOp) { // First get the list of operands that this operator operates on. // e.g. 2 + 3 + 4 + 5 is stored as (((2 + 3) + 4) + 5) in the tree and we // want to get the list [2, 3, 4, 5] @@ -126,11 +125,11 @@ function flattenSupportedOperation(node, parentOp) { // have ended up at the root. if (node.op === '/' && (operands.length > 2 || hasMultiplicationBesideDivision(node))) { - node = Node.Creator.operator('*', operands); + node = nodeHelper.Creator.operator('*', operands); } // similarily, - will become + always else if (node.op === '-') { - node = Node.Creator.operator('+', operands); + node = nodeHelper.Creator.operator('+', operands); } // otherwise keep the operator, replace operands else { @@ -150,12 +149,12 @@ function flattenSupportedOperation(node, parentOp) { // of type `op`. // Op is a string e.g. '+' or '*' // returns the list of all the node operated on by `parentOp` -function getOperands(node, parentOp) { +function getOperands(node: mNode, parentOp) { // We can only recurse on operations of type op. // If the node is not an operator node or of the right operation type, // we can't break up or flatten this tree any further, so we return just // the current node, and recurse on it to flatten its ops. - if (!Node.Type.isOperator(node)) { + if (!nodeHelper.Type.isOperator(node)) { return [flattenOperands(node)]; } switch (node.op) { @@ -175,7 +174,7 @@ function getOperands(node, parentOp) { default: return [flattenOperands(node)]; } - if (Node.PolynomialTerm.isPolynomialTerm(node, true)) { + if (nodeHelper.PolynomialTerm.isPolynomialTerm(node, true)) { node.args.forEach((arg, i) => { node.args[i] = flattenOperands(node.args[i]); }); @@ -221,7 +220,7 @@ function getOperands(node, parentOp) { // 2*2*x (which has three children). // So this function would return true for the input 2*2x, if it was stored as // an expression tree with root node * and children 2*2 and x -function isPolynomialTermMultiplication(node) { +function isPolynomialTermMultiplication(node: mNode) { // This concept only applies when we're flattening multiplication operations if (node.op !== '*') { return false; @@ -233,8 +232,8 @@ function isPolynomialTermMultiplication(node) { // The second node should be for the form x or x^2 (ie a polynomial term // with no coefficient) const secondOperand = node.args[1]; - if (Node.PolynomialTerm.isPolynomialTerm(secondOperand)) { - const polyNode = new Node.PolynomialTerm(secondOperand); + if (nodeHelper.PolynomialTerm.isPolynomialTerm(secondOperand)) { + const polyNode = new nodeHelper.PolynomialTerm(secondOperand); return !polyNode.hasCoeff(); } else { @@ -246,7 +245,7 @@ function isPolynomialTermMultiplication(node) { // and flattens it appropriately so the coefficient and symbol are grouped // together. Returns a new list of operands from this node that should be // multiplied together. -function maybeFlattenPolynomialTerm(node) { +function maybeFlattenPolynomialTerm(node: mNode) { // We recurse on the left side of the tree to find operands so far const operands = getOperands(node.args[0], '*'); @@ -262,10 +261,10 @@ function maybeFlattenPolynomialTerm(node) { const nextOperand = flattenOperands(node.args[1]); // a coefficient can be constant or a fraction of constants - if (Node.Type.isConstantOrConstantFraction(lastOperand)) { + if (nodeHelper.Type.isConstantOrConstantFraction(lastOperand)) { // we replace the constant (which we popped) with constant*symbol operands.push( - Node.Creator.operator('*', [lastOperand, nextOperand], true)); + nodeHelper.Creator.operator('*', [lastOperand, nextOperand], true)); } // Now we know it isn't a polynomial term, it's just another seperate operand else { @@ -280,7 +279,7 @@ function maybeFlattenPolynomialTerm(node) { // are to be multiplied together. Otherwise, a list of length one with // just the division node is returned. getOperands might change the // operator accordingly. -function flattenDivision(node) { +function flattenDivision(node: mNode) { // We recurse on the left side of the tree to find operands so far // Flattening division is always considered part of a bigger picture // of multiplication, so we get operands with '*' @@ -298,7 +297,7 @@ function flattenDivision(node) { const denominator = flattenOperands(node.args[1]); // Note that this means 2 * 3 * 4 / 5 / 6 * 7 will flatten but keep the 4/5/6 // as an operand - in simplifyDivision.js this is changed to 4/(5*6) - const divisionNode = Node.Creator.operator('/', [numerator, denominator]); + const divisionNode = nodeHelper.Creator.operator('/', [numerator, denominator]); operands.push(divisionNode); } @@ -309,8 +308,8 @@ function flattenDivision(node) { // operators or parentheses between them. // e.g. returns true: 2*3/4, 2 / 5 / 6 * 7 / 8 // e.g. returns false: 3/4/5, ((3*2) - 5) / 7, (2*5)/6 -function hasMultiplicationBesideDivision(node) { - if (!Node.Type.isOperator(node)) { +function hasMultiplicationBesideDivision(node: mNode) { + if (!nodeHelper.Type.isOperator(node)) { return false; } if (node.op === '*') { @@ -321,6 +320,4 @@ function hasMultiplicationBesideDivision(node) { return false; } return node.args.some(hasMultiplicationBesideDivision); -} - -module.exports = flattenOperands; +} \ No newline at end of file diff --git a/lib/util/print.js b/src/util/printNode.ts similarity index 74% rename from lib/util/print.js rename to src/util/printNode.ts index 3bfb02bd..34d0ea88 100644 --- a/lib/util/print.js +++ b/src/util/printNode.ts @@ -1,13 +1,12 @@ -const clone = require('./clone'); -const flatten = require('./flattenOperands'); -const Node = require('../node'); - +import clone from "./clone"; +import * as flatten from "./flattenOperands"; +import * as nodeHelper from "../nodeHelper"; // Prints an expression node in asciimath // If showPlusMinus is true, print + - (e.g. 2 + -3) // If it's false (the default) 2 + -3 would print as 2 - 3 // (The + - is needed to support the conversion of subtraction to addition of // negative terms. See flattenOperands for more details if you're curious.) -function print(node, showPlusMinus=false) { +export function print(node: mNode, showPlusMinus=false) { node = flatten(clone(node)); let string = printTreeTraversal(node); @@ -17,9 +16,9 @@ function print(node, showPlusMinus=false) { return string; } -function printTreeTraversal(node, parentNode) { - if (Node.PolynomialTerm.isPolynomialTerm(node)) { - const polyTerm = new Node.PolynomialTerm(node); +function printTreeTraversal(node: mNode, parentNode?: mNode): string { + if (nodeHelper.PolynomialTerm.isPolynomialTerm(node)) { + const polyTerm = new nodeHelper.PolynomialTerm(node); // This is so we don't print 2/3 x^2 as 2 / 3x^2 // Still print x/2 as x/2 and not 1/2 x though if (polyTerm.hasFractionCoeff() && node.op !== '/') { @@ -34,12 +33,12 @@ function printTreeTraversal(node, parentNode) { } } - if (Node.Type.isIntegerFraction(node)) { + if (nodeHelper.Type.isIntegerFraction(node)) { return `${node.args[0]}/${node.args[1]}`; } - if (Node.Type.isOperator(node)) { - if (node.op === '/' && Node.Type.isOperator(node.args[1])) { + if (nodeHelper.Type.isOperator(node)) { + if (node.op === '/' && nodeHelper.Type.isOperator(node.args[1])) { return `${printTreeTraversal(node.args[0])} / (${printTreeTraversal(node.args[1])})`; } @@ -59,7 +58,7 @@ function printTreeTraversal(node, parentNode) { break; case '/': // no space for constant fraction divisions (slightly easier to read) - if (Node.Type.isConstantFraction(node, true)) { + if (nodeHelper.Type.isConstantFraction(node, true)) { opString = `${node.op}`; } else { @@ -79,7 +78,7 @@ function printTreeTraversal(node, parentNode) { // Check #120, #126 issues for more details. // { "/" [{ "+" ["x", "2"] }, "2"] } -> (x + 2) / 2. if (parentNode && - Node.Type.isOperator(parentNode) && + nodeHelper.Type.isOperator(parentNode) && node.op && parentNode.op && '*/^'.indexOf(parentNode.op) >= 0 && '+-'.indexOf(node.op) >= 0) { @@ -88,13 +87,13 @@ function printTreeTraversal(node, parentNode) { return str; } - else if (Node.Type.isParenthesis(node)) { + else if (nodeHelper.Type.isParenthesis(node)) { return `(${printTreeTraversal(node.content)})`; } - else if (Node.Type.isUnaryMinus(node)) { - if (Node.Type.isOperator(node.args[0]) && + else if (nodeHelper.Type.isUnaryMinus(node)) { + if (nodeHelper.Type.isOperator(node.args[0]) && '*/^'.indexOf(node.args[0].op) === -1 && - !Node.PolynomialTerm.isPolynomialTerm(node)) { + !nodeHelper.PolynomialTerm.isPolynomialTerm(node)) { return `-(${printTreeTraversal(node.args[0])})`; } else { @@ -105,5 +104,3 @@ function printTreeTraversal(node, parentNode) { return node.toString(); } } - -module.exports = print; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..82e472b1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "noImplicitAny": true, + "removeComments": true, + "preserveConstEnums": true, + "outDir": "lib", + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file