-
Notifications
You must be signed in to change notification settings - Fork 1
Fractions #50
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
base: master
Are you sure you want to change the base?
Fractions #50
Changes from 55 commits
04bb0ee
f37f48c
5745443
b462d8a
3ca4c7f
adcbc10
3cb89d4
75ab164
3beef3e
c35f087
20bc705
336c5c3
3dbb8cd
2335275
d91eb26
7fdbd7e
e8e86a1
7a3e271
fbd4938
3ea7468
96d2bb9
caa0ae0
14a091f
aa21a98
08b3ca1
03ffdf1
648f0f7
632b525
7403644
2cb2f76
fa1c35c
85f51d0
9f81447
9781f2d
51885d6
99deef2
fd811ac
a3715ff
9f29c4e
28e3d34
e83c322
e7ff24f
3c83192
4eac6c4
41ceb20
a868faf
d55286f
0a7e6aa
25dda64
5c9274a
7d8e286
e45f4a3
05e2c52
410afe5
da0efa6
e7c80ac
8164020
d31d216
783d092
6396397
1893d87
43c63ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a TODO to move these helper functions to |
||
|
|
||
| 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is super confusing. Can you rewrite this use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might want to encapsulate this into an |
||
| } | ||
|
|
||
| return hasFraction && !sameDenom ? {node} : null | ||
| } | ||
| }, | ||
|
|
||
| (node) => { | ||
| node = canApplyRule(CONVERT_TO_FRACTION, node) | ||
| ? applyRule(CONVERT_TO_FRACTION, node) | ||
| : node | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The first part is correct. The match function of
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| newNumerators = nums.map( | ||
| (num, i) => LCM / denoms[i] == 1 | ||
| ? num | ||
| : build.mul(num, | ||
| build.number(LCM / denoms[i]))) | ||
| } else { | ||
| newDenoms = build.mul(...denoms) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't ideal, if all of the denominators are
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When will all the denominators be
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]))) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. In
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A similar test case that we should also consider is: |
||
| } | ||
|
|
||
| /* | ||
| 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 | ||
|
|
||
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
You should ask Gen for their input on this.
What happens if some of the fractions have the same denominator while others don't?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
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.
There was a problem hiding this comment.
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.