Enhancement: Multiply denominators with terms#88
Conversation
|
Looking sweet! Thanks so much for this big change and also all the little changes you're making to make the codebase cleaner 🌟 I'm feeling a bit sick today, so I'll take a more thorough look early this week. I also hope to chat about this a bit with Ahmed, who wrote the original equations code. |
evykassirer
left a comment
There was a problem hiding this comment.
Thank you!!
I made a whole bunch of comments:
- a bunch are gratitude for helpful things you did 😄
- a bunch are also formatting/documentation related
- some are small changes for you to make
For the most part I think this approach is right. My one concern is around the additions to cancelling out- the is existing code is already is a bit more messy than average, so I'm happy to discuss what we can do a bit more once you've read over the comments.
You're the first contributor to make a change this big - thanks so much for your time, and for your patience in waiting for me to have the time to review it ;D
I'm also in general really excited for this change because we'll be able to solve a whole bunch more equations thanks to you!
| return node; | ||
| } | ||
| // Otherwise, it's a sum of terms. Look through the operands for a term | ||
| // If it's a sum of terms, look through the operands for a term |
| return new Node.PolynomialTerm(node).getCoeffNode(); | ||
| } catch (e) { | ||
| if (e.message.startsWith('denominator must be constant node')) { | ||
| // Not sure what this should return for a case such as 1/(2x) |
There was a problem hiding this comment.
probably just return null like below - unless you want to use part of the 1/(2x) term somehow
and even if the error isn't "denominator must be constant node..." you would also want to return null right?
we should probably better define what "symbol term" and "non symbol term" mean for things like 1/2x, depending on whatever's useful for solving equations -- and then yeah I think moving this stuff to an equation specific file would make more sense
| return true; | ||
| } | ||
| } else if (hasDenominatorSymbol(node, symbolName)) { | ||
| return true |
There was a problem hiding this comment.
missing semi colon! can you run the linter with npm lint to make sure you're not missing other stuff?
also, generally we put else on new line, so:
return true;
}
}
else if (hasDenominatorSymbol(node, symbolName)) {
(I just added this to #44)
|
|
||
| function hasDenominatorSymbol(node, symbolName) { | ||
| if (Node.Type.isOperator(node) && node.op === '/') { | ||
| return Symbols.getSymbolsInExpression(node).size > 0; |
There was a problem hiding this comment.
you should use symbolName here to make sure that the symbol in the expression is indeed the symbol we were looking for
| node, onlyImplicitMultiplication=false) { | ||
| try { | ||
| // will throw error if node isn't poly term | ||
| // |
| return node; | ||
| } | ||
|
|
||
| function parseNode(node, onlyImplicitMultiplication) { |
There was a problem hiding this comment.
can you add a docstring, now that it's its own function?
| function cancelLikeTerms(node) { | ||
| // Cancel out terms if the term being multiplied | ||
| // is the same as the denominator | ||
| if (Node.Type.isOperator(node) && node.op === '*') { |
There was a problem hiding this comment.
can you merge in master (now with #91) and change this to isOperator(node, '*')? thanks!
| // e.g. (2x^2 * 5) / 2x^2 => 5 / 1 | ||
| // Returns a Node.Status object | ||
| function cancelLikeTerms(node) { | ||
| // Cancel out terms if the term being multiplied |
There was a problem hiding this comment.
I think this comment isn't super clear about what 'the term being multiplied' is
can you add an example?
| if (Node.Type.isOperator(node) && node.op === '*') { | ||
| let newNode = clone(node); | ||
| let leftNode = newNode.args[0]; | ||
| let rightNode = newNode.args[1]; |
There was a problem hiding this comment.
what if the roles you gave left node and right node are reversed? e.g. the right node is a fraction or sum that cancels out with the left node?
also, how do you know there's only two things being multiplied by each other? there can be any number of terms in a multiplication
There was a problem hiding this comment.
I'm actually surprised you need this - I think that multiplyFractions will turn (2+x) * (3+x)/(2+x) into ((2+x)(3+x))/(2+x) which should then just cancel out with the existing code... but if you had to add this stuff, there's probably something missing here that needs to be added. Not sure if what you have fills the hole quite right, but let's talk about it more!
There was a problem hiding this comment.
The code on master doesn't cancel out terms for (2+x) * (3+x)/(2+x), which was what prompted these changes. Currently it would distribute out the (2+x). Without canceling, my changes caused an infinite loop by multiplying the denominator, distributing out, multiplying the denominator, etc.
Regarding the left node / right node positioning, I had made an assumption based on what would happen during equations steps, but you're right that it misses out on if an equation starts as (2+x) * (3+x)/(2+x). I'll work on resolving this in the next commit
From what I can tell that scenario wouldn't occur during equation solving though. Instead of multiplying by an inverted fraction, the steps would multiply by the denominator and then divide by the numerator
| // being multiplied | ||
| if (Node.Type.isOperator(leftNode) && leftNode.op === '+') { | ||
| let allMatchingDenominators = true; | ||
| for (let i = 0; i < leftNode.args.length; i++) { |
There was a problem hiding this comment.
you could use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every here, I think
e0274dd to
8eb739b
Compare
|
I'm actually starting to think that this enhancement is a bit premature. There's a lot of functionality that I seem to need out of cancelLikeTerms that should probably be done first before working on the equation solving. For multiplication across, these things need to be handled first I'm also not sure how the current approach would deal with factorization and canceling: I think I'll pull out some of the generic changes here out into a separate pull request, but I'm going to pause progress on this PR and maybe look into canceling terms separately when I have some time. I feel like that's going to be a considerable beast on its own |
|
I honestly think we can merge a lot of the stuff in this PR :) if in
(aside, For factoring, we haven't done any factoring stuff yet but it's on the horizon! There's a lot of cases where your code would be helpful without needing factoring, and I think it's quite reasonable to add this functionality now and wait for factoring later. tl;dr, I think we should:
what do you think? |
|
|
|
Sure, I'll try flipping those methods tomorrow and see where I get |
|
@kevinbarabash very true. We don't have anything that stores information like that yet. Do you think adding it to the |
|
@evykassirer a new |
|
hey @arbylee - did you get a chance to try it? how'd it go? :) |
|
Hey @evykassirer, I did try removing the cancelLikeTerms changes and flipping multiplyFractionsSearch and breakUpNumeratorSearch. It ended up breaking a few test cases, and I haven't had enough time to look through them (I moved this past weekend). I've updated the branch if you want to have a look |
|
thanks! I'll take a look and see what makes sense to fix here and what makes sense to fix in other issues this change is a nice goldmine of revealing bugs ;D |
evykassirer
left a comment
There was a problem hiding this comment.
ah, okay I see what you mean about not really being able to merge this until those other things are fixed... otherwise none of it works haha!
I opened issues for the bugs, for exactly what needs to be fixed. Feel free to take them on, or we can wait until someone else can do them and then come back to this.
I also added some changes you can make in the meantime.
Hope you're settling into your new place nicely!
| Symbols.getLastNonSymbolTerm = function(node, symbolName) { | ||
| if (isSymbolTerm(node, symbolName)) { | ||
| return new Node.PolynomialTerm(node).getCoeffNode(); | ||
| try { |
There was a problem hiding this comment.
actually this might be clearer to do a check for if it's a polynomial term instead of the throw catch - that's commonly used with the PolynomialTerm constructor actually
| }; | ||
|
|
||
| // Iterates through a node and returns the last term that has a | ||
| // symbolName in its denominator |
There was a problem hiding this comment.
it actually returns the denominator, not the term, right?
| return true; | ||
| } | ||
| } | ||
| else if (hasDenominatorSymbol(node, symbolName)) { |
There was a problem hiding this comment.
why did you add this to isSymbolTerm?
There was a problem hiding this comment.
It's been a while now so I'm trying to remember. I believe I had changed isSymbolTerm to return true if there was a symbol in the denominator, and there's logic that would then call getLastSymbolTerm. And then getLastSymbolTerm relies on this isSymbolTerm function
There was a problem hiding this comment.
Jumping back to this because it should probably be discussed (and have code/comments updated appropriately). Prior to this change, isSymbolTerm specifically was checking only for polynomial symbol terms. So a term like 1/x would not be considered a symbol term because it's not a polynomial term. This change makes it so that terms with a symbol in the denominator are considered symbol terms as well.
In turn, this affects getLastNonSymbolTerm and getLastSymbolTerm which both use isSymbolTerm. I'm not sure if this change is going against the intentions of the Symbols object. If it is, then I'll need to find some other way to get a symbol term in a denominator
There was a problem hiding this comment.
This makes sense to me! Agreed re: updating the comments to explain this
|
|
||
| tests.forEach(t => runTest(Symbols.getLastDenominatorSymbolTerm, t[0], t[1])); | ||
| }); | ||
|
|
There was a problem hiding this comment.
extra line here (you only need one new line at the bottom of a file)
| tests.forEach(t => runTest(Symbols.getLastSymbolTerm, t[0], t[1])); | ||
| }); | ||
|
|
||
| describe('getLastDenominatorSymbolTerm', function() { |
| ['( u )/( 0.3) = 4u + 6.28', 'u = -9.42'], | ||
| ['- q - 4.36= ( 2.2q )/( 1.8)', 'q = -1.962'], | ||
| //['2/x = 1', 'x = 2'], TODO: fixme | ||
| //['2/(4x) = 1', 'x = 1/2'], TODO: bombs out |
There was a problem hiding this comment.
this is also related because (2 / (4x)) * 4x = 4x doesn't get cancelled out
I can see that we'll have to fix that first before we can actually test any of this (or have this functionality work at all)
| ['- q - 4.36= ( 2.2q )/( 1.8)', 'q = -1.962'], | ||
| //['2/x = 1', 'x = 2'], TODO: fixme | ||
| //['2/(4x) = 1', 'x = 1/2'], TODO: bombs out | ||
| //['2/(8 - 4x) = 1/2', 'x = 1'], TODO: fixme |
There was a problem hiding this comment.
same thing... :(
(2 / (8 - 4x)) * (8 - 4x) = (1/2) * (8 - 4x)
4 - 2x = (2 / (8 - 4x)) * 8 + (2 / (8 - 4x)) * -4x
(4 - 2x) - 4 = ((2 / (8 - 4x)) * 8 + (2 / (8 - 4x)) * -4x) - 4
(-2x) / -2 = (((2 / (8 - 4x)) * 8 + (2 / (8 - 4x)) * -4x) - 4) / -2
....
| //['2/x = 1', 'x = 2'], TODO: fixme | ||
| //['2/(4x) = 1', 'x = 1/2'], TODO: bombs out | ||
| //['2/(8 - 4x) = 1/2', 'x = 1'], TODO: fixme | ||
| //['2/(1 + 1 + 4x) = 1/3', 'x = 1'], TODO: fixme |
There was a problem hiding this comment.
and again
(2 / (4x + 2)) * (4x + 2) = (1/3) * (4x + 2)
4x / 3 + 2/3 = (2 / (4x + 2)) * 4x + (2 / (4x + 2)) * 2
(4x / 3 + 2/3) * 3 = ((2 / (4x + 2)) * 4x + (2 / (4x + 2)) * 2) * 3
| //['2/(4x) = 1', 'x = 1/2'], TODO: bombs out | ||
| //['2/(8 - 4x) = 1/2', 'x = 1'], TODO: fixme | ||
| //['2/(1 + 1 + 4x) = 1/3', 'x = 1'], TODO: fixme | ||
| //['(3 + x) / (x^2 + 3) = 1', '-x^2 + x = 0'], TODO: bombs out |
There was a problem hiding this comment.
this problem is different! it's a bad distribution
(3 / (x^2 + 3) + x / (x^2 + 3)) * (x^2 + 3) turns into
(3 / (x^2 + 3) * x^2 + 9 / (x^2 + 3) + x / (x^2 + 3) * x^2 + 3x / (x^2 + 3))
I think distribution should check to see if one of the multiplying things is also a list of fractions, and if only one of them is, then multiply the other one into its numerators. I'll make this a separate issue #138
There was a problem hiding this comment.
eventually it'd be nice to not split the numerator like that, but I'm not sure when we'd want and when we wouldn't
| //['2/(8 - 4x) = 1/2', 'x = 1'], TODO: fixme | ||
| //['2/(1 + 1 + 4x) = 1/3', 'x = 1'], TODO: fixme | ||
| //['(3 + x) / (x^2 + 3) = 1', '-x^2 + x = 0'], TODO: bombs out | ||
| //['6/x + 8/2x = 10', '(-10x) + 4x^2 = -6'], TODO: fixme |
There was a problem hiding this comment.
this is an interesting case where x * 6/x isn't multiplied to get (x * 6) / x, which would cancel out
one solution is just to have better cancelling code that doesn't need it to be a fraction.
another (easier for now) solution would be to add a case for a symbol times a symbol in the denominator (i.e. we don't want 9/2 * x to multiply to 9x / 2, but we would want 9/x * x to multiply to 9)
... anyways I'll open an issue #139 for this too
(btw 8/2x is parsed as 8/2 x ie 4x, not as 8 / 2x)
|
also @arbylee do you want some mathsteps stickers as a thank you for your contributions? 😸 if you're interested, email mathsteps@socratic.org with your mailing address and I can send them over! |
86505eb to
b8a625d
Compare
|
@evykassirer Rebased and pushed with just the comment change for now. I'll have a look at some of the issues raised to see where I can help. And yes, I'd love some mathsteps stickers 😄 |
b8a625d to
b66688c
Compare
|
Hi @arbylee, I'd love to get this into master, what can I do to support you? |
|
I think a 'high priority' tag would do the trick! I'd also use it for things that are blocked but we want to do soon |
|
So all blocking issues are taken care of now. @evykassirer how should we proceed? I could familiarize myself with this pull and try to fix the conflicts? Not sure if @arbylee is around to help? |
|
If this is urgent for Socratic you could take it over right away, but if you have stuff to work on and can give @arbylee until next week to see this and finish if they want, I think that'd be ideal. Your call though, I know this was a pretty big thing |
|
I've rebased and I think addressed the remaining change requests on this. For the test case @ldworkin mentioned, I've only set it to |
|
Awesome, this looks great @arbylee. I'll make a separate note/issue to address that test case. @evykassirer thoughts on merging?! |
|
kevin mentioned earlier
(see above for more context) we should open an issue for this thing so we can address it once we make the NodeStatus more complicated and able to support constraints |
evykassirer
left a comment
There was a problem hiding this comment.
ahhhh amazing thanks!!! just commented on a style nit, and one case I think you're missing, after that looks ready to go! So excited for this change :D
| Symbols.getLastNonSymbolTerm = function(node, symbolName) { | ||
| if (isSymbolTerm(node, symbolName)) { | ||
| return new Node.PolynomialTerm(node).getCoeffNode(); | ||
| if (Node.PolynomialTerm.isPolynomialTerm(node)) { |
There was a problem hiding this comment.
this would probably be cleaner as if (isSymbolTerm(node, symbolName) && Node.PolynomialTerm.isPolynomialTerm(node))
There was a problem hiding this comment.
That would actually change the behavior of the function, but I've cleaned it up to be more obvious
| // symbolName in its denominator | ||
| // e.g. 1/(2x) with `symbolName=x` would return 2x | ||
| // e.g. 1/(x+2) with `symbolName=x` would return x+2 | ||
| // e.g. 1/(x+2) + (x+1)/(2x+3) with `symbolName=x` would return 2x+3 |
There was a problem hiding this comment.
I'm curious - does it return 2x+3 or does it wrap it in parens like (2x+3) ?
There was a problem hiding this comment.
It is wrapped in parens, updated
| // e.g. false for 5x | ||
| function hasDenominatorSymbol(node, symbolName) { | ||
| if (Node.Type.isOperator(node) && node.op === '/') { | ||
| const allSymbols = Symbols.getSymbolsInExpression(node); |
There was a problem hiding this comment.
I think this will return true for x/2 too, so should this line be calling Symbols.getSymbolsInExpression only on the denominator?
There was a problem hiding this comment.
You're right. I've corrected that now
| // pass for now | ||
| // Ensures that a symbol is not in the denominator by multiplying | ||
| // both sides by the denominator if there is a symbol present. | ||
| EquationOperations.removeSymbolFromDenominator = function(equation, symbolName) { |
| const leftNode = equation.leftNode; | ||
| const denominator = Symbols.getLastDenominatorWithSymbolTerm(leftNode, symbolName); | ||
| if (denominator) { | ||
| return performTermOperationOnEquation( |
| ['x/(x+3)', 'x / (x + 3)'], | ||
| ]; | ||
|
|
||
| tests.forEach(t => runTest(Symbols.getLastSymbolTerm, t[0], t[1])); |
There was a problem hiding this comment.
can you pass 'x' in too, and then also have some tests where there are multiple symbols and you are looking for one of them? e.g. '1/x + y', y
There was a problem hiding this comment.
Added some test cases with other variables
| ['1/x', 'x'], | ||
| ['1/(x+2)', '(x + 2)'], | ||
| ['1/(x+2) + 3x', '(x + 2)'], | ||
| ['1/(x+2) + 3x/(1+x)', '(1 + x)'], |
There was a problem hiding this comment.
can you add 2 + 2/x + x/2 to test that case I raised above?
There was a problem hiding this comment.
Done. There was even an existing issue with how symbols were being parsed that was made obvious with this particular test case. I've added a commit to recursively parse the nodes for symbols. It was previously only comparing 2 + 2/x and x/2 as the only two nodes, instead of checking 2 and 2/x individually
There was a problem hiding this comment.
yay! glad we caught that :D
| ['2/(8 - 4x) = 1/2', 'x = 1'], | ||
| ['2/(1 + 1 + 4x) = 1/3', 'x = 1'], | ||
| ['(3 + x) / (x^2 + 3) = 1', 'x = [0, 1]'], | ||
| ['6/x + 8/2x = 10', NO_STEPS], |
There was a problem hiding this comment.
can you add a TODO for this just so it's clear that this isn't what we want?
There was a problem hiding this comment.
I realized that the test I wanted from this was actually missing parens in 8/(2x). I've added that and adjusted the test result.
I didn't leave the previous test case without parens because it started to look similar to the test cases in the other TODO related to complex numbers. I can add it back with a comment if you think it's a separate scenario
There was a problem hiding this comment.
@ldworkin how do you want to track this? You said "I'll make a separate note/issue to address that test case."
is it in your Asana board? or can we open an issue here? should we just put the TODO in the code? I'm fine as long as it's being noted somewhere :) so we remember
bc08502 to
845f5b1
Compare
|
So excited for this, thanks for all your work, @arbylee !! |
|
Dammit, this change a21240d broke something! Investigating, will try to fix. |
|
Ok, I ended up reverting that change. I need to revisit the logic there. So this should be good -- not sure why it says there are still conflicts/failing checks, I think if we merge the latest master into it, it will be all good. |
This reverts commit 2a35214.
These functions don't necessarily return just polynomial terms anymore since isSymbol now returns true if there is a term in the denominator such as 1/x, which is not a polynomial term
The parsed nodes were previously comparing something like: 2 + 2/x + x/2 as 2 nodes. One node of `2 + 2/x` being added to `x/2` We needed to drill down to check `2` and `2/x` and `x/2` as individual nodes
42a0604 to
1708900
Compare
| // Look through the operands for a | ||
| // denominator term with `symbolName` | ||
| else if (Node.Type.isOperator(node) && node.op === '+') { | ||
| else if (Node.Type.isOperator(node, '+')) { |
evykassirer
left a comment
There was a problem hiding this comment.
🎉 so excited about this, thanks @arbylee

Attempt to resolve #51 and implement removeSymbolFromDenominator.
This change also needed to enhance the cancelLikeTerms logic. Cancelling needed to account for denominators matching what was being multiplied. Otherwise if they didn't cancel out, it would continue trying to multiply the denominator across until the equation reaches the length limit. Let me know any feedback