Repository: josdejong/mathjs
Pull request #292 (2015-03-01 09:37:55)

New generic implementation of toString
See #290 

One thing is still unclear: Should `getPrecedence`, `getAssociativity` and `isAssociativeWith` be added to `Node.prototype`?


Patch:
diff --git a/lib/expression/node/AssignmentNode.js b/lib/expression/node/AssignmentNode.js
index 207287bc0..49ce5a8a4 100644
--- a/lib/expression/node/AssignmentNode.js
+++ b/lib/expression/node/AssignmentNode.js
@@ -4,6 +4,7 @@ var Node = require('./Node'),
     ArrayNode = require('./ArrayNode'),
 
     keywords = require('../keywords'),
+    operators = require('../operators'),
 
     latex = require('../../util/latex'),
     isString = require('../../util/string').isString;
@@ -77,7 +78,13 @@ AssignmentNode.prototype.clone = function() {
  * @return {String}
  */
 AssignmentNode.prototype.toString = function() {
-  return this.name + ' = ' + this.expr.toString();
+  var precedence = operators.getPrecedence(this);
+  var exprPrecedence = operators.getPrecedence(this.expr);
+  var expr = this.expr.toString();
+  if ((exprPrecedence !== null) && (exprPrecedence <= precedence)) {
+    expr = '(' + expr + ')';
+  }
+  return this.name + ' = ' + expr;
 };
 
 /**
diff --git a/lib/expression/node/ConditionalNode.js b/lib/expression/node/ConditionalNode.js
index 459b7d353..54ea5c02f 100644
--- a/lib/expression/node/ConditionalNode.js
+++ b/lib/expression/node/ConditionalNode.js
@@ -6,6 +6,7 @@ var BigNumber = require('../../type/BigNumber');
 var Complex = require('../../type/Complex');
 var Unit = require('../../type/Unit');
 var util = require('../../util');
+var operators = require('../operators');
 var isString = util.string.isString;
 var isNumber = util.number.isNumber;
 var isBoolean = util['boolean'].isBoolean;
@@ -119,10 +120,33 @@ ConditionalNode.prototype.clone = function() {
  * @return {String} str
  */
 ConditionalNode.prototype.toString = function() {
-  // TODO: not nice adding parenthesis al the time
-  return '(' + this.condition.toString() + ') ? (' +
-      this.trueExpr.toString() + ') : (' +
-      this.falseExpr.toString() + ')';
+  var precedence = operators.getPrecedence(this);
+
+  //Enclose Arguments in parentheses if they are an OperatorNode
+  //or have lower or equal precedence
+  //NOTE: enclosing all OperatorNodes in parentheses is a decision
+  //purely based on aesthetics and readability
+  var condition = this.condition.toString();
+  var conditionPrecedence = operators.getPrecedence(this.condition);
+  if ((this.condition.type === 'OperatorNode')
+      || ((conditionPrecedence !== null) && (conditionPrecedence <= precedence))) {
+    condition = '(' + condition + ')';
+  }
+
+  var trueExpr = this.trueExpr.toString();
+  var truePrecedence = operators.getPrecedence(this.trueExpr);
+  if ((this.trueExpr.type === 'OperatorNode')
+      || ((truePrecedence !== null) && (truePrecedence <= precedence))) {
+    trueExpr = '(' + trueExpr + ')';
+  }
+
+  var falseExpr = this.falseExpr.toString();
+  var falsePrecedence = operators.getPrecedence(this.falseExpr);
+  if ((this.falseExpr.type === 'OperatorNode')
+      || ((falsePrecedence !== null) && (falsePrecedence <= precedence))) {
+    falseExpr = '(' + falseExpr + ')';
+  }
+  return condition + ' ? ' + trueExpr + ' : ' + falseExpr;
 };
 
 /**
diff --git a/lib/expression/node/FunctionAssignmentNode.js b/lib/expression/node/FunctionAssignmentNode.js
index 5eb9ad3c7..c2a8d5477 100644
--- a/lib/expression/node/FunctionAssignmentNode.js
+++ b/lib/expression/node/FunctionAssignmentNode.js
@@ -3,6 +3,7 @@
 var Node = require('./Node');
 var keywords = require('../keywords');
 var latex = require('../../util/latex');
+var operators = require('../operators');
 var isString = require('../../util/string').isString;
 var isArray = Array.isArray;
 
@@ -96,9 +97,15 @@ FunctionAssignmentNode.prototype.clone = function() {
  * @return {String} str
  */
 FunctionAssignmentNode.prototype.toString = function() {
+  var precedence = operators.getPrecedence(this);
+  var exprPrecedence = operators.getPrecedence(this.expr);
+
+  var expr = this.expr.toString();
+  if ((exprPrecedence !== null) && (exprPrecedence <= precedence)) {
+    expr = '(' + expr + ')';
+  }
   return 'function ' + this.name +
-      '(' + this.params.join(', ') + ') = ' +
-      this.expr.toString();
+      '(' + this.params.join(', ') + ') = ' + expr;
 };
 
 /**
diff --git a/lib/expression/node/FunctionNode.js b/lib/expression/node/FunctionNode.js
index db76d1863..b36255502 100644
--- a/lib/expression/node/FunctionNode.js
+++ b/lib/expression/node/FunctionNode.js
@@ -119,4 +119,12 @@ FunctionNode.prototype.toTex = function() {
   return latex.toArgs(this);
 };
 
+/**
+ * Get identifier.
+ * @return {String}
+ */
+FunctionNode.prototype.getIdentifier = function () {
+  return this.type + ':' + this.name;
+};
+
 module.exports = FunctionNode;
diff --git a/lib/expression/node/Node.js b/lib/expression/node/Node.js
index bb63208cf..5b97d5693 100644
--- a/lib/expression/node/Node.js
+++ b/lib/expression/node/Node.js
@@ -228,6 +228,14 @@ Node.prototype.toTex = function() {
   return '';
 };
 
+/**
+ * Get identifier.
+ * @return {String}
+ */
+Node.prototype.getIdentifier = function () {
+ return this.type;
+};
+
 /**
  * Test whether an object is a Node
  * @param {*} object
diff --git a/lib/expression/node/OperatorNode.js b/lib/expression/node/OperatorNode.js
index 6116862a7..8398fc17b 100644
--- a/lib/expression/node/OperatorNode.js
+++ b/lib/expression/node/OperatorNode.js
@@ -4,7 +4,10 @@ var Node = require('./Node'),
     ConstantNode = require('./ConstantNode'),
     SymbolNode = require('./SymbolNode'),
     FunctionNode = require('./FunctionNode'),
-    latex = require('../../util/latex');
+    latex = require('../../util/latex'),
+    operators = require('../operators'),
+    isArray = Array.isArray,
+    isNode = Node.isNode;
 
 /**
  * @constructor OperatorNode
@@ -20,7 +23,17 @@ function OperatorNode (op, fn, args) {
     throw new SyntaxError('Constructor must be called with the new operator');
   }
 
-  // TODO: validate input
+  //validate input
+  if (typeof op !== 'string') {
+    throw new TypeError('string expected for parameter "op"');
+  }
+  if (typeof fn !== 'string') {
+    throw new TypeError('string expected for parameter "fn"');
+  }
+  if (!isArray(args) || !args.every(isNode)) {
+    throw new TypeError('Array containing Nodes expected for parameter "args"');
+  }
+
   this.op = op;
   this.fn = fn;
   this.args = args || [];
@@ -82,36 +95,142 @@ OperatorNode.prototype.clone = function() {
 };
 
 /**
- * Get string representation
+ * Calculate which parentheses are necessary. Gets an OperatorNode
+ * (which is the root of the tree) and an Array of Nodes
+ * (this.args) and returns an array where 'true' means that an argument
+ * has to be enclosed in parentheses whereas 'false' means the opposite.
+ *
+ * @param {OperatorNode} root
+ * @param {Node[]} arguments
+ * @return {bool[]}
+ * @private
+ */
+function calculateNecessaryParentheses (root, args) {
+  //precedence of the root OperatorNode
+  var precedence = operators.getPrecedence(root);
+  var associativity = operators.getAssociativity(root);
+
+  switch (args.length) {
+    case 1: //unary operators
+      //precedence of the operand
+      var operandPrecedence = operators.getPrecedence(args[0]);
+
+      if (operandPrecedence === null) {
+        //if the operand has no defined precedence, no parens are needed
+        return [false];
+      }
+
+      if (operandPrecedence <= precedence) {
+        //if the operands precedence is lower, parens are needed
+        return [true];
+      }
+
+      //otherwise, no parens needed
+      return [false];
+
+    case 2: //binary operators
+      var lhsParens; //left hand side needs parenthesis?
+      //precedence of the left hand side
+      var lhsPrecedence = operators.getPrecedence(args[0]);
+      //is the root node associative with the left hand side
+      var assocWithLhs = operators.isAssociativeWith(root, args[0]);
+
+      if (lhsPrecedence === null) {
+        //if the left hand side has no defined precedence, no parens are needed
+        //FunctionNode for example
+        lhsParens = false;
+      }
+      else if ((lhsPrecedence === precedence) && (associativity === 'right') && !assocWithLhs) {
+        //In case of equal precedence, if the root node is left associative
+        // parens are **never** necessary for the left hand side.
+        //If it is right associative however, parens are necessary
+        //if the root node isn't associative with the left hand side
+        lhsParens = true;
+      }
+      else if (lhsPrecedence < precedence) {
+        lhsParens = true;
+      }
+      else {
+        lhsParens = false;
+      }
+
+      var rhsParens; //right hand side needs parenthesis?
+      //precedence of the right hand side
+      var rhsPrecedence = operators.getPrecedence(args[1]);
+      //is the root node associative with the right hand side?
+      var assocWithRhs = operators.isAssociativeWith(root, args[1]);
+
+      if (rhsPrecedence === null) {
+        //if the right hand side has no defined precedence, no parens are needed
+        //FunctionNode for example
+        rhsParens = false;
+      }
+      else if ((rhsPrecedence === precedence) && (associativity === 'left') && !assocWithRhs) {
+        //In case of equal precedence, if the root node is right associative
+        // parens are **never** necessary for the right hand side.
+        //If it is left associative however, parens are necessary
+        //if the root node isn't associative with the right hand side
+        rhsParens = true;
+      }
+      else if (rhsPrecedence < precedence) {
+        rhsParens = true;
+      }
+      else {
+        rhsParens = false;
+      }
+      return [lhsParens, rhsParens];
+    default:
+      //behavior is undefined, fall back to putting everything in parens
+      var parens = [];
+      args.forEach(function () {
+        parens.push(true);
+      });
+      return parens;
+  }
+}
+
+/**
+ * Get string representation.
  * @return {String} str
  */
 OperatorNode.prototype.toString = function() {
   var args = this.args;
+  var parens = calculateNecessaryParentheses(this, args);
 
   switch (args.length) {
-    case 1:
-      if (this.op == '-') {
-        // special case: unary minus
-        return '-' + args[0].toString();
+    case 1: //unary operators
+      var assoc = operators.getAssociativity(this);
+
+      var operand = args[0].toString();
+      if (parens[0]) {
+        operand = '(' + operand + ')';
       }
-      else {
-        // for example '5!'
-        return args[0].toString() + this.op;
+
+      if (assoc === 'right') { //prefix operator
+        return this.op + operand;
+      }
+      else if (assoc === 'left') { //postfix
+        return operand + this.op;
       }
 
-    case 2: // for example '2+3'
-      var lhs = args[0].toString();
-      if (args[0] instanceof OperatorNode) {
+      //fall back to postfix
+      return operand + this.op;
+
+    case 2:
+      var lhs = args[0].toString(); //left hand side
+      var rhs = args[1].toString(); //right hand side
+      if (parens[0]) { //left hand side in parenthesis?
         lhs = '(' + lhs + ')';
       }
-      var rhs = args[1].toString();
-      if (args[1] instanceof OperatorNode) {
+      if (parens[1]) { //right hand side in parenthesis?
         rhs = '(' + rhs + ')';
       }
+
       return lhs + ' ' + this.op + ' ' + rhs;
 
-    default: // this should not occur. format as a function call
-      return this.op + '(' + this.args.join(', ') + ')';
+    default:
+      //fallback to formatting as a function call
+      return this.fn + '(' + this.args.join(', ') + ')';
   }
 };
 
@@ -214,4 +333,12 @@ OperatorNode.prototype.toTex = function() {
   }
 };
 
+/**
+ * Get identifier.
+ * @return {String}
+ */
+OperatorNode.prototype.getIdentifier = function () {
+  return this.type + ':' + this.fn;
+};
+
 module.exports = OperatorNode;
diff --git a/lib/expression/node/RangeNode.js b/lib/expression/node/RangeNode.js
index 595b037e4..f1f08b2bb 100644
--- a/lib/expression/node/RangeNode.js
+++ b/lib/expression/node/RangeNode.js
@@ -1,6 +1,7 @@
 'use strict';
 
 var Node = require('./Node');
+var operators = require('../operators');
 
 var isNode = Node.isNode;
 
@@ -87,12 +88,33 @@ RangeNode.prototype.clone = function() {
  * @return {String} str
  */
 RangeNode.prototype.toString = function() {
-  // format the range like "start:step:end"
-  var str = this.start.toString();
+  var precedence = operators.getPrecedence(this);
+
+  //format string as start:step:stop
+  var str;
+
+  var start = this.start.toString();
+  var startPrecedence = operators.getPrecedence(this.start);
+  if ((startPrecedence !== null) && (startPrecedence <= precedence)) {
+    start = '(' + start + ')';
+  }
+  str = start;
+
   if (this.step) {
-    str += ':' + this.step.toString();
+    var step = this.step.toString();
+    var stepPrecedence = operators.getPrecedence(this.step);
+    if ((stepPrecedence !== null) && (stepPrecedence <= precedence)) {
+      step = '(' + step + ')';
+    }
+    str += ':' + step;
+  }
+
+  var end = this.end.toString();
+  var endPrecedence = operators.getPrecedence(this.end);
+  if ((endPrecedence !== null) && (endPrecedence <= precedence)) {
+    end = '(' + end + ')';
   }
-  str += ':' + this.end.toString();
+  str += ':' + end;
 
   return str;
 };
diff --git a/lib/expression/operators.js b/lib/expression/operators.js
new file mode 100644
index 000000000..1f406d735
--- /dev/null
+++ b/lib/expression/operators.js
@@ -0,0 +1,272 @@
+'use strict'
+
+//list of identifiers of nodes in order of their precedence
+//also contains information about left/right associativity
+//and which other operator the operator is associative with
+//Example:
+// addition is associative with addition and subtraction, because:
+// (a+b)+c=a+(b+c)
+// (a+b)-c=a+(b-c)
+//
+// postfix operators are left associative, prefix operators 
+// are right associative
+var properties = [
+  { //assignment
+    'AssignmentNode': {},
+    'FunctionAssignmentNode': {}
+  },
+  { //conditional expression
+    'ConditionalNode': {}
+  },
+  { //logical or
+    'OperatorNode:or': {
+      associativity: 'left',
+      associativeWith: []
+    }
+
+  },
+  { //logical xor
+    'OperatorNode:xor': {
+      associativity: 'left',
+      associativeWith: []
+    }
+  },
+  { //logical and
+    'OperatorNode:and': {
+      associativity: 'left',
+      associativeWith: []
+    }
+  },
+  { //bitwise or
+    'OperatorNode:bitOr': {
+      associativity: 'left',
+      associativeWith: []
+    }
+  },
+  { //bitwise xor
+    'OperatorNode:bitXor': {
+      associativity: 'left',
+      associativeWith: []
+    }
+  },
+  { //bitwise and
+    'OperatorNode:bitAnd': {
+      associativity: 'left',
+      associativeWith: []
+    }
+  },
+  { //relational operators
+    'OperatorNode:equal': {
+      associativity: 'left',
+      associativeWith: []
+    },
+    'OperatorNode:unequal': {
+      associativity: 'left',
+      associativeWith: []
+    },
+    'OperatorNode:smaller': {
+      associativity: 'left',
+      associativeWith: []
+    },
+    'OperatorNode:larger': {
+      associativity: 'left',
+      associativeWith: []
+    },
+    'OperatorNode:smallerEq': {
+      associativity: 'left',
+      associativeWith: []
+    },
+    'OperatorNode:largerEq': {
+      associativity: 'left',
+      associativeWith: []
+    }
+  },
+  { //bitshift operators
+    'OperatorNode:leftShift': {
+      associativity: 'left',
+      associativeWith: []
+    },
+    'OperatorNode:rightArithShift': {
+      associativity: 'left',
+      associativeWith: []
+    },
+    'OperatorNode:rightLogShift': {
+      associativity: 'left',
+      associativeWith: []
+    }
+  },
+  { //unit conversion
+    'OperatorNode:to': {
+      associativity: 'left',
+      associativeWith: []
+    }
+  },
+  { //range
+    'RangeNode': {}
+  },
+  { //addition, subtraction
+    'OperatorNode:add': {
+      associativity: 'left',
+      associativeWith: ['OperatorNode:add', 'OperatorNode:subtract']
+    },
+    'OperatorNode:subtract': {
+      associativity: 'left',
+      associativeWith: []
+    }
+  },
+  { //multiply, divide, modulus
+    'OperatorNode:multiply': {
+      associativity: 'left',
+      associativeWith: [
+        'OperatorNode:multiply',
+        'OperatorNode:divide',
+        'Operator:dotMultiply',
+        'Operator:dotDivide'
+      ]
+    },
+    'OperatorNode:divide': {
+      associativity: 'left',
+      associativeWith: []
+    },
+    'OperatorNode:dotMultiply': {
+      associativity: 'left',
+      associativeWith: [
+        'OperatorNode:multiply',
+        'OperatorNode:divide',
+        'OperatorNode:dotMultiply',
+        'OperatorNode:doDivide'
+      ]
+    },
+    'OperatorNode:dotDivide': {
+      associativity: 'left',
+      associativeWith: []
+    },
+    'OperatorNode:mod': {
+      associativity: 'left',
+      associativeWith: []
+    }
+  },
+  { //unary prefix operators
+    'OperatorNode:unaryPlus': {
+      associativity: 'right'
+    },
+    'OperatorNode:unaryMinus': {
+      associativity: 'right'
+    },
+    'OperatorNode:bitNot': {
+      associativity: 'right'
+    },
+    'OperatorNode:not': {
+      associativity: 'right'
+    }
+  },
+  { //exponentiation
+    'OperatorNode:pow': {
+      associativity: 'right',
+      associativeWith: []
+    },
+    'OperatorNode:dotPow': {
+      associativity: 'right',
+      associativeWith: []
+    }
+  },
+  { //factorial
+    'OperatorNode:factorial': {
+      associativity: 'left'
+    }
+  },
+  { //matrix transpose
+    'OperatorNode:transpose': {
+      associativity: 'left'
+    }
+  }
+];
+
+/**
+ * Get the precedence of a Node.
+ * Higher number for higher precedence, starting with 0.
+ * Returns null if the precedence is undefined.
+ *
+ * @param {Node}
+ * @return {Number|null}
+ */
+function getPrecedence (node) {
+  var identifier = node.getIdentifier();
+  for (var i = 0; i < properties.length; i++) {
+    if (identifier in properties[i]) {
+      return i;
+    }
+  }
+  return null;
+}
+
+/**
+ * Get the associativity of an operator (left or right).
+ * Returns a string containing 'left' or 'right' or null if
+ * the associativity is not defined.
+ *
+ * @param {Node}
+ * @return {String|null}
+ * @throws {Error}
+ */
+function getAssociativity (node) {
+  var identifier = node.getIdentifier();
+  var index = getPrecedence(node);
+  if (index === null) {
+    //node isn't in the list
+    return null;
+  }
+  var property = properties[index][identifier];
+
+  if (property.hasOwnProperty('associativity')) {
+    if (property.associativity === 'left') {
+      return 'left';
+    }
+    if (property.associativity === 'right') {
+      return 'right';
+    }
+    //associativity is invalid
+    throw Error('\'' + identifier + '\' has the invalid associativity \''
+                + property.associativity + '\'.');
+  }
+
+  //associativity is undefined
+  return null;
+}
+
+/**
+ * Check if an operator is associative with another operator.
+ * Returns either true or false or null if not defined.
+ *
+ * @param {Node} nodeA
+ * @param {Node} nodeB
+ * @return {bool|null}
+ */
+function isAssociativeWith (nodeA, nodeB) {
+  var identifierA = nodeA.getIdentifier();
+  var identifierB = nodeB.getIdentifier();
+  var index = getPrecedence(nodeA);
+  if (index === null) {
+    //node isn't in the list
+    return null;
+  }
+  var property = properties[index][identifierA];
+
+  if (property.hasOwnProperty('associativeWith')
+      && (property.associativeWith instanceof Array)) {
+    for (var i = 0; i < property.associativeWith.length; i++) {
+      if (property.associativeWith[i] === identifierB) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  //associativeWith is not defined
+  return null;
+}
+
+module.exports.properties = properties;
+module.exports.getPrecedence = getPrecedence;
+module.exports.getAssociativity = getAssociativity;
+module.exports.isAssociativeWith = isAssociativeWith;


Test patch:
diff --git a/test/expression/node/AssignmentNode.test.js b/test/expression/node/AssignmentNode.test.js
index b9ddddfaa..94c733ac6 100644
--- a/test/expression/node/AssignmentNode.test.js
+++ b/test/expression/node/AssignmentNode.test.js
@@ -200,6 +200,15 @@ describe('AssignmentNode', function() {
     assert.equal(n.toString(), 'b = 3');
   });
 
+  it ('should stringify an AssignmentNode containing and AssignmentNode', function () {
+    var a = new ConstantNode(2);
+    var b = new AssignmentNode('a', a);
+
+    var n = new AssignmentNode('b', b);
+
+    assert.equal(n.toString(), 'b = (a = 2)');
+  });
+
   it ('should LaTeX 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 e1b7ae37e..e50ab1305 100644
--- a/test/expression/node/ConditionalNode.test.js
+++ b/test/expression/node/ConditionalNode.test.js
@@ -251,7 +251,7 @@ describe('ConditionalNode', function() {
   it ('should stringify a ConditionalNode', function () {
     var n = new ConditionalNode(condition, a, b);
 
-    assert.equal(n.toString(), '(true) ? (a = 2) : (b = 3)');
+    assert.equal(n.toString(), 'true ? (a = 2) : (b = 3)');
   });
 
   it ('should LaTeX a ConditionalNode', function () {
diff --git a/test/expression/node/FunctionAssignmentNode.test.js b/test/expression/node/FunctionAssignmentNode.test.js
index e48260c76..6f3cc3c22 100644
--- a/test/expression/node/FunctionAssignmentNode.test.js
+++ b/test/expression/node/FunctionAssignmentNode.test.js
@@ -6,6 +6,7 @@ var assert = require('assert'),
     ConstantNode = require('../../../lib/expression/node/ConstantNode'),
     OperatorNode = require('../../../lib/expression/node/OperatorNode'),
     FunctionAssignmentNode = require('../../../lib/expression/node/FunctionAssignmentNode'),
+    AssignmentNode = require('../../../lib/expression/node/AssignmentNode'),
     RangeNode = require('../../../lib/expression/node/RangeNode'),
     SymbolNode = require('../../../lib/expression/node/SymbolNode');
 
@@ -178,6 +179,15 @@ describe('FunctionAssignmentNode', function() {
     assert.equal(n.toString(), 'function f(x) = 2 + x');
   });
 
+  it ('should stringify a FunctionAssignmentNode conataining an AssignmentNode', function () {
+    var a = new ConstantNode(2);
+
+    var n1 = new AssignmentNode('a', a);
+    var n = new FunctionAssignmentNode('f', ['x'], n1);
+
+    assert.equal(n.toString(), 'function f(x) = (a = 2)');
+  });
+
   it ('should LaTeX a FunctionAssignmentNode', function() {
     var a = new ConstantNode(2);
     var x = new SymbolNode('x');
diff --git a/test/expression/node/FunctionNode.test.js b/test/expression/node/FunctionNode.test.js
index 3d8338029..fe6e118ea 100644
--- a/test/expression/node/FunctionNode.test.js
+++ b/test/expression/node/FunctionNode.test.js
@@ -281,4 +281,11 @@ describe('FunctionNode', function() {
     assert.equal(n3.toTex(), '{\\left({4}+{5}\\right)!}');
   });
 
+  it ('should have an identifier', function () {
+    var a = new ConstantNode(2);
+    var n = new FunctionNode('factorial', [a]);
+
+    assert.equal(n.getIdentifier(), 'FunctionNode:factorial');
+  });
+
 });
diff --git a/test/expression/node/Node.test.js b/test/expression/node/Node.test.js
index e398affba..355856d16 100644
--- a/test/expression/node/Node.test.js
+++ b/test/expression/node/Node.test.js
@@ -93,4 +93,10 @@ describe('Node', function() {
     }, /Cannot compile a Node interface/);
   });
 
+  it ('should have an identifier', function () {
+    var node = new Node();
+
+    assert.equal(node.getIdentifier(), 'Node');
+  });
+
 });
diff --git a/test/expression/node/OperatorNode.test.js b/test/expression/node/OperatorNode.test.js
index 45800fc9f..607e94122 100644
--- a/test/expression/node/OperatorNode.test.js
+++ b/test/expression/node/OperatorNode.test.js
@@ -10,7 +10,7 @@ var assert = require('assert'),
 describe('OperatorNode', function() {
 
   it ('should create an OperatorNode', function () {
-    var n = new OperatorNode();
+    var n = new OperatorNode('op', 'fn', []);
     assert(n instanceof OperatorNode);
     assert(n instanceof Node);
     assert.equal(n.type, 'OperatorNode');
@@ -57,7 +57,7 @@ describe('OperatorNode', function() {
   });
 
   it ('should filter an OperatorNode without contents', function () {
-    var n = new OperatorNode();
+    var n = new OperatorNode('op', 'fn', []);
 
     assert.deepEqual(n.filter(function (node) {return node instanceof OperatorNode}),  [n]);
     assert.deepEqual(n.filter(function (node) {return node instanceof SymbolNode}),    []);
@@ -176,55 +176,98 @@ describe('OperatorNode', function() {
     assert.strictEqual(d.args[1], c.args[1]);
   });
 
-  it ('should stringify an OperatorNode', function () {
-    var a = new ConstantNode(2);
-    var b = new ConstantNode(3);
-    var c = new ConstantNode(4);
+  describe('toString', function () {
+    it ('should stringify an OperatorNode', function () {
+      var a = new ConstantNode(2);
+      var b = new ConstantNode(3);
+      var c = new ConstantNode(4);
 
-    var n = new OperatorNode('+', 'add', [a, b]);
-    assert.equal(n.toString(), '2 + 3');
-  });
+      var n = new OperatorNode('+', 'add', [a, b]);
+      assert.equal(n.toString(), '2 + 3');
+    });
 
-  it ('should stringify an OperatorNode with factorial', function () {
-    var a = new ConstantNode(2);
-    var n = new OperatorNode('!', 'factorial', [a]);
-    assert.equal(n.toString(), '2!');
-  });
+    it ('should stringify an OperatorNode with factorial', function () {
+      var a = new ConstantNode(2);
+      var n = new OperatorNode('!', 'factorial', [a]);
+      assert.equal(n.toString(), '2!');
+    });
 
-  it ('should stringify an OperatorNode with unary minus', function () {
-    var a = new ConstantNode(2);
-    var n = new OperatorNode('-', 'unaryMinus', [a]);
-    assert.equal(n.toString(), '-2');
-  });
+    it ('should stringify an OperatorNode with unary minus', function () {
+      var a = new ConstantNode(2);
+      var n = new OperatorNode('-', 'unaryMinus', [a]);
+      assert.equal(n.toString(), '-2');
+    });
 
-  it ('should stringify an OperatorNode with zero arguments', function () {
-    var n = new OperatorNode('foo', 'foo', []);
-    assert.equal(n.toString(), 'foo()');
-  });
+    it ('should stringify an OperatorNode with zero arguments', function () {
+      var n = new OperatorNode('foo', 'foo', []);
+      assert.equal(n.toString(), 'foo()');
+    });
 
-  it ('should stringify an OperatorNode with more than two operators', function () {
-    var a = new ConstantNode(2);
-    var b = new ConstantNode(3);
-    var c = new ConstantNode(4);
+    it ('should stringify an OperatorNode with more than two operators', function () {
+      var a = new ConstantNode(2);
+      var b = new ConstantNode(3);
+      var c = new ConstantNode(4);
 
-    var n = new OperatorNode('foo', 'foo', [a, b, c]);
-    assert.equal(n.toString(), 'foo(2, 3, 4)');
+      var n = new OperatorNode('foo', 'foo', [a, b, c]);
+      assert.equal(n.toString(), 'foo(2, 3, 4)');
 
-  });
+    });
 
-  it ('should stringify an OperatorNode with nested operator nodes', function () {
-    var a = new ConstantNode(2);
-    var b = new ConstantNode(3);
-    var c = new ConstantNode(4);
-    var d = new ConstantNode(5);
+    it ('should stringify an OperatorNode with nested operator nodes', function () {
+      var a = new ConstantNode(2);
+      var b = new ConstantNode(3);
+      var c = new ConstantNode(4);
+      var d = new ConstantNode(5);
 
-    var n1 = new OperatorNode('+', 'add', [a, b]);
-    var n2 = new OperatorNode('-', 'subtract', [c, d]);
-    var n3 = new OperatorNode('*', 'multiply', [n1, n2]);
+      var n1 = new OperatorNode('+', 'add', [a, b]);
+      var n2 = new OperatorNode('-', 'subtract', [c, d]);
+      var n3 = new OperatorNode('*', 'multiply', [n1, n2]);
+
+      assert.equal(n1.toString(), '2 + 3');
+      assert.equal(n2.toString(), '4 - 5');
+      assert.equal(n3.toString(), '(2 + 3) * (4 - 5)');
+    });
+
+    it ('should stringify left associative OperatorNodes that are associative with another Node', function () {
+      assert.equal(math.parse('(a+b)+c').toString(), 'a + b + c');
+      assert.equal(math.parse('a+(b+c)').toString(), 'a + b + c');
+      assert.equal(math.parse('(a+b)-c').toString(), 'a + b - c');
+      assert.equal(math.parse('a+(b-c)').toString(), 'a + b - c');
+
+      assert.equal(math.parse('(a*b)*c').toString(), 'a * b * c');
+      assert.equal(math.parse('a*(b*c)').toString(), 'a * b * c');
+      assert.equal(math.parse('(a*b)/c').toString(), 'a * b / c');
+      assert.equal(math.parse('a*(b/c)').toString(), 'a * b / c');
+    });
+
+    it ('should stringify left associative OperatorNodes that are not associative with another Node', function () {
+      assert.equal(math.parse('(a-b)-c').toString(), 'a - b - c');
+      assert.equal(math.parse('a-(b-c)').toString(), 'a - (b - c)');
+      assert.equal(math.parse('(a-b)+c').toString(), 'a - b + c');
+      assert.equal(math.parse('a-(b+c)').toString(), 'a - (b + c)');
+
+      assert.equal(math.parse('(a/b)/c').toString(), 'a / b / c');
+      assert.equal(math.parse('a/(b/c)').toString(), 'a / (b / c)');
+      assert.equal(math.parse('(a/b)*c').toString(), 'a / b * c');
+      assert.equal(math.parse('a/(b*c)').toString(), 'a / (b * c)');
+    });
 
-    assert.equal(n1.toString(), '2 + 3');
-    assert.equal(n2.toString(), '4 - 5');
-    assert.equal(n3.toString(), '(2 + 3) * (4 - 5)');
+    it ('should stringify right associative OperatorNodes that are not associative with another Node', function () {
+      assert.equal(math.parse('(a^b)^c').toString(), '(a ^ b) ^ c');
+      assert.equal(math.parse('a^(b^c)').toString(), 'a ^ b ^ c');
+    });
+
+    it ('should stringify unary OperatorNodes containing a binary OperatorNode', function () {
+      assert.equal(math.parse('(a*b)!').toString(), '(a * b)!');
+      assert.equal(math.parse('-(a*b)').toString(), '-(a * b)');
+      assert.equal(math.parse('-(a+b)').toString(), '-(a + b)');
+    });
+
+    it ('should stringify unary OperatorNodes containing a unary OperatorNode', function () {
+      assert.equal(math.parse('(-a)!').toString(), '(-a)!');
+      assert.equal(math.parse('-(a!)').toString(), '-a!');
+      assert.equal(math.parse('-(-a)').toString(), '-(-a)');
+    });
   });
 
   it ('should LaTeX an OperatorNode', function () {
@@ -326,4 +369,13 @@ describe('OperatorNode', function() {
     assert.equal(m3.toTex(), '{\\left({{2}+{3}}\\right) \\cdot {4}}-{5}');
   });
 
+  it ('should have an identifier', function () {
+    var a = new ConstantNode(1);
+    var b = new ConstantNode(2);
+
+    var n = new OperatorNode('+', 'add', [a, b]);
+
+    assert.equal(n.getIdentifier(), 'OperatorNode:add');
+  });
+
 });
diff --git a/test/expression/node/RangeNode.test.js b/test/expression/node/RangeNode.test.js
index 8380d1893..a69c529cd 100644
--- a/test/expression/node/RangeNode.test.js
+++ b/test/expression/node/RangeNode.test.js
@@ -5,7 +5,8 @@ var assert = require('assert'),
     Node = require('../../../lib/expression/node/Node'),
     ConstantNode = require('../../../lib/expression/node/ConstantNode'),
     SymbolNode = require('../../../lib/expression/node/SymbolNode'),
-    RangeNode = require('../../../lib/expression/node/RangeNode');
+    RangeNode = require('../../../lib/expression/node/RangeNode'),
+  OperatorNode = require('../../../lib/expression/node/OperatorNode');
 
 describe('RangeNode', function() {
 
@@ -246,6 +247,30 @@ describe('RangeNode', function() {
 
     assert.equal(n.toString(), '0:2:10');
   });
+  
+  it ('should stringify a RangeNode with an OperatorNode', function () {
+    var a = new ConstantNode(1);
+    var b = new ConstantNode(2);
+
+    var o1 = new OperatorNode('+', 'add', [a, b]);
+    var o2 = new OperatorNode('<', 'smaller', [a, b]);
+
+    var n = new RangeNode(o1, o1, o2);
+
+    assert.equal(n.toString(), '1 + 2:(1 < 2):1 + 2');
+  });
+
+  it ('should stringify a RangeNode with a RangeNode', function () {
+    var start1 = new ConstantNode(0);
+    var end1 = new ConstantNode(10);
+    var step2 = new ConstantNode(2);
+    var end2 = new ConstantNode(100);
+
+    var start2 = new RangeNode(start1, end1);
+    var n = new RangeNode(start2, end2, step2);
+
+    assert.equal(n.toString(), '(0:10):2:100');
+  });
 
   it ('should LaTeX a RangeNode without step', function () {
     var start = new ConstantNode(0);
diff --git a/test/expression/operators.test.js b/test/expression/operators.test.js
new file mode 100644
index 000000000..2d5ec60a5
--- /dev/null
+++ b/test/expression/operators.test.js
@@ -0,0 +1,73 @@
+var assert = require('assert');
+var operators = require('../../lib/expression/operators');
+var OperatorNode = require('../../lib/expression/node/OperatorNode');
+var AssignmentNode = require('../../lib/expression/node/AssignmentNode');
+var ConstantNode = require('../../lib/expression/node/ConstantNode');
+var Node = require('../../lib/expression/node/Node');
+
+describe('operators', function () {
+  it('should return the precedence of a node', function () {
+    var a = new ConstantNode(1);
+    var b = new ConstantNode(2);
+
+    var n1 = new AssignmentNode('a', a);
+    var n2 = new OperatorNode('or', 'or', [a, b]);
+
+    assert.equal(operators.getPrecedence(n1), 0);
+    assert.equal(operators.getPrecedence(n2), 2);
+  });
+
+  it('should return null if precedence is not defined for a node', function () {
+    var n = new Node();
+
+    assert.equal(operators.getPrecedence(n), null);
+  });
+
+  it('should return the associativity of a node', function () {
+    var a = new ConstantNode(1);
+
+    var n1 = new OperatorNode('+', 'add', [a, a]);
+    var n2 = new OperatorNode('^', 'pow', [a, a]);
+    var n3 = new OperatorNode('-', 'unaryMinus', [a]);
+    var n4 = new OperatorNode('!', 'factorial', [a]);
+
+    assert.equal(operators.getAssociativity(n1), 'left');
+    assert.equal(operators.getAssociativity(n2), 'right');
+    assert.equal(operators.getAssociativity(n3), 'right');
+    assert.equal(operators.getAssociativity(n4), 'left');
+  });
+
+  it('should return null if associativity is not defined for a node', function () {
+    var a = new ConstantNode(1);
+
+    var n1 = new Node();
+    var n2 = new AssignmentNode('a', a);
+
+    assert.equal(operators.getAssociativity(n1), null);
+    assert.equal(operators.getAssociativity(n2), null);
+  });
+
+  it('should return if a Node is associative with another Node', function () {
+    var a = new ConstantNode(1);
+
+    var n1 = new OperatorNode('+', 'add', [a, a]);
+    var n2 = new OperatorNode('-', 'subtract', [a, a]);
+
+    assert.equal(operators.isAssociativeWith(n1, n1), true);
+    assert.equal(operators.isAssociativeWith(n1, n2), true);
+    assert.equal(operators.isAssociativeWith(n2, n2), false);
+    assert.equal(operators.isAssociativeWith(n2, n1), false);
+  });
+
+  it('should return null if the associativity between two Nodes is not defined', function () {
+    var a = new ConstantNode(1);
+
+    var n1 = new Node();
+    var n2 = new AssignmentNode('a', a);
+
+    assert.equal(operators.isAssociativeWith(n1, n1), null);
+    assert.equal(operators.isAssociativeWith(n1, n2), null);
+    assert.equal(operators.isAssociativeWith(n2, n2), null);
+    assert.equal(operators.isAssociativeWith(n2, n1), null);
+  });
+});
diff --git a/test/expression/parse.test.js b/test/expression/parse.test.js
index f107a25e5..29cb4fc22 100644
--- a/test/expression/parse.test.js
+++ b/test/expression/parse.test.js
@@ -1262,8 +1262,8 @@ describe('parse', function() {
         var node = math.parse('a ? (b ? c : d) : (e ? f : g)');
         assert(node instanceof ConditionalNode);
         assert.equal(node.condition.toString(), 'a');
-        assert.equal(node.trueExpr.toString(), '(b) ? (c) : (d)');
-        assert.equal(node.falseExpr.toString(), '(e) ? (f) : (g)');
+        assert.equal(node.trueExpr.toString(), 'b ? c : d');
+        assert.equal(node.falseExpr.toString(), 'e ? f : g');
       });
 
       it('should respect precedence of range operator and relational operators', function () {
@@ -1547,8 +1547,8 @@ describe('parse', function() {
     it('should correctly stringify a node tree', function() {
       assert.equal(parse('0').toString(), '0');
       assert.equal(parse('"hello"').toString(), '"hello"');
-      assert.equal(parse('[1, 2 + 3i, 4]').toString(), '[1, 2 + (3 * i), 4]');
-      assert.equal(parse('1/2a').toString(), '(1 / 2) * a');
+      assert.equal(parse('[1, 2 + 3i, 4]').toString(), '[1, 2 + 3 * i, 4]');
+      assert.equal(parse('1/2a').toString(), '1 / 2 * a');
     });
 
     describe('custom nodes', function () {