From aa7ca9178ffeb8ca8408f3db558da12aa860e1db Mon Sep 17 00:00:00 2001 From: Max Bruckner Date: Thu, 7 May 2015 01:38:46 +0200 Subject: [PATCH] Implement 'all' parenthesis option --- lib/expression/node/AssignmentNode.js | 20 ++++--- lib/expression/node/ConditionalNode.js | 9 ++- lib/expression/node/FunctionAssignmentNode.js | 22 ++++--- lib/expression/node/IndexNode.js | 31 +++++++++- lib/expression/node/OperatorNode.js | 26 +++++--- lib/expression/node/RangeNode.js | 59 +++++++++++++++---- lib/expression/node/UpdateNode.js | 12 +++- lib/util/latex.js | 1 + test/expression/node/AssignmentNode.test.js | 8 +++ test/expression/node/ConditionalNode.test.js | 6 ++ .../node/FunctionAssignmentNode.test.js | 8 +++ test/expression/node/IndexNode.test.js | 4 +- test/expression/node/OperatorNode.test.js | 13 ++++ test/expression/node/RangeNode.test.js | 7 +++ test/expression/node/UpdateNode.test.js | 9 ++- 15 files changed, 193 insertions(+), 42 deletions(-) diff --git a/lib/expression/node/AssignmentNode.js b/lib/expression/node/AssignmentNode.js index c6720dad9..88e1f434d 100644 --- a/lib/expression/node/AssignmentNode.js +++ b/lib/expression/node/AssignmentNode.js @@ -75,15 +75,24 @@ function factory (type, config, load, typed) { return new AssignmentNode(this.name, this.expr); }; + /* + * Is parenthesis needed? + * @private + */ + function needParenthesis(node) { + var precedence = operators.getPrecedence(node, config); + var exprPrecedence = operators.getPrecedence(node.expr, config); + return (config.parenthesis === 'all') + || ((exprPrecedence !== null) && (exprPrecedence <= precedence)); + } + /** * Get string representation * @return {String} */ AssignmentNode.prototype._toString = function() { - var precedence = operators.getPrecedence(this, config); - var exprPrecedence = operators.getPrecedence(this.expr, config); var expr = this.expr.toString(); - if ((exprPrecedence !== null) && (exprPrecedence <= precedence)) { + if (needParenthesis(this)) { expr = '(' + expr + ')'; } return this.name + ' = ' + expr; @@ -95,11 +104,8 @@ function factory (type, config, load, typed) { * @return {String} */ AssignmentNode.prototype._toTex = function(callbacks) { - var precedence = operators.getPrecedence(this, config); - var exprPrecedence = operators.getPrecedence(this.expr, config); - var expr = this.expr.toTex(callbacks); - if ((exprPrecedence !== null) && (exprPrecedence <= precedence)) { + if (needParenthesis(this)) { expr = '\\left(' + expr + '\\right)'; } diff --git a/lib/expression/node/ConditionalNode.js b/lib/expression/node/ConditionalNode.js index ae9fe5903..82664fe61 100644 --- a/lib/expression/node/ConditionalNode.js +++ b/lib/expression/node/ConditionalNode.js @@ -133,21 +133,24 @@ function factory (type, config, load, typed) { //purely based on aesthetics and readability var condition = this.condition.toString(); var conditionPrecedence = operators.getPrecedence(this.condition, config); - if ((this.condition.type === 'OperatorNode') + if ((config.parenthesis === 'all') + || (this.condition.type === 'OperatorNode') || ((conditionPrecedence !== null) && (conditionPrecedence <= precedence))) { condition = '(' + condition + ')'; } var trueExpr = this.trueExpr.toString(); var truePrecedence = operators.getPrecedence(this.trueExpr, config); - if ((this.trueExpr.type === 'OperatorNode') + if ((config.parenthesis === 'all') + || (this.trueExpr.type === 'OperatorNode') || ((truePrecedence !== null) && (truePrecedence <= precedence))) { trueExpr = '(' + trueExpr + ')'; } var falseExpr = this.falseExpr.toString(); var falsePrecedence = operators.getPrecedence(this.falseExpr, config); - if ((this.falseExpr.type === 'OperatorNode') + if ((config.parenthesis === 'all') + || (this.falseExpr.type === 'OperatorNode') || ((falsePrecedence !== null) && (falsePrecedence <= precedence))) { falseExpr = '(' + falseExpr + ')'; } diff --git a/lib/expression/node/FunctionAssignmentNode.js b/lib/expression/node/FunctionAssignmentNode.js index 3785d713d..c4848dc8a 100644 --- a/lib/expression/node/FunctionAssignmentNode.js +++ b/lib/expression/node/FunctionAssignmentNode.js @@ -99,16 +99,25 @@ function factory (type, config, load, typed) { return new FunctionAssignmentNode(this.name, this.params.slice(0), this.expr); }; + /** + * Is parenthesis needed? + * @private + */ + function needParenthesis(node) { + var precedence = operators.getPrecedence(node, config); + var exprPrecedence = operators.getPrecedence(node.expr, config); + + return (config.parenthesis === 'all') + || ((exprPrecedence !== null) && (exprPrecedence <= precedence)); + } + /** * get string representation * @return {String} str */ FunctionAssignmentNode.prototype._toString = function () { - var precedence = operators.getPrecedence(this, config); - var exprPrecedence = operators.getPrecedence(this.expr, config); - var expr = this.expr.toString(); - if ((exprPrecedence !== null) && (exprPrecedence <= precedence)) { + if (needParenthesis(this)) { expr = '(' + expr + ')'; } return 'function ' + this.name + @@ -121,11 +130,8 @@ function factory (type, config, load, typed) { * @return {String} str */ FunctionAssignmentNode.prototype._toTex = function (callbacks) { - var precedence = operators.getPrecedence(this, config); - var exprPrecedence = operators.getPrecedence(this.expr, config); - var expr = this.expr.toTex(callbacks); - if ((exprPrecedence !== null) && (exprPrecedence <= precedence)) { + if (needParenthesis(this)) { expr = '\\left(' + expr + '\\right)'; } diff --git a/lib/expression/node/IndexNode.js b/lib/expression/node/IndexNode.js index 3fc38f7cb..1329dc0ae 100644 --- a/lib/expression/node/IndexNode.js +++ b/lib/expression/node/IndexNode.js @@ -203,13 +203,34 @@ function factory (type, config, load, typed) { return new IndexNode(this.object, this.ranges.slice(0)); }; + /** + * Is parenthesis needed? + * @private + */ + function needParenthesis(node) { + switch (node.object.type) { + case 'ArrayNode': + case 'ConstantNode': //TODO don't know if this one makes sense + case 'SymbolNode': + case 'ParenthesisNode': + //those nodes don't need parentheses within an index node + return false; + default: + return true; + } + } + /** * Get string representation * @return {String} str */ IndexNode.prototype._toString = function () { + var object = this.object.toString(); + if (needParenthesis(this)) { + object = '(' + object + '('; + } // format the parameters like "[1, 0:5]" - return this.object.toString() + '[' + this.ranges.join(', ') + ']'; + return object + '[' + this.ranges.join(', ') + ']'; }; /** @@ -218,10 +239,16 @@ function factory (type, config, load, typed) { * @return {String} str */ IndexNode.prototype._toTex = function (callbacks) { + var object = this.object.toTex(callbacks); + if (needParenthesis(this)) { + object = '\\left(' + object + '\\right)'; + } + var ranges = this.ranges.map(function (range) { return range.toTex(callbacks); }); - return this.object.toTex(callbacks) + '_{\\left[' + ranges.join(',') + '\\right]}'; + + return object + '_{' + ranges.join(',') + '}'; }; return IndexNode; diff --git a/lib/expression/node/OperatorNode.js b/lib/expression/node/OperatorNode.js index f52c0de90..19155b009 100644 --- a/lib/expression/node/OperatorNode.js +++ b/lib/expression/node/OperatorNode.js @@ -114,7 +114,26 @@ function factory (type, config, load, typed, math) { var precedence = operators.getPrecedence(root, config); var associativity = operators.getAssociativity(root, config); + if ((config.parenthesis === 'all') || (args.length > 2)) { + var parens = []; + args.forEach(function (arg) { + switch (arg.getContent().type) { //Nodes that don't need extra parentheses + case 'ArrayNode': + case 'ConstantNode': + case 'SymbolNode': + case 'ParenthesisNode': + parens.push(false); + break; + default: + parens.push(true); + } + }); + return parens; + } + switch (args.length) { + case 0: + return []; case 1: //unary operators //precedence of the operand var operandPrecedence = operators.getPrecedence(args[0], config); @@ -244,13 +263,6 @@ function factory (type, config, load, typed, math) { } return [lhsParens, rhsParens]; - default: - //behavior is undefined, fall back to putting everything in parens - var parens = []; - args.forEach(function () { - parens.push(true); - }); - return parens; } } diff --git a/lib/expression/node/RangeNode.js b/lib/expression/node/RangeNode.js index 99733cf26..fac4b2fcf 100644 --- a/lib/expression/node/RangeNode.js +++ b/lib/expression/node/RangeNode.js @@ -85,35 +85,59 @@ function factory (type, config, load, typed) { return new RangeNode(this.start, this.end, this.step && this.step); }; + /** + * Calculate the necessary parentheses + * @param {Node} node + * @return {Object} parentheses + * @private + */ + function calculateNecessaryParentheses(node) { + var precedence = operators.getPrecedence(node, config); + var parens = {}; + + var startPrecedence = operators.getPrecedence(node.start, config); + parens.start = ((startPrecedence !== null) && (startPrecedence <= precedence)) + || (config.parenthesis === 'all'); + + if (node.step) { + var stepPrecedence = operators.getPrecedence(node.step, config); + parens.step = ((stepPrecedence !== null) && (stepPrecedence <= precedence)) + || (config.parenthesis === 'all'); + } + + var endPrecedence = operators.getPrecedence(node.end, config); + parens.end = ((endPrecedence !== null) && (endPrecedence <= precedence)) + || (config.parenthesis === 'all'); + + return parens; + } + /** * Get string representation * @return {String} str */ RangeNode.prototype._toString = function () { - var precedence = operators.getPrecedence(this, config); + var parens = calculateNecessaryParentheses(this); //format string as start:step:stop var str; var start = this.start.toString(); - var startPrecedence = operators.getPrecedence(this.start, config); - if ((startPrecedence !== null) && (startPrecedence <= precedence)) { + if (parens.start) { start = '(' + start + ')'; } str = start; if (this.step) { var step = this.step.toString(); - var stepPrecedence = operators.getPrecedence(this.step, config); - if ((stepPrecedence !== null) && (stepPrecedence <= precedence)) { + if (parens.step) { step = '(' + step + ')'; } str += ':' + step; } var end = this.end.toString(); - var endPrecedence = operators.getPrecedence(this.end, config); - if ((endPrecedence !== null) && (endPrecedence <= precedence)) { + if (parens.end) { end = '(' + end + ')'; } str += ':' + end; @@ -127,11 +151,26 @@ function factory (type, config, load, typed) { * @return {String} str */ RangeNode.prototype._toTex = function (callbacks) { + var parens = calculateNecessaryParentheses(this); + var str = this.start.toTex(callbacks); - if (this.step) { - str += ':' + this.step.toTex(callbacks); + if (parens.start) { + str = '\\left(' + str + '\\right)'; } - str += ':' + this.end.toTex(callbacks); + + if (this.step) { + var step = this.step.toTex(callbacks); + if (parens.step) { + step = '\\left(' + step + '\\right)'; + } + str += ':' + step; + } + + var end = this.end.toTex(callbacks); + if (parens.end) { + end = '\\left(' + end + '\\right)'; + } + str += ':' + end; return str; }; diff --git a/lib/expression/node/UpdateNode.js b/lib/expression/node/UpdateNode.js index 4fa4b32df..21dc38ce8 100644 --- a/lib/expression/node/UpdateNode.js +++ b/lib/expression/node/UpdateNode.js @@ -87,7 +87,11 @@ function factory (type, config, load, typed) { * @return {String} */ UpdateNode.prototype._toString = function () { - return this.index.toString() + ' = ' + this.expr._toString(); + var expr = this.expr.toString(); + if (config.parenthesis === 'all') { + expr = '(' + expr + ')'; + } + return this.index.toString() + ' = ' + expr; }; /** @@ -96,7 +100,11 @@ function factory (type, config, load, typed) { * @return {String} */ UpdateNode.prototype._toTex = function (callbacks) { - return this.index.toTex(callbacks) + ':=' + this.expr.toTex(callbacks); + var expr = this.expr.toTex(); + if (config.parenthesis === 'all') { + expr = '\\left(' + expr + '\\right)'; + } + return this.index.toTex(callbacks) + ':=' + expr; }; return UpdateNode; diff --git a/lib/util/latex.js b/lib/util/latex.js index 0029ee4da..0c73fcf34 100644 --- a/lib/util/latex.js +++ b/lib/util/latex.js @@ -336,6 +336,7 @@ exports.toSymbol = function (name, isUnit) { }; //returns the latex output for a given function +//TODO: this doesn't yet use the different parenthesis options exports.toFunction = function (node, callbacks, name) { var latexConverter = functions[name]; var args = node.args.map(function (arg) { //get LaTeX of the arguments diff --git a/test/expression/node/AssignmentNode.test.js b/test/expression/node/AssignmentNode.test.js index 8df7a039b..57f674691 100644 --- a/test/expression/node/AssignmentNode.test.js +++ b/test/expression/node/AssignmentNode.test.js @@ -198,6 +198,14 @@ describe('AssignmentNode', function() { assert.strictEqual(e.expr, d.expr); }); + it ('should respect the \'all\' parenthesis option', function () { + var allMath = math.create({parenthesis: 'all'}); + + var expr = allMath.parse('a=1'); + assert.equal(expr.toString(), 'a = (1)'); + assert.equal(expr.toTex(), 'a:=\\left(1\\right)'); + }); + it ('should stringify a AssignmentNode', function () { var b = new ConstantNode(3); var n = new AssignmentNode('b', b); diff --git a/test/expression/node/ConditionalNode.test.js b/test/expression/node/ConditionalNode.test.js index b017d9efd..811c24cb2 100644 --- a/test/expression/node/ConditionalNode.test.js +++ b/test/expression/node/ConditionalNode.test.js @@ -253,6 +253,12 @@ describe('ConditionalNode', function() { assert.strictEqual(d.falseExpr, c.falseExpr); }); + it ('should respect the \'all\' parenthesis option', function () { + var allMath = math.create({parenthesis: 'all'}); + + assert.equal(allMath.parse('a?b:c').toString(), '(a) ? (b) : (c)'); + }); + it ('should stringify a ConditionalNode', function () { var n = new ConditionalNode(condition, a, b); diff --git a/test/expression/node/FunctionAssignmentNode.test.js b/test/expression/node/FunctionAssignmentNode.test.js index 82d04506e..2b2605a97 100644 --- a/test/expression/node/FunctionAssignmentNode.test.js +++ b/test/expression/node/FunctionAssignmentNode.test.js @@ -245,6 +245,14 @@ describe('FunctionAssignmentNode', function() { assert.strictEqual(e.expr, d.expr); }); + it ('should respect the \'all\' parenthesis option', function () { + var allMath = math.create({parenthesis: 'all'}); + + var expr = allMath.parse('f(x)=x+1'); + assert.equal(expr.toString(), 'function f(x) = (x + 1)'); + assert.equal(expr.toTex(), '\\mathrm{f}\\left(x\\right):=\\left( x+1\\right)'); + }); + it ('should stringify a FunctionAssignmentNode', function () { var a = new ConstantNode(2); var x = new SymbolNode('x'); diff --git a/test/expression/node/IndexNode.test.js b/test/expression/node/IndexNode.test.js index 9d2b70714..ec99fb215 100644 --- a/test/expression/node/IndexNode.test.js +++ b/test/expression/node/IndexNode.test.js @@ -285,10 +285,10 @@ describe('IndexNode', function() { ]; var n = new IndexNode(a, ranges); - assert.equal(n.toTex(), ' a_{\\left[2,1\\right]}'); + assert.equal(n.toTex(), ' a_{2,1}'); var n2 = new IndexNode(a, []); - assert.equal(n2.toTex(), ' a_{\\left[\\right]}') + assert.equal(n2.toTex(), ' a_{}') }); it ('should LaTeX an IndexNode with custom toTex', function () { diff --git a/test/expression/node/OperatorNode.test.js b/test/expression/node/OperatorNode.test.js index dc5d386d6..707890049 100644 --- a/test/expression/node/OperatorNode.test.js +++ b/test/expression/node/OperatorNode.test.js @@ -284,6 +284,19 @@ describe('OperatorNode', function() { }); }); + it ('should respect the \'all\' parenthesis option', function () { + var allMath = math.create({parenthesis: 'all'}); + + assert.equal(allMath.parse('1+1+1').toString(), '(1 + 1) + 1' ); + assert.equal(allMath.parse('1+1+1').toTex(), '\\left(1+1\\right)+1' ); + }); + + it ('should correctly LaTeX fractions in \'all\' parenthesis mode', function () { + var allMath = math.create({parenthesis: 'all'}); + + assert.equal(allMath.parse('1/2/3').toTex(), '\\frac{\\left(\\frac{1}{2}\\right)}{3}'); + }); + it ('should LaTeX an OperatorNode', function () { var a = new ConstantNode(2); var b = new ConstantNode(3); diff --git a/test/expression/node/RangeNode.test.js b/test/expression/node/RangeNode.test.js index c5f583baa..3e3429585 100644 --- a/test/expression/node/RangeNode.test.js +++ b/test/expression/node/RangeNode.test.js @@ -280,6 +280,13 @@ describe('RangeNode', function() { assert.equal(n.toString(), '(0:10):2:100'); }); + it ('should respect the \'all\' parenthesis option', function () { + var allMath = math.create({parenthesis: 'all'}); + + assert.equal(allMath.parse('1:2:3').toString(), '(1):(2):(3)'); + assert.equal(allMath.parse('1:2:3').toTex(), '\\left(1\\right):\\left(2\\right):\\left(3\\right)'); + }); + it ('should LaTeX a RangeNode without step', function () { var start = new ConstantNode(0); var end = new ConstantNode(10); diff --git a/test/expression/node/UpdateNode.test.js b/test/expression/node/UpdateNode.test.js index 1b1a4d99a..88d8703dc 100644 --- a/test/expression/node/UpdateNode.test.js +++ b/test/expression/node/UpdateNode.test.js @@ -321,6 +321,13 @@ describe('UpdateNode', function() { assert.equal(n.toString(), 'a[2, 1] = 5'); }); + it ('should respect the \'all\' parenthesis option', function () { + var allMath = math.create({parenthesis: 'all'}); + + assert.equal(allMath.parse('a[1]=2').toString(), 'a[1] = (2)' ); + assert.equal(allMath.parse('a[1]=2').toTex(), ' a_{1}:=\\left(2\\right)' ); + }); + it ('should LaTeX an UpdateNode', function () { var a = new SymbolNode('a'); var ranges = [ @@ -330,7 +337,7 @@ describe('UpdateNode', function() { var v = new ConstantNode(5); var n = new UpdateNode(new IndexNode(a, ranges), v); - assert.equal(n.toTex(), ' a_{\\left[2,1\\right]}:=5'); + assert.equal(n.toTex(), ' a_{2,1}:=5'); }); it ('should LaTeX an UpdateNode with custom toTex', function () {