Skip to content
Open
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
04bb0ee
added rules for patterns w/o constraints
aliang8 Apr 30, 2017
f37f48c
added rules test, some errors
aliang8 Apr 30, 2017
5745443
rm file
aliang8 Apr 30, 2017
b462d8a
rm file
aliang8 Apr 30, 2017
3ca4c7f
format
aliang8 Apr 30, 2017
adcbc10
fixed tabing, remove comments, added rules, need to check variables
aliang8 Apr 30, 2017
3cb89d4
added extra test cases, not done
aliang8 Apr 30, 2017
75ab164
not sure what's up with these tests
aliang8 Apr 30, 2017
3beef3e
finished adding cases, still some errors
aliang8 May 1, 2017
c35f087
fixed merge errors
aliang8 May 2, 2017
20bc705
one last test
aliang8 May 2, 2017
336c5c3
fixed tests and yarn
aliang8 May 2, 2017
3dbb8cd
done
aliang8 May 2, 2017
2335275
removed lint, added TODO
aliang8 May 2, 2017
d91eb26
merging
aliang8 May 5, 2017
7fdbd7e
rebasing
aliang8 May 7, 2017
e8e86a1
merge
aliang8 May 9, 2017
7a3e271
merged
aliang8 May 10, 2017
fbd4938
merge
aliang8 May 14, 2017
3ea7468
merge
aliang8 May 15, 2017
96d2bb9
merged
aliang8 May 18, 2017
caa0ae0
addingPolynomials
aliang8 May 20, 2017
14a091f
fixed comments
aliang8 May 20, 2017
aa21a98
added more tests, one of them fails
aliang8 May 20, 2017
08b3ca1
remove . file
aliang8 May 20, 2017
03ffdf1
merge
aliang8 May 20, 2017
648f0f7
remove alphabetize
aliang8 May 20, 2017
632b525
fixed comments
aliang8 May 20, 2017
7403644
removed comment
aliang8 May 20, 2017
2cb2f76
starting to work on LCM
aliang8 May 20, 2017
fa1c35c
fractional polynomial and rearrange coeffs
aliang8 May 21, 2017
85f51d0
remove . file
aliang8 May 21, 2017
9f81447
fixed failing case
aliang8 May 21, 2017
9781f2d
fixed comments
aliang8 May 21, 2017
51885d6
back to master
aliang8 May 22, 2017
99deef2
Merge branch 'master' into master
aliang8 May 25, 2017
fd811ac
merge w/master
aliang8 Jun 5, 2017
a3715ff
merge
aliang8 Jun 5, 2017
9f29c4e
added common-denom
aliang8 Jun 8, 2017
28e3d34
merge
aliang8 Jun 12, 2017
e83c322
updating fractions function
aliang8 Jun 14, 2017
e7ff24f
fixed common denoms
aliang8 Jun 15, 2017
3c83192
remove comments
aliang8 Jun 15, 2017
4eac6c4
comment out test case
aliang8 Jun 15, 2017
41ceb20
added rewrite fractional polynomial
aliang8 Jun 15, 2017
a868faf
fixed case for number + fraction
aliang8 Jun 18, 2017
d55286f
rm dot file
aliang8 Jun 18, 2017
0a7e6aa
general case
aliang8 Jun 18, 2017
25dda64
refactored negatives logic
aliang8 Jun 18, 2017
5c9274a
made output more friendly
aliang8 Jun 18, 2017
7d8e286
few more edge cases
aliang8 Jun 18, 2017
e45f4a3
added a convert to fraction rule
aliang8 Jun 18, 2017
05e2c52
added decimal_to_fraction function
aliang8 Jun 18, 2017
410afe5
small fix
aliang8 Jun 18, 2017
da0efa6
an extra test case
aliang8 Jun 18, 2017
e7c80ac
change to if statements
aliang8 Jun 18, 2017
8164020
still fixing test case
aliang8 Jun 19, 2017
d31d216
working on simplify_terms
aliang8 Jun 20, 2017
783d092
almost done with simplify terms
aliang8 Jun 20, 2017
6396397
fix edge cases
aliang8 Jun 20, 2017
1893d87
fixing common denominator
aliang8 Jun 20, 2017
43c63ea
adding comments and fixed paren issue
aliang8 Jun 21, 2017
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
38 changes: 33 additions & 5 deletions lib/__test__/rule-list.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,21 +172,49 @@ describe('rules', () => {
['x^((x + 1) / -1)', 'x^(-(x + 1) / 1)'],
])

suite('add numerators', rules.COMBINE_NUMERATORS, [
suite('rewrite fractional polynomial', rules.REWRITE_FRACTIONAL_POLYNOMIAL, [
['2x/3', '2 / 3 x'],
['2x^2/3', '2 / 3 x^2'],
['2(x+1)^2/3', '2 / 3 (x + 1)^2'],
['2xyz/3', '2 / 3 xyz'],
])

suite('combine numerators', rules.COMBINE_NUMERATORS, [
['1/3 + 2/3', '(1 + 2) / 3'],
['1/x + 2/x + 3/x', '(1 + 2 + 3) / x'],
['2/3 - 1/3', '(2 - 1) / 3'],
['(1/3 + 2/3) / x', '(1 + 2) / 3 / x'],
])

suite('common denominators', rules.COMMON_DENOMINATOR, [
suite('convert to fraction', rules.CONVERT_TO_FRACTION, [
['2/3 + x', '2 / 3 + x / 1'],
['2/3 - x', '2 / 3 - x / 1'],
['2/3 - x - 2', '2 / 3 - x / 1 - 2 / 1'],
['2/(3 + x) + 2 + (x + 1)^2', '2 / (3 + x) + 2 / 1 + (x + 1)^2 / 1'],
['2.2 + 3/4', '11 / 5 + 3 / 4'],
['1.33 + 2.12 + 2/3', '133 / 100 + 53 / 25 + 2 / 3'],
['1.44 - 2 - 3/4', '36 / 25 - 2 / 1 - 3 / 4'],
['-2 + 3.33 + 2/3', '-2 / 1 + 333 / 100 + 2 / 3']
])

suite('common denominators', rules.COMMON_DENOMINATOR, [
['2/6 + 1/4', '(2 * 2) / (6 * 2) + (1 * 3) / (4 * 3)'],
['2/(3 + x) + 3/(2 + x)', '(2 * (2 + x)) / ((3 + x) * (2 + x)) + (3 * (3 + x)) / ((3 + x) * (2 + x))'],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There should be some tests with:

  • a mix of numeric denominators and variable denominators
  • repeated variable denominators
  • repeated numeric denominators
  • non-fractional terms in the sum

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

COMMON_DENOMINATORS won't match when the denominator is the same throughout regardless of whether they are variable or numeric. I'm not entirely sure how we want to handle non-fractional terms in the sum yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There are a couple of options:

  • don't include non-fractions when matching
  • include non-fractions, but convert them to fractions before computing the common denominator.

You should ask Gen for their input on this.

What happens if some of the fractions have the same denominator while others don't?

@aliang8 aliang8 Jun 18, 2017

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yup good points. I had a test case that was commented out addressing this issue. I will ask for Gen's input.

What happens if some of the fractions have the same denominator while others don't?

It works fine in this scenario. I've added a test case for this. The only time when this rule doesn't match is when all the terms are fractions and they all have the same denominator.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added a fix for it.

['2/5 + 1/(3x + 7)', '(2 * (3 x + 7)) / (5 * (3 x + 7)) + (1 * 5) / (5 * (3 x + 7))'],
['2/6 - 1/4', '(2 * 2) / (6 * 2) - (1 * 3) / (4 * 3)'],
['2/6 + 1/4 - 2/5', '(2 * 10) / (6 * 10) + (1 * 15) / (4 * 15) - (2 * 12) / (5 * 12)'],
['2/6 + 1/4 - 3/4', '(2 * 2) / (6 * 2) + (1 * 3) / (4 * 3) - (3 * 3) / (4 * 3)'],
// TODO: return the original expression if the denominators are already
// the same?
['2/4 - 1/4', '(2 * 1) / (4 * 1) - (1 * 1) / (4 * 1)'],
['(3 + x)/2 + 2/(3 + x) + 2/3', '((3 + x) * (3 + x) * 3) / (2 * (3 + x) * 3) + (2 * 2 * 3) / (2 * (3 + x) * 3) + (2 * 2 * (3 + x)) / (2 * (3 + x) * 3)'],
// Some terms have same denom
['2/3 + 2/3 + 1/4 + 1/4', '(2 * 4) / (3 * 4) + (2 * 4) / (3 * 4) + (1 * 3) / (4 * 3) + (1 * 3) / (4 * 3)'],
// Number surrounded by fraction
['3 + 2/3', '(3 * 3) / 3 + 2 / 3'],
['x + 2 + 3/4', '(x * 4) / 4 + (2 * 4) / 4 + 3 / 4'],
['(x + 1)^2 + 2/3', '((x + 1)^2 * 3) / 3 + 2 / 3'],
['2(x + 1)^2 + 2/3', '(2 (x + 1)^2 * 3) / 3 + 2 / 3'],
['2/3 - x - 2', '2 / 3 - (x * 3) / 3 - (2 * 3) / 3'],
['2/3 - x/3 - 2/4', '(2 * 4) / (3 * 4) - (x * 4) / (3 * 4) - (2 * 3) / (4 * 3)'],
['-2 + 3 - 2/4', '-(2 * 4) / 4 + (3 * 4) / 4 - 2 / 4'],
])

suite('multiply fractions', rules.MULTIPLY_FRACTIONS, [
Expand Down
176 changes: 170 additions & 6 deletions lib/rule-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const EVALUATE_POWER = defineRuleString(
// e.g. -(-3) -> 3
export const NEGATION = defineRuleString('--#a', '#a')

// e.g. x * 5 -> 5 x
export const REARRANGE_COEFF = defineRuleString('#b * #a', '#a #b', {a: query.isNumber, b: isPolynomialTerm})

// ARITHMETIC
Expand Down Expand Up @@ -133,7 +134,6 @@ export const RESOLVE_DOUBLE_MINUS = defineRuleString('#a - -#b', '#a + #b')
// e.g -3 * -2 -> 3 * 2
export const MULTIPLY_NEGATIVES = defineRuleString('-#a * -#b', '#a * #b')


// FRACTIONS

// e.g. (x + 1) / 2 -> x / 2 + 1 / 2
Expand All @@ -155,17 +155,181 @@ export const SIMPLIFY_FRACTION = defineRuleString(
// e.g. 2/-3 -> -2/3
export const SIMPLIFY_SIGNS = defineRuleString('#a / -#b', '-#a / #b')

// e.g. 2x/3 -> 2/3 x
export const REWRITE_FRACTIONAL_POLYNOMIAL = defineRuleString(
'#a #b / #c', '#a / #c #b', {a: query.isNumber, b: isPolynomialTerm, c: query.isNumber}
)

// ADDING FRACTIONS

// e.g. 2/5 + 2/5 -> (2 + 2)/5
export const COMBINE_NUMERATORS =
defineRuleString('#a_0 / #b + ...', '(#a_0 + ...) / #b')

export const COMMON_DENOMINATOR =
defineRuleString(
'#a_0 / #b_0 + ...',
'(#a_0 * #eval(lcm(#b_0, ...) / #b_0)) / (#b_0 * #eval(lcm(#b_0, ...) / #b_0)) + ...'
)

export const getNegatives = (arr) => arr.map(arg => query.isNeg(arg))

export const isFraction = (node) => query.isNeg(node)
? query.isDiv(node.args[0])
: query.isDiv(node)

export const getNumerator = (node) => query.isNeg(node)
? node.args[0].args[0]
: node.args[0]

export const getDenominator = (node) => query.isNeg(node)
? node.args[0].args[1]
: node.args[1]

export const isDecimal = (node) => query.isNumber(node) && query.getValue(node) % 1 != 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you add a TODO to move these helper functions to math-nodes?


export const decimal_to_fraction = (node) => {
// split up the decimal
const [int, dec] = node.value.toString().split('.')

// e.g. .2 -> 2/10
const fraction = build.div(build.number(parseInt(dec)), build.number(parseInt('1' + '0'.repeat(dec.length))))

// simplified the fraction if possible
const simplified = canApplyRule(SIMPLIFY_FRACTION, fraction)
? applyRule(SIMPLIFY_FRACTION, fraction)
: fraction

const newNumerator = parseInt(int) * query.getValue(simplified.args[1]) + query.getValue(simplified.args[0])
return build.div(build.number(newNumerator), simplified.args[1])
}

// e.g. 2 + 3/2 -> 2/1 + 3/2
// e.g. 1.2 + 3/2 -> 6/5 + 3/2
export const CONVERT_TO_FRACTION = defineRule(
(node) => {
if (query.isAdd(node)) {
const terms = node.args
// only some of the terms are fractions
const hasFraction = terms.some(term => isFraction(term)) && !terms.every(term => isFraction(term))

return hasFraction ? {node} : null
}
},

(node) => {
let terms = node.args

const negatives = getNegatives(terms)

// transform constant terms
terms = terms.map(term => isDecimal(term) && query.isNeg(term)
? decimal_to_fraction(term.args[0])
: isDecimal(term)
? decimal_to_fraction(term)
: isPolynomialTerm(term) && query.isNeg(term)
? build.div(term.args[0], build.number(1))
: isPolynomialTerm(term)
? build.div(term, build.number(1))
: query.isNeg(term)
? term.args[0]
: term)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is super confusing. Can you rewrite this use if-else statements?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah xD I realized. Sure.


const result = build.add(
...terms.map((term, i) => negatives[i]
? build.neg(term, {wasMinus: true})
: term))
return result
}
)

// Finds common denominator in any scenario

// e.g. 2/6 + 1/4 -> (2 * 2) / (6 * 2) + (1 * 3) / (4 * 3)
// e.g. 2/(3 + x) + 2/3 -> (2 * 3)/ (3 * (3 + x)) + 2 * (3 + x) / (3 * (3 + x))
// e.g. 3 + 2/3 -> (3 * 3) / 3 + 2 / 3
export const COMMON_DENOMINATOR = defineRule(
(node) => {
if (query.isAdd(node)) {
const terms = node.args

// checks if at least one of the terms is a fraction
const hasFraction = terms.some(term => isFraction(term))

let sameDenom
// rule cannot apply if all of the terms are fractions
// and they all have the same denominator

@k4b7 k4b7 Jun 18, 2017

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be worth moving this comment up to the top of the function so that people don't have to read the whole function to understand the restrictions.

if (terms.every(term => isFraction(term))) {
const denom = getDenominator(terms[0])

sameDenom = terms.every(term =>
print(getDenominator(term)) == print(denom))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We might want to encapsulate this into an equals function in utils.js.

}

return hasFraction && !sameDenom ? {node} : null
}
},

(node) => {
node = canApplyRule(CONVERT_TO_FRACTION, node)
? applyRule(CONVERT_TO_FRACTION, node)
: node

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We convert non-fraction terms to fractions here, but in the match function part of this rule we return false if any of the terms are non-fractions. Am I missing something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The first part is correct. The match function of COMMON_DENOMINATORS returns false when all the terms are fractions and they all have the same denoms or none of the terms are fractions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see, it has to have at least one fraction... I obviously need to get more sleep. :P


let terms = node.args
// an array storing the index where fraction is negative
const negatives = getNegatives(terms)

// get numerators and denominators of all fractions
let nums = terms.map(term => getNumerator(term))
let denoms = terms.map(term => getDenominator(term))
let newDenoms
let newNumerators

/*
If all denoms are numbers, the new denom is
the [denom * (LCM / denom)] and the new numerators
is [num * (LCM / denom)]

Else the new denom is the product of all the terms
and the new numerator is [num * all other denoms]
*/

if(denoms.every(denom => query.isNumber(denom))) {
denoms = denoms.map(denom => query.getValue(denom))
const LCM = lcm(...denoms)

// remove multiplication by 1
newDenoms = denoms.map(
denom => denom == 1
? build.number(LCM / denom)
: LCM / denom == 1
? build.number(denom)
: build.mul(build.number(denom),
build.number(LCM / denom)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These chained ternaries are really hard to understand. Can you rewrite this using if-else statements?

newNumerators = nums.map(
(num, i) => LCM / denoms[i] == 1
? num
: build.mul(num,
build.number(LCM / denoms[i])))
} else {
newDenoms = build.mul(...denoms)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This isn't ideal, if all of the denominators are x then we'll make this overly complicated.

@aliang8 aliang8 Jun 16, 2017

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When will all the denominators be x? I don't think COMMON_DENOMINATORS will match that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What happens for 2 / x + 3 / x + 4 / y? Can you add a tests case like this? I would expect the result to be 2y / xy + 3y / xy + 4x / xy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah, good catch. Will fix.

newNumerators = nums.map(
(num, i) => build.mul(num, ...denoms.filter(e => e != denoms[i])))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about situations where there are multiple factors in the denominator to begin with? I think there's an implicit assumption that each fraction as a single factor denominator. Can you add a test case for 2 / (x * x) + 3 / (x * y) + 4 / (y * y)?

@aliang8 aliang8 Jun 19, 2017

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see. In ChangeTypes, MULTIPLYING_POLYNOMIALScomes before COMMON_DENOMINATORS, so we won't see this exact case. We would see, however, 2/ x^2 + 3/ x^1y^1 + 4 / y^2 which we would want to simplify to (2 * y)/ x^2y^1 + (3 * x) / x^2y^1 + (4 * x) / x^2y^1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A similar test case that we should also consider is:
2 / ((x^2 + 1)^2 * (x + 1)^1) + 3 / ((x^2 + 1)^2 * x)

}

/*
The newDenoms variable is an array when all the denoms
are numbers because all the newDenoms are different.
*/

const result = Array.isArray(newDenoms)
? build.add(...newDenoms.map(
(den, i) => negatives[i]
? build.neg(build.div(newNumerators[i], den), {wasMinus: true})
: build.div(newNumerators[i], den)))
: build.add(...newNumerators.map(
(num, i) => negatives[i]
? build.neg(build.div(num, newDenoms), {wasMinus: true})
: build.div(num, newDenoms)))

return result
}
)

// Have a 'negatives' array which marks things as being negative or not for
// items in a variable length node. When we're populating that variable length
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/collect-like-terms.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const isPolynomialTerm = (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)) {
Expand Down