-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Negated fraction multiplied implicitly not stringified correctly #1431
Comments
The parser parses a unary minus as an operator, it does not negate when followed by a number. The confusion here is probably that you're using implicit multiplication, which has certain heuristic rules and is always tricky to use! In your code example you have a typo in It may be an interesting improvement in the parser to parse a unary minus followed by a number in a constant with the negated value. Or at least, have |
It appears as if it was some change that I made to the simplify ruleset that caused the issue with the script that I presented. I should have done more testing before I reported it as an issue. Although, I still find it odd how the unary minus at one point changes the precedence of the implicit multiplication further along int the expression. |
Ok!
That should not be the case, and so far I haven't been able to reproduce such behavior. If you have an example please let me know. |
math.parse("1 / 2 a").toString({parenthesis: "all"}) // => "(1 / 2) a"
math.parse("- 1 / 2 a").toString({parenthesis: "all"}) // => "(-1) / (2 a)" In the first example, the a is parsed as multiplied by 1/2, while in the second example, it is part of the divisor. RunKit example at https://runkit.com/embed/4zht1kqm9k1i |
Ah, yes. That is basically intended behavior, as in, there are a number of rules to determine when to give implicit multiplication higher precedence then division, and this is an example where it doesn't look logical. There are many more examples of this, but the basic culprit is that using implicit multiplication is tricky because it's very ambiguous. math.parse("1 / 2 a").toString({parenthesis: "all"}) // => "(1 / 2) a"
math.parse("- 1 / 2 a").toString({parenthesis: "all"}) // => "(-1) / (2 a)"
math.parse("5 - 1 / 2 a").toString({parenthesis: "all"}) // => well, can you guess?... See #792 for more details |
I finally found a way to share my problem with this. The mathjs object represented by the following JSON, when converted to a string with parenthesis set to auto and then reparsed, comes up with a different order of operations. {"mathjs":"OperatorNode","op":"*","fn":"multiply","args":[{"mathjs":"OperatorNode","op":"/","fn":"divide","args":[{"mathjs":"OperatorNode","op":"-","fn":"unaryMinus","args":[{"mathjs":"ConstantNode","value":1}],"implicit":false},{"mathjs":"ConstantNode","value":2}],"implicit":false},{"mathjs":"SymbolNode","name":"a"}],"implicit":true} Reprsented object with parenthesis set to all: In case I didn't explain my issue well enough, here's another RunKit example: https://runkit.com/embed/4xsmd0abvhox |
ahh, thanks for sharing! That helps a lot. here some extra output of your example so we can understand what happens: start = JSON.parse('{"mathjs":"OperatorNode","op":"*","fn":"multiply","args":[{"mathjs":"OperatorNode","op":"/","fn":"divide","args":[{"mathjs":"OperatorNode","op":"-","fn":"unaryMinus","args":[{"mathjs":"ConstantNode","value":1}],"implicit":false},{"mathjs":"ConstantNode","value":2}],"implicit":false},{"mathjs":"SymbolNode","name":"a"}],"implicit":true}', math.json.reviver)
console.log(start.toString({parenthesis: "all"})) // ((-1) / 2) a
console.log(start.toString({parenthesis: "auto"})) // -1 / 2 a
console.log(math.parse(start.toString({parenthesis: "auto"}))
.toString({parenthesis: "all"})) // (-1) / (2 a) What happens here is that when stringifying the implicit multiplication, it should put parenthesis around it's left and right hand side if that turns out to be an OperatorNode. This cannot happen when parsing an expression I think, since you have to use parenthesis in order to create such a node tree. It can happen though when you transform a node tree so it would be a good idea to improve OperatorNode such that it adds parenthesis when needed. I think the code for that is already there, but probably does not reckon with implicit multiplication and concludes that parenthesis are not needed because division and multiplication have the same precedence. Anyone interested in fixing this? |
This is because parseRule2() in parse.js only deals with [number] / [number] [symbol] like 1/2x. I don't understand why 2 x / 5 y = (2 x) / (5 y) in #792. I thought AsciiMath should be parsed as http://asciimath.org/. |
Yes you're right @shenzhuxi
I'm not sure what you mean, we're not talking about AsciiMath here but about the syntax of mathjs. |
if "1 / 2 a" = "(1 / 2) * a", it's consistent for "x / 2 a" = "(x / 2) * a" and "2 x / 5 y " = "(2 * x / 5) * y" but not for "2 x / 5 y = (2 x) / (5 y)". If "2 x / 5 y = (2 x) / (5 y)", "1 / 2 a" should be "(1) / (2 * a)". The syntax of mathjs doesn't have to be the same as AsciiMath. At least AsciiMath syntax is consistent at this point. |
Are you sure it's about underscore? I guess you mean #1502. |
I can confirm this issue still exists exactly as described in mathjs 10.1.0. I would be happy to (try to) produce a PR. Before I get started on that, I just wanted to confirm that the desired resolution is to leave the parsing of |
@gwhitney thanks for picking this up. It's quite some time ago 😅 but indeed like I explained in #1431 (comment), it is about reckoning implicit multiplication correctly in |
OK, I have looked into this a little bit. I have two questions: (2) There is already one sort of special case in
Do you have a specific example of an expression (Node) that this is intended to handle? I definitely feel like there should be at least one specific test for this special case. Thanks! |
(1) there are unit tests for toString() for any of the (2) I see this code is introduce in the following commit: b285739. It has clear corresponding unit tests, for example "should stringify implicit multiplications between ConstantNodes with parentheses". Does that have the examples that you're looking for? |
If the first operand of an implicit multiplication is a fraction in which either the numerator or denominator has a unary operator, that operand must be parenthesized, because of the rules for interpreting implicit multiplication in the parser. This commit checks for that condition and inserts the parentheses. Resolves josdejong#1431.
Thanks so much for the guidance; as you can see, I was able to work up a PR. I did also want to mention the relationship of this issue/PR #2403 to issue #2370, which suggested that the parsing of Note I am completely neutral about which way |
Thanks @gwhitney for clearly pointing out that if we decide to change the behavior of parsing the unary minus (and unary plus) discussed in #2370, this will resolve this issue and make #2403 redundant. I hadn't realized that 😅 . If I must decide, I prefer changing the behavior of |
Although I just submitted PR #2460 implementing the revised rule for precedence of division over implicit multiplication as described in #2370, and in fact the exact case described here does now work correctly (and I added a test for it in #2460), I did not claim that PR resolves this issue because there are still other inconsistent cases. For example, if there is a negation in a denominator of an implicitly multiplied fraction, stringifying and parsing the result produces a non-equivalent expression. I.e., the following assertion fails:
So this issue should remain open, and a new PR similar but not identical to #2403 is needed to correct the parenthesization in cases of .toString() of an OperatorNode like the above example. |
Thanks @gwhitney , your solution looks very neat. Good point that that the regular |
…rsing Also greatly extends the tests on OperatorNode.toString() and .toTex(), and ensures that all tests are performed on both. (toHTML() is still a testing stepchild.) Also fixes other small bugs in .toString() and .toTex() revealed by the new tests. Resolves josdejong#1431.
…rsing Also greatly extends the tests on OperatorNode.toString() and .toTex(), and ensures that all tests are performed on both. (toHTML() is still a testing stepchild.) Also fixes other small bugs in .toString() and .toTex() revealed by the new tests. Resolves #1431.
…rsing Also greatly extends the tests on OperatorNode.toString() and .toTex(), and ensures that all tests are performed on both. (toHTML() is still a testing stepchild.) Also fixes other small bugs in .toString() and .toTex() revealed by the new tests. Resolves #1431.
…rsing Also greatly extends the tests on OperatorNode.toString() and .toTex(), and ensures that all tests are performed on both. (toHTML() is still a testing stepchild.) Also fixes other small bugs in .toString() and .toTex() revealed by the new tests. Resolves #1431.
…rsing Also greatly extends the tests on OperatorNode.toString() and .toTex(), and ensures that all tests are performed on both. (toHTML() is still a testing stepchild.) Also fixes other small bugs in .toString() and .toTex() revealed by the new tests. Resolves #1431.
…rsing Also greatly extends the tests on OperatorNode.toString() and .toTex(), and ensures that all tests are performed on both. (toHTML() is still a testing stepchild.) Also fixes other small bugs in .toString() and .toTex() revealed by the new tests. Resolves #1431.
…rsing Also greatly extends the tests on OperatorNode.toString() and .toTex(), and ensures that all tests are performed on both. (toHTML() is still a testing stepchild.) Also fixes other small bugs in .toString() and .toTex() revealed by the new tests. Resolves #1431.
…rsing Also greatly extends the tests on OperatorNode.toString() and .toTex(), and ensures that all tests are performed on both. (toHTML() is still a testing stepchild.) Also fixes other small bugs in .toString() and .toTex() revealed by the new tests. Resolves #1431.
When using math.parse to parse an expression,
"1 / 2 a"
turns into(1 / 2) * a
, as expected, but"-1 / 2 a"
turns into((-1) / (2 * a))
, not((-1) / 2) * a
or( -(1 / 2)) * a
as expected. This may not seem like too much of a problem, but it does mess up when an equation is simplified correctly, stringified with parenthesis set to auto, and then reparsed, such as in the below.For some reason, math.parse will return the expression with the parentheses inserted, while math.simplify won't. So either math.simplify needs better parenthesis insertion, or math.parse needs to parse it's equations slightly differently.
The text was updated successfully, but these errors were encountered: