diff --git a/lib/__test__/__snapshots__/query.test.js.snap b/lib/__test__/__snapshots__/query.test.js.snap index 39b406b..dac50cc 100644 --- a/lib/__test__/__snapshots__/query.test.js.snap +++ b/lib/__test__/__snapshots__/query.test.js.snap @@ -177,6 +177,45 @@ exports[`query getCoefficientsAndConstants 3x^2 * 2x^2 1`] = ` } `; +exports[`query getCoefficientsAndConstants x + 4 + 2^x + x 1`] = ` +{ + "others": [ + { + "type": "Apply", + "op": "pow", + "args": [ + { + "value": "2", + "type": "Number" + }, + { + "type": "Identifier", + "name": "x" + } + ] + } + ], + "constants": [ + { + "value": "4", + "type": "Number" + } + ], + "coefficientMap": { + "x": [ + { + "value": "1", + "type": "Number" + }, + { + "value": "1", + "type": "Number" + } + ] + } +} +`; + exports[`query getCoefficientsAndConstants x^3 + y^3 + x y z + 3 1`] = ` { "others": [ @@ -210,6 +249,55 @@ exports[`query getCoefficientsAndConstants x^3 + y^3 + x y z + 3 1`] = ` } `; +exports[`query getCoefficientsAndConstants z + 2*(y + x) + 4 + z 1`] = ` +{ + "others": [ + { + "type": "Apply", + "op": "mul", + "args": [ + { + "value": "2", + "type": "Number" + }, + { + "type": "Apply", + "op": "add", + "args": [ + { + "type": "Identifier", + "name": "y" + }, + { + "type": "Identifier", + "name": "x" + } + ] + } + ] + } + ], + "constants": [ + { + "value": "4", + "type": "Number" + } + ], + "coefficientMap": { + "z": [ + { + "value": "1", + "type": "Number" + }, + { + "value": "1", + "type": "Number" + } + ] + } +} +`; + exports[`query getVariableFactors 2 x y z 1`] = ` [ { @@ -236,6 +324,29 @@ exports[`query getVariableFactors 2x 1`] = ` ] `; +exports[`query getVariableFactors 2x^2 * y 1`] = ` +[ + { + "type": "Apply", + "op": "pow", + "args": [ + { + "type": "Identifier", + "name": "x" + }, + { + "value": "2", + "type": "Number" + } + ] + }, + { + "type": "Identifier", + "name": "y" + } +] +`; + exports[`query getVariableFactors 2x^2 1`] = ` [ { @@ -255,6 +366,39 @@ exports[`query getVariableFactors 2x^2 1`] = ` ] `; +exports[`query getVariableFactors x^2 * 2/3 y^2 1`] = ` +[ + { + "type": "Apply", + "op": "pow", + "args": [ + { + "type": "Identifier", + "name": "x" + }, + { + "value": "2", + "type": "Number" + } + ] + }, + { + "type": "Apply", + "op": "pow", + "args": [ + { + "type": "Identifier", + "name": "y" + }, + { + "value": "2", + "type": "Number" + } + ] + } +] +`; + exports[`query getVariableFactors x^2 1`] = ` [ { diff --git a/lib/__test__/query.test.js b/lib/__test__/query.test.js index 2a84882..9e6c88f 100644 --- a/lib/__test__/query.test.js +++ b/lib/__test__/query.test.js @@ -1,5 +1,5 @@ import assert from 'assert' -import {parse} from 'math-parser' +import {parse, print} from 'math-parser' import stringify from 'json-stable-stringify' import * as query from '../query' @@ -54,6 +54,8 @@ describe('query', () => { 'x^2', '2x^2', '2 x y z', + '2x^2 * y', + 'x^2 * 2/3 y^2' ]) suite('getCoefficientsAndConstants', query.getCoefficientsAndConstants, [ @@ -65,7 +67,9 @@ describe('query', () => { 'x^3 + y^3 + x y z + 3', '2/3 + 3 + cos(4)', '3x^2 * 2x^2', - '2/3(x + 1)^1' + '2/3(x + 1)^1', + 'x + 4 + 2^x + x', + 'z + 2*(y + x) + 4 + z' ]) it('isVariableFactor', () => { @@ -106,6 +110,27 @@ describe('query', () => { assert(!query.isPolynomial(parse('1/x'))) }) + it('hasCoeff', () => { + assert(query.hasCoeff(parse('2x'))) + assert(query.hasCoeff(parse('2x^2'))) + assert(query.hasCoeff(parse('2 x y z'))) + assert(!query.hasCoeff(parse('2'))) + assert(!query.hasCoeff(parse('x'))) + }) + + it('negate', () => { + assert.equal(print(build.negate(parse('2'))), -2) + assert.equal(print(build.negate(parse('2x^2'))), '-2 x^2') + assert.equal(print(build.negate(parse('2xyz'))), '-2 xyz') + assert.equal(print(build.negate(parse('-2'))), 2) + assert.equal(print(build.negate(parse('-2x^2'))), '2 x^2') + assert.equal(print(build.negate(parse('-2x^2 y^2'))), '2 x^2 y^2') + assert.equal(print(build.negate(parse('2x^2 y^2'))), '-2 x^2 y^2') + assert.equal(print(build.negate(parse('2/3'))), '-2 / 3') + assert.equal(print(build.negate(parse('-2/3'))), '2 / 3') + assert.equal(print(build.negate(parse('(x+3)/3'))), '-(x + 3) / 3') + }) + it('hasSameBase', () => { assert(query.hasSameBase(parse('x^1'), parse('x^2'))) assert(query.hasSameBase(parse('(x+1)^2'), parse('(x+1)^x'))) @@ -262,10 +287,13 @@ describe('query', () => { assert(!query.isRel(x)) }) - it('isNumber', () => { - assert(query.isNumber(a)) - assert(query.isNumber(parse('-2'))) - assert(!query.isNumber(x)) + it('isRationalNumber', () => { + assert(query.isRationalNumber(a)) + assert(query.isRationalNumber(parse('-2'))) + assert(query.isRationalNumber(parse('2/3'))) + assert(query.isRationalNumber(parse('2.2'))) + assert(!query.isRationalNumber(x)) + assert(!query.isRationalNumber(parse('(2 + x)/2'))) }) it('getValue', () => { diff --git a/lib/build.js b/lib/build.js index 4c4a4ce..a05df6d 100644 --- a/lib/build.js +++ b/lib/build.js @@ -2,6 +2,8 @@ * Functions to build nodes */ +const q = require('./query.js') + export const apply = (op, args, options = {}) => ({ type: 'Apply', op: op, @@ -56,3 +58,23 @@ export const parensNode = parens export const numberNode = number export const identifierNode = identifier export const applyNode = apply + +// e.g 3 -> -3, -3x -> 3x, x + 3 -> -(x + 3), 2/3 -> -2 / 3 +export const negate = (node) => { + if (q.isNeg(node)) { + return node.args[0] + } else if (q.isAdd(node)) { + return neg(node) + } else if (q.isFraction(node)) { + return div(negate(q.getNumerator(node)), q.getDenominator(node)) + } else if (q.isPolynomialTerm(node)) { + if (q.isNeg(q.getCoefficient(node))) { + return apply( + 'mul', + [negate(q.getCoefficient(node)), ...node.args.slice(1)], + {implicit: node.implicit}) + } else { + return neg(node) + } + } +} diff --git a/lib/query.js b/lib/query.js index 4264493..db79dc4 100644 --- a/lib/query.js +++ b/lib/query.js @@ -10,7 +10,7 @@ export const isApply = node => node && node.type === 'Apply' export const isParens = node => node && node.type === 'Parentheses' // deprecated, use isApply -export const isOperation = node => isApply(node) && !isNumber(node) +export const isOperation = node => isApply(node) && !isRationalNumber(node) export const isFunction = node => isApply(node) && isIdentifier(node.op) // TODO: curry it? @@ -28,9 +28,9 @@ export const isFact = node => _isOp('fact', node) && hasArity(1, node) export const isNthRoot = node => _isOp('nthRoot', node) && hasArity(2, node) export const isFraction = node => isNeg(node) ? isFraction(node.args[0]) : isDiv(node) -export const isConstantFraction = node => isFraction(node) && node.args.every(isNumber) +export const isConstantFraction = node => isFraction(node) && node.args.every(isRationalNumber) export const isIntegerFraction = node => isFraction(node) && node.args.every(isInteger) -export const isDecimal = node => isNumber(node) && getValue(node) % 1 != 0 +export const isDecimal = node => getValue(node) % 1 != 0 const relationIdentifierMap = { 'eq': '=', @@ -43,25 +43,25 @@ const relationIdentifierMap = { export const isRel = node => isApply(node) && node.op in relationIdentifierMap -export const isNumber = node => { - if (node.type === 'Number') { +export const isRationalNumber = node => { + if (node.type === 'Number' || isConstantFraction(node) || isIntegerFraction(node) || isDecimal(node) ) { return true } else if (isNeg(node)) { - return isNumber(node.args[0]) + return isRationalNumber(node.args[0]) } else { return false } } export const isInteger = node => { - return isNumber(node) && Number.isInteger(getValue(node)) + return isRationalNumber(node) && Number.isInteger(getValue(node)) } export const isPolynomial = (node) => { if (isPolynomialTerm(node)) { return true } else if (isDiv(node)) { - return isNumber(getDenominator(node)) && isPolynomial(getNumerator(node)) + return isRationalNumber(getDenominator(node)) && isPolynomial(getNumerator(node)) } else if (isAdd(node) || isMul(node)) { return node.args.every(isPolynomialTerm) } else { @@ -72,10 +72,10 @@ export const isPolynomial = (node) => { export const isVariableFactor = (node) => isIdentifier(node) || isPow(node) && (isPolynomial(node.args[0]) || isPolynomialTerm(node)) - && (isNumber(node.args[1]) || isVariableFactor(node.args[1])) + && (isRationalNumber(node.args[1]) || isVariableFactor(node.args[1])) export const isPolynomialTerm = (node) => { - if (isNumber(node) || isConstantFraction(node) || isDecimal(node)) { + if (isRationalNumber(node)) { return true } else if (isIdentifier(node)) { return true @@ -132,13 +132,13 @@ export const getDenominator = (node) => { } export const hasConstantBase = node => isPow(node) && - (isNumber(node.args[0]) || isConstantFraction(node.args[0]) || isIntegerFraction(node.args[0])) + (isRationalNumber(node.args[0]) || isConstantFraction(node.args[0]) || isIntegerFraction(node.args[0])) // TODO: handle multivariable polynomials // Get degree of a polynomial term // e.g. 6x^2 -> 2 export const getPolyDegree = (node) => { - if (isNumber(node)) { + if (isRationalNumber(node)) { return build.number(0) } else if (isIdentifier(node)) { return build.number(1) @@ -158,14 +158,14 @@ export const getPolyDegree = (node) => { // TODO: 2^2 export const getCoefficient = (node) => { - if (isNumber(node) || hasConstantBase(node)) { + if (isRationalNumber(node) || hasConstantBase(node)) { return node } else if (isIdentifier(node) || isPow(node)) { return build.numberNode('1') } else if (isNeg(node)) { return build.neg(getCoefficient(node.args[0]), {wasMinus: node.wasMinus}) } else if (isMul(node)) { - const numbers = node.args.filter(arg => isNumber(arg) || isConstantFraction(arg)) + const numbers = node.args.filter(arg => isRationalNumber(arg) || isConstantFraction(arg)) if (numbers.length > 1) { return build.mul(...numbers) } else if (numbers.length === 1) { @@ -176,17 +176,16 @@ export const getCoefficient = (node) => { } } -export const getVariableFactors = (node) => { +export const getVariableFactors = (node, factors = []) => { if (isVariableFactor(node)) { - return [node] + factors.push(node) } else if (isMul(node)) { - return node.args.filter(isVariableFactor) + node.args.forEach(arg => getVariableFactors(arg, factors)) } else if (isNeg(node)) { // TODO: figure out how to handle (x)(-y)(z) return getVariableFactors(node.args[0]) - } else { - return [] } + return factors } export const getVariableFactorName = (node) => { @@ -213,12 +212,12 @@ export const sortVariables = (variables) => {constants: [3], coefficientMap: ['x^2': [2, 5], 'x': [3]]} */ export const getCoefficientsAndConstants = (node, coefficientMap = {}, constants = [], others = []) => { - if (isNumber(node) || isConstantFraction(node)) { + if (isRationalNumber(node) || isConstantFraction(node)) { constants.push(node) - } else if (isFunction(node)) { + } else if (isFunction(node) || hasConstantBase(node)) { // cos, sin, f(a), etc others.push(node) - } else if (isPolynomialTerm(node) && !hasConstantBase(node)) { + } else if (isPolynomialTerm(node)) { // x^2, 3x^2 // sort the variables first (2yzx -> 2xyz) @@ -235,8 +234,8 @@ export const getCoefficientsAndConstants = (node, coefficientMap = {}, constants } else { coefficientMap[key].push(coefficient) } - } else if(isPolynomial(node) || isApply(node)) { - // 2x^2 + 3x + 1, 3x^2 * 2x^2 + } else if(isPolynomial(node) || isAdd(node) || (isMul(node) && node.args.every(isPolynomialTerm))) { + // 2x^2 + 3x + 1, 3x^2 * 2x^2, x + 4 + x + 2^x node.args.forEach(function(arg) { getCoefficientsAndConstants(arg, coefficientMap, constants, others) }) @@ -258,3 +257,7 @@ export const hasSameBase = (node1, node2) => { } export const nodeEquals = (node1, node2) => print(node1) === print(node2) + +export const hasCoeff = (node) => { + return isPolynomialTerm(node) && !isIdentifier(node) && !isRationalNumber(node) +}