diff --git a/lib/__test__/rule-list.test.js b/lib/__test__/rule-list.test.js index 32f70c0..3b37211 100644 --- a/lib/__test__/rule-list.test.js +++ b/lib/__test__/rule-list.test.js @@ -364,6 +364,12 @@ describe('rules', () => { ['(2^3)^x', '8^x'], ]) + suite('adding exponent of one', rules.ADD_EXPONENT_OF_ONE, [ + ['x^2 * x', 'x^2 * x^1'], + ['x^2 * 2 * x * x', 'x^2 * 2 * x^1 * x^1'], + ['2 + 3x^2 * x y z', '2 + 3 x^2 * (x^1 * y^1 * z^1)'], + ]) + suite('product rule', rules.PRODUCT_RULE, [ ['10^2 * 10^5 * 10^3', '10^(2 + 5 + 3)'], ['x^a * x^b * x^c', 'x^(a + b + c)'], @@ -380,6 +386,31 @@ describe('rules', () => { ['x^-a / x^-b', 'x^(-a - -b)'], ]) + suite('multiplying coefficients', rules.MULTIPLY_COEFFICIENTS, [ + ['x^2 * y^2', 'x^2 * y^2'], + ['x^2y^2z^2 * 2x^2', '2 (x^2 * y^2 * z^2 * x^2)'], + ['3x^2 * x^2', '3 (x^2 * x^2)'], + ['x^3 * y^2', 'x^3 * y^2'], + ['x^3 + 2x^1 + 3x^1 * 5x^1', 'x^3 + 2 x^1 + (3 * 5) (x^1 * x^1)'], + ['x^3 * x^3 * x^3', 'x^3 * x^3 * x^3'], + ['x^1 * x^1 * x^1 * x^1 * x^1', 'x^1 * x^1 * x^1 * x^1 * x^1'], + ['2/3x^1 * 3x^1', '(2 / 3 * 3) (x^1 * x^1)'], + ['2/3(x + 1)^1 * 2x^3', '(2 / 3 * 2) ((x + 1)^1 * x^3)'], + ['(x + 1)^3 * 2(x + 3)^3', '2 ((x + 1)^3 * (x + 3)^3)'], + ]) + + suite('simplfy coefficients', rules.SIMPLIFY_COEFFICIENTS, [ + ['(3 * 2)(x^2 * y^3)', '6 (x^2 * y^3)'], + ]) + + suite('multiplying polynomial terms', rules.MULTIPLY_POLYNOMIAL_TERMS, [ + ['x^2 * x^1', 'x^3'], + ['x^3 * y^2', 'x^3 y^2'], + ['x^3 + x^1 + x^1 * x^1 * y^3', 'x^3 + x^1 + x^2 y^3'], + ['(x+1)^2 * (x+1)^3', '(x + 1)^5'], + ['x^1 * x^3 * (2x + 3)^2', 'x^4 (2 x + 3)^2'], + ]) + suite('power of a product', rules.POWER_OF_A_PRODUCT, [ ['(2*3)^x', '2^x * 3^x'], ['(2*3*5)^x', '2^x * 3^x * 5^x'], diff --git a/lib/rule-list.js b/lib/rule-list.js index 45f7c5e..8e9ffc5 100644 --- a/lib/rule-list.js +++ b/lib/rule-list.js @@ -5,7 +5,7 @@ import {build, query} from 'math-nodes' import {traverse} from 'math-traverse' import {defineRule, definePatternRule, applyRule, canApplyRule} from './rule' -import {isPolynomialTerm, getCoefficient, getVariableFactors} from './rules/collect-like-terms.js' +import {isPolynomialTerm, getCoefficient, getVariableFactors, getCoefficientsAndConstants} from './rules/collect-like-terms.js' import {clone, getRanges} from './utils' const defineRuleString = (matchPattern, rewritePattern, constraints) => { @@ -350,10 +350,46 @@ export const GROUP_TERMS_BY_ROOT = defineRule( // e.g nthRoot(9, 2) -> 3 export const NTH_ROOT_VALUE = defineRuleString('nthRoot(#a, #b)', '#eval(nthRoot(#a, #b))', {a: query.isNumber, b: query.isNumber}) -// MULTIPLYING POLYNOMIALS - // e.g. x^2 * x -> x^2 * x^1 -// export const ADD_EXPONENT_OF_ONE = ... +export const ADD_EXPONENT_OF_ONE = defineRule( + (node) => { + let hasIdentifier = false + if (query.isMul(node)) { + for (var i in node.args) { + let term = node.args[i] + // check if there is a variable with exponent 1 + hasIdentifier = query.isMul(term) + ? term.some(arg => {return query.isIdentifier(arg)}) + : query.isIdentifier(term) + } + } + + return hasIdentifier ? {node} : null + }, + + (node) => { + const result = build.applyNode( + 'mul', + node.args.map(term => { + let one = build.number(1) + if (query.isMul(term)) { + term.args(arg => { + if (query.isIdentifier(arg)){ + return build.apply('pow', [term, one]) + } else { + return arg + } + }) + } else if (query.isIdentifier(term)) { + return build.apply('pow', [term, one]) + } else { + return term + } + }) + ) + return result + } +) // EXPONENT RULES @@ -363,6 +399,82 @@ export const PRODUCT_RULE = defineRuleString('#a^#b_0 * ...', '#a^(#b_0 + ...)') // e.g. x^5 / x^3 -> x^(5 - 3) export const QUOTIENT_RULE = defineRuleString('#a^#p / #a^#q', '#a^(#p - #q)') +// e.g. 3x^2 * 2x^2 -> (3 * 2)(x^2 * x^2) +export const MULTIPLY_COEFFICIENTS = defineRule( + (node) => { + let isMulOfPolynomials = false + + if (query.isMul(node)) { + const {constants, coefficientMap} = getCoefficientsAndConstants(node) + isMulOfPolynomials = Object.keys(coefficientMap).length > 1 + || Object.keys(coefficientMap) + .some(key => coefficientMap[key].length > 1) + } + + return (isMulOfPolynomials && !node.implicit) ? {node} : null + }, + (node) => { + const terms = [] + const coeffs = [] + traverse(node, { + enter(node) { + if(query.isPow(node)){ + terms.push(node) + } + } + }) + node.args.forEach(function(arg) { + if (query.getValue(getCoefficient(arg)) != 1) { + coeffs.push(getCoefficient(arg)) + } + }) + + let poly = build.mul(...terms) + + const result = coeffs.length > 1 + ? build.implicitMul(build.mul(...coeffs), poly) + : coeffs.length == 1 + ? build.implicitMul(coeffs[0], poly) + : poly + + return result + } +) + +// e.g. (3 * 2)(x^3 y^2) -> 6 x^3 y^2 +export const SIMPLIFY_COEFFICIENTS = defineRuleString( + '(#a_0 * ...) #b', '#eval(#a_0 * ...) #b' +) + +// done after SIMPLIFY_COEFFICIENTS +// e.g. x^3 * x^2 * y^2 -> x^5 * y^2 +export const MULTIPLY_POLYNOMIAL_TERMS = defineRule( + (node) => { + let coefficientOfOne = false + if(query.isMul(node)){ + coefficientOfOne = node.args.every(term => { + return isPolynomialTerm(term) + && query.getValue(getCoefficient(term)) == 1 + }) + } + + return coefficientOfOne ? {node} : null + }, + + (node) => { + let result = node + while(canApplyRule(PRODUCT_RULE, result)) { + result = applyRule(PRODUCT_RULE, result) + } + while(canApplyRule(SIMPLIFY_ARITHMETIC, result)) { + result = applyRule(SIMPLIFY_ARITHMETIC, result) + } + return query.isMul(result) + ? build.apply('mul', result.args, {implicit: true}) + : result + } +) + // e.g. (a * b)^x -> a^x * b^x export const POWER_OF_A_PRODUCT = defineRuleString('(#a_0 * ...)^#b', '#a_0^#b * ...') @@ -403,7 +515,9 @@ export const DISTRIBUTE_NEGATIVE_ONE = // COLLECT AND COMBINE export {default as COLLECT_LIKE_TERMS} from './rules/collect-like-terms' -export const FRACTIONAL_POLYNOMIALS = defineRuleString('#a #b/#c', '#a / #c #b') +export const FRACTIONAL_POLYNOMIALS = defineRuleString( + '#a #b/#c', '#a / #c #b' +) export {ADD_POLYNOMIAL_TERMS} from './rules/collect-like-terms' diff --git a/lib/rules/collect-like-terms.js b/lib/rules/collect-like-terms.js index 25e439f..22df2dd 100644 --- a/lib/rules/collect-like-terms.js +++ b/lib/rules/collect-like-terms.js @@ -15,20 +15,19 @@ const isPolynomial = (node) => { return query.isAdd(node) && node.args.every(isPolynomialTerm) } -// x, 2x, xy, 2xy, x^2, ... -// isFactor matches x, 2, x^2 +// x, 2x, xy, 2xy, x^2, (x+1)^2 ... // match either #x, #x^#b, or #a where #x is an identifier and #b and #a are numbers // but really we want #b to match either a number or a fraction with numbers for // numerator and denominator export const isPolynomialTerm = (node) => { - if (query.isNumber(node)) { + if (query.isNumber(node) || isConstantFraction(node)) { return true } else if (query.isIdentifier(node)) { return true } else if (query.isPow(node)) { const [base, exponent] = node.args - return query.isIdentifier(base) && isPolynomialTerm(exponent) + return (query.isIdentifier(base) || isPolynomial(base)) && isPolynomialTerm(exponent) } else if (query.isNeg(node)) { return isPolynomialTerm(node.args[0]) } else if (query.isMul(node)) { @@ -36,6 +35,10 @@ export const isPolynomialTerm = (node) => { } } +export const isConstantFraction = (node) => { + return query.isDiv(node) && query.isNumber(node.args[0]) && query.isNumber(node.args[1]) +} + export const getCoefficient = (node) => { if (query.isNumber(node)) { return node @@ -46,7 +49,7 @@ export const getCoefficient = (node) => { result.wasMinus = node.wasMinus return result } else if (query.isMul(node)) { - const numbers = node.args.filter(query.isNumber) + const numbers = node.args.filter(arg => query.isNumber(arg) || isConstantFraction(arg)) if (numbers.length > 1) { return build.applyNode('mul', numbers) } else if (numbers.length > 0) { @@ -57,10 +60,11 @@ export const getCoefficient = (node) => { } } +// isFactor matches x, x^2, (x + 1)^2 export const isVariableFactor = (node) => query.isIdentifier(node) || - query.isPow(node) && query.isIdentifier(node.args[0]) && - (query.isNumber(node.args[1]) || isVariableFactor(node.args[1])) + query.isPow(node) && (query.isNumber(node.args[1]) || isVariableFactor(node.args[1])) + export const getVariableFactors = (node) => { @@ -76,7 +80,7 @@ export const getVariableFactors = (node) => { } } -const getVariableFactorName = (node) => { +export const getVariableFactorName = (node) => { if (query.isIdentifier(node)) { return node.name } else if (query.isPow(node)) { @@ -100,12 +104,13 @@ const isImplicit = (node) => { } } -const getCoefficientsAndConstants = (node) => { +// TODO: fix the case of single polynomials ex. 2x^2 +export const getCoefficientsAndConstants = (node) => { const coefficientMap = {} const constants = [] node.args.forEach(arg => { - if (query.isNumber(arg)) { + if (query.isNumber(arg) || isConstantFraction(arg)) { constants.push(arg) } else { const sortedVariables = sortVariables(getVariableFactors(arg)) diff --git a/package.json b/package.json index ce4a265..ebbde4e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "babel-plugin-transform-object-rest-spread": "^6.23.0", "babel-polyfill": "^6.23.0", "babel-preset-es2015": "^6.24.1", - "eslint": "^3.15.0", + "eslint": "^3.19.0", "jest": "^20.0.4", "pre-commit": "^1.2.2", "webpack": "^2.6.1"