Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some additional helper functions #7

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions lib/__test__/query.test.js
Original file line number Diff line number Diff line change
@@ -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'

const reverseAlphabetical = (a, b) => a.key < b.key ? 1 : -1
Expand Down Expand Up @@ -109,6 +109,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(query.negate(parse('2'))), -2)
assert.equal(print(query.negate(parse('2x^2'))), `-2 x^2`)
assert.equal(print(query.negate(parse('2xyz'))), `-2 xyz`)
assert.equal(print(query.negate(parse('-2'))), 2)
assert.equal(print(query.negate(parse('-2x^2'))), `2 x^2`)
assert.equal(print(query.negate(parse('-2x^2 y^2'))), `2 x^2 y^2`)
assert.equal(print(query.negate(parse('2x^2 y^2'))), `-2 x^2 y^2`)
assert.equal(print(query.negate(parse('2/3'))), `-2 / 3`)
assert.equal(print(query.negate(parse('-2/3'))), `2 / 3`)
assert.equal(print(query.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')))
Expand Down Expand Up @@ -225,10 +246,10 @@ describe('query', () => {
assert(!query.isRel(x))
})

it('isNumber', () => {
assert(query.isNumber(a))
assert(query.isNumber(parse('-2')))
assert(!query.isNumber(x))
it('isRationalNumber', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add some tests that check for factions and decimals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

assert(query.isRationalNumber(a))
assert(query.isRationalNumber(parse('-2')))
assert(!query.isRationalNumber(x))
})

it('getValue', () => {
Expand Down
56 changes: 40 additions & 16 deletions lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -26,9 +26,9 @@ export const isAbs = node => _isOp('abs', node)
export const isFact = node => _isOp('fact', node)
export const isNthRoot = node => _isOp('nthRoot', 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': '=',
Expand All @@ -41,25 +41,25 @@ const relationIdentifierMap = {

export const isRel = node => isApply(node) && node.op in relationIdentifierMap

export const isNumber = node => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep isNumber in addition to isRationalNumber.

Copy link
Contributor Author

@aliang8 aliang8 Jul 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm I don't see how it would be useful, but I add it back.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we isInteger. Okay, let's leave this as is. We can always add it back later if it does turn out we need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure :D

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 {
Expand All @@ -70,10 +70,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
Expand Down Expand Up @@ -130,13 +130,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)
Expand All @@ -156,14 +156,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) {
Expand Down Expand Up @@ -210,7 +210,7 @@ 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) || hasConstantBase(node)) {
// cos, sin, f(a), etc
Expand All @@ -233,7 +233,7 @@ export const getCoefficientsAndConstants = (node, coefficientMap = {}, constants
coefficientMap[key].push(coefficient)
}
} else if(isPolynomial(node) || isAdd(node) || (isMul(node) && node.args.every(isPolynomialTerm))) {
// 2x^2 + 3x + 1, 3x^2 * 2x^2
// 2x^2 + 3x + 1, 3x^2 * 2x^2, x + 4 + x + 2^x
node.args.forEach(function(arg) {
getCoefficientsAndConstants(arg, coefficientMap, constants, others)
})
Expand All @@ -255,3 +255,27 @@ 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)
}

// e.g 3 -> -3, -3x -> 3x, x + 3 -> -(x + 3), 2/3 -> -2 / 3
export const negate = (node) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be part of build not query.

if (isNeg(node)) {
return node.args[0]
} else if (isAdd(node)) {
return build.neg(node)
} else if (isFraction(node)) {
return build.div(negate(getNumerator(node)), getDenominator(node))
} else if (isPolynomialTerm(node)) {
if (isNeg(getCoefficient(node))) {
return build.apply(
'mul',
[negate(getCoefficient(node)), ...node.args.slice(1)],
{implicit: node.implicit})
} else {
return build.neg(node)
}
}
}