Browse Source

Merge branch 'feature/mongo_2.6.5_expressions' into feature/mongo_2.6.5_expressions_SetUnion

Tony Ennis 11 years ago
parent
commit
ee853e1fc0
49 changed files with 1991 additions and 2081 deletions
  1. 2 2
      lib/pipeline/Document.js
  2. 13 28
      lib/pipeline/expressions/AllElementsTrueExpression.js
  3. 12 29
      lib/pipeline/expressions/AnyElementTrueExpression.js
  4. 83 78
      lib/pipeline/expressions/CompareExpression.js
  5. 32 87
      lib/pipeline/expressions/CondExpression.js
  6. 11 23
      lib/pipeline/expressions/DayOfMonthExpression.js
  7. 13 24
      lib/pipeline/expressions/DayOfWeekExpression.js
  8. 17 32
      lib/pipeline/expressions/DayOfYearExpression.js
  9. 22 23
      lib/pipeline/expressions/DivideExpression.js
  10. 13 26
      lib/pipeline/expressions/HourExpression.js
  11. 9 24
      lib/pipeline/expressions/IfNullExpression.js
  12. 11 25
      lib/pipeline/expressions/MillisecondExpression.js
  13. 11 23
      lib/pipeline/expressions/MinuteExpression.js
  14. 25 36
      lib/pipeline/expressions/ModExpression.js
  15. 11 23
      lib/pipeline/expressions/MonthExpression.js
  16. 11 24
      lib/pipeline/expressions/NotExpression.js
  17. 11 25
      lib/pipeline/expressions/SecondExpression.js
  18. 9 20
      lib/pipeline/expressions/SizeExpression.js
  19. 21 27
      lib/pipeline/expressions/StrcasecmpExpression.js
  20. 23 30
      lib/pipeline/expressions/SubstrExpression.js
  21. 35 24
      lib/pipeline/expressions/SubtractExpression.js
  22. 13 30
      lib/pipeline/expressions/WeekExpression.js
  23. 8 23
      lib/pipeline/expressions/YearExpression.js
  24. 148 49
      test/lib/pipeline/expressions/AllElementsTrueExpression.js
  25. 142 96
      test/lib/pipeline/expressions/AnyElementTrueExpression.js
  26. 409 308
      test/lib/pipeline/expressions/CompareExpression.js
  27. 0 72
      test/lib/pipeline/expressions/CondExpression.js
  28. 93 104
      test/lib/pipeline/expressions/CondExpression_test.js
  29. 29 59
      test/lib/pipeline/expressions/DayOfMonthExpression.js
  30. 23 28
      test/lib/pipeline/expressions/DayOfWeekExpression.js
  31. 23 28
      test/lib/pipeline/expressions/DayOfYearExpression.js
  32. 47 0
      test/lib/pipeline/expressions/DivideExpression_test.js
  33. 23 28
      test/lib/pipeline/expressions/HourExpression.js
  34. 0 58
      test/lib/pipeline/expressions/IfNullExpression.js
  35. 40 45
      test/lib/pipeline/expressions/IfNullExpression_test.js
  36. 28 43
      test/lib/pipeline/expressions/MillisecondExpression.js
  37. 23 28
      test/lib/pipeline/expressions/MinuteExpression.js
  38. 62 35
      test/lib/pipeline/expressions/ModExpression.js
  39. 23 28
      test/lib/pipeline/expressions/MonthExpression.js
  40. 0 45
      test/lib/pipeline/expressions/NotExpression.js
  41. 47 0
      test/lib/pipeline/expressions/NotExpression_test.js
  42. 23 34
      test/lib/pipeline/expressions/SecondExpression.js
  43. 36 30
      test/lib/pipeline/expressions/SizeExpression.js
  44. 89 40
      test/lib/pipeline/expressions/StrcasecmpExpression.js
  45. 0 161
      test/lib/pipeline/expressions/SubstrExpression.js
  46. 109 0
      test/lib/pipeline/expressions/SubstrExpression_test.js
  47. 111 27
      test/lib/pipeline/expressions/SubtractExpression.js
  48. 23 28
      test/lib/pipeline/expressions/WeekExpression.js
  49. 24 21
      test/lib/pipeline/expressions/YearExpression.js

+ 2 - 2
lib/pipeline/Document.js

@@ -39,7 +39,7 @@ klass.toJson = function toJson(doc) {
 //SKIPPED: most of MutableDocument except for getNestedField and setNestedField, squashed into Document here (because that's how they use it)
 function getNestedFieldHelper(obj, path) {
 	// NOTE: DEVIATION FROM MONGO: from MutableDocument; similar but necessarily different
-	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fields : path.split(".")),
+	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fieldNames : path.split(".")),
 		lastKey = keys[keys.length - 1];
 	for (var i = 0, l = keys.length - 1, cur = obj; i < l && cur instanceof Object; i++) {
 		var next = cur[keys[i]];
@@ -51,7 +51,7 @@ function getNestedFieldHelper(obj, path) {
 klass.getNestedField = getNestedFieldHelper;  // NOTE: ours is static so these are the same
 klass.setNestedField = function setNestedField(obj, path, val) {
 	// NOTE: DEVIATION FROM MONGO: from MutableDocument; similar but necessarily different
-	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fields : path.split(".")),
+	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fieldNames : path.split(".")),
 		lastKey = keys[keys.length - 1];
 	for (var i = 0, l = keys.length - 1, cur = obj; i < l && cur instanceof Object; i++) {
 		var next = cur[keys[i]];

+ 13 - 28
lib/pipeline/expressions/AllElementsTrueExpression.js

@@ -6,43 +6,28 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var AllElementsTrueExpression = module.exports = function AllElementsTrueExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-},
-	klass = AllElementsTrueExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = AllElementsTrueExpression, base = require("./FixedArityExpressionT")(AllElementsTrueExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	CoerceToBoolExpression = require("./CoerceToBoolExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$allElementsTrue";
-};
-
-/**
- * Takes an array of one or more numbers and returns true if all.
- * @method @evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var value = evaluateInternal(vars);
-	if (!vars instanceof Array) throw new Error("$allElementsTrue requires an array");
-
-	for (var i = 0, n = this.operands.length; i < n; ++i) {
-		var checkValue = this.operands[i].evaluateInternal(vars);
-		if (!checkValue.coerceToBool()) return false;
+	var arr = this.operands[0].evaluateInternal(vars);
+	if (!(arr instanceof Array)) throw new Error(this.getOpName() + "'s argument must be an array, but is " + Value.getType(arr) + "; uassert code 17040");
+	for (var i = 0, l = arr.length; i < l; ++i) {
+		if (!Value.coerceToBool(arr[i])) {
+			return false;
+		}
 	}
 	return true;
 };
 
-/** Register Expression */
 Expression.registerExpression("$allElementsTrue", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$allElementsTrue";
+};

+ 12 - 29
lib/pipeline/expressions/AnyElementTrueExpression.js

@@ -6,45 +6,28 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var AnyElementTrueExpression = module.exports = function AnyElementTrueExpression(){
+ */
+var AnyElementTrueExpression = module.exports = function AnyElementTrueExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-},
-	klass = AnyElementTrueExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype,{
-		constructor:{
-			value:klass
-		}
-	});
+}, klass = AnyElementTrueExpression, base = require("./FixedArityExpressionT")(AnyElementTrueExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$anyElementTrue";
-};
-
-/**
- * Takes an array of one or more numbers and returns true if any.
- * @method @evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var arr = this.operands[0].evaluateInternal(vars);
-	if (!(arr instanceof Array)) {
-		throw new Error("uassert 17041: $anyElementTrue's " +
-						"argument must be an array, but is " +
-						typeof arr);
-	}
-	for (var i=0, n=arr.length; i<n; ++i) {
-		if (Value.coerceToBool(arr[i]))
+	if (!(arr instanceof Array)) throw new Error(this.getOpName() + "'s argument must be an array, but is " + Value.getType(arr) + "; uassert code 17041");
+	for (var i = 0, l = arr.length; i < l; ++i) {
+		if (Value.coerceToBool(arr[i])) {
 			return true;
+		}
 	}
 	return false;
 };
 
-/** Register Expression */
 Expression.registerExpression("$anyElementTrue",base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$anyElementTrue";
+};

+ 83 - 78
lib/pipeline/expressions/CompareExpression.js

@@ -6,108 +6,113 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var CompareExpression = module.exports = function CompareExpression(cmpOp) {
+	if (!(arguments.length === 1 && typeof cmpOp === "string")) throw new Error(klass.name + ": args expected: cmpOp");
     this.cmpOp = cmpOp;
     base.call(this);
-}, klass = CompareExpression,
-    FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-    proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-    });
-
-// DEPENDENCIES
-var Value = require("../Value");
-var Expression = require("./Expression");
-var ConstantExpression = require("./ConstantExpression");
-var FieldPathExpression = require("./FieldPathExpression");
-var FieldRangeExpression = require("./FieldRangeExpression");
-var NaryExpression = require("./NaryExpression");
-
-// NESTED CLASSES
+}, klass = CompareExpression, base = require("./FixedArityExpressionT")(CompareExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+
+var Value = require("../Value"),
+	Expression = require("./Expression");
+
+
+klass.parse = function parse(jsonExpr, vps, op) {
+	var expr = new CompareExpression(op),
+		args = base.parseArguments(jsonExpr, vps);
+	expr.validateArguments(args);
+	expr.operands = args;
+	return expr;
+};
+
+
 /**
  * Lookup table for truth value returns
- *
  * @param truthValues   truth value for -1, 0, 1
  * @param reverse               reverse comparison operator
  * @param name                  string name
- **/
-var CmpLookup = (function() { // emulating a struct
-	// CONSTRUCTOR
-	var klass = function CmpLookup(truthValues, reverse, name) {
-		if (arguments.length !== 3) throw new Error("args expected: truthValues, reverse, name");
-		this.truthValues = truthValues;
-		this.reverse = reverse;
-		this.name = name;
-	}, base = Object,
-		proto = klass.prototype = Object.create(base.prototype, {
-			constructor: {
-				value: klass
-			}
-		});
-	return klass;
-})();
-
-// verify we need this below
-// PRIVATE STATIC MEMBERS
+ */
+var CmpLookup = function CmpLookup(truthValues, reverse, name) { // emulating a struct
+	if (arguments.length !== 3) throw new Error("args expected: truthValues, reverse, name");
+	this.truthValues = truthValues;
+	this.reverse = reverse;
+	this.name = name;
+};
+
+
+/**
+ * Enumeration of comparison operators. Any changes to these values require adjustment of
+ * the lookup table in the implementation.
+ */
+var CmpOp = klass.CmpOp = {
+	EQ: "$eq",
+	NE: "$ne",
+	GT: "$gt",
+	GTE: "$gte",
+	LT: "$lt",
+	LTE: "$lte",
+	CMP: "$cmp",
+};
+
+
 /**
  * a table of cmp type lookups to truth values
  * @private
- **/
+ */
 var cmpLookupMap = [ //NOTE: converted from this Array to a Dict/Object below using CmpLookup#name as the key
-    //              -1      0      1      reverse             name     (taking advantage of the fact that our 'enums' are strings below)
-    new CmpLookup([false, true, false], CompareExpression.EQ, CompareExpression.EQ),
-    new CmpLookup([true, false, true], CompareExpression.NE, CompareExpression.NE),
-    new CmpLookup([false, false, true], CompareExpression.LT, CompareExpression.GT),
-    new CmpLookup([false, true, true], CompareExpression.LTE, CompareExpression.GTE),
-    new CmpLookup([true, false, false], CompareExpression.GT, CompareExpression.LT),
-    new CmpLookup([true, true, false], CompareExpression.GTE, CompareExpression.LTE),
-    new CmpLookup([false, false, false], CompareExpression.CMP, CompareExpression.CMP)
+	//              -1      0      1      reverse             name     (taking advantage of the fact that our 'enums' are strings below)
+	new CmpLookup([false, true, false], CmpOp.EQ, CmpOp.EQ),
+	new CmpLookup([true, false, true], CmpOp.NE, CmpOp.NE),
+	new CmpLookup([false, false, true], CmpOp.LT, CmpOp.GT),
+	new CmpLookup([false, true, true], CmpOp.LTE, CmpOp.GTE),
+	new CmpLookup([true, false, false], CmpOp.GT, CmpOp.LT),
+	new CmpLookup([true, true, false], CmpOp.GTE, CmpOp.LTE),
+
+	// CMP is special. Only name is used.
+	new CmpLookup([false, false, false], CmpOp.CMP, CmpOp.CMP)
 ].reduce(function(r, o) {
 	r[o.name] = o;
 	return r;
 }, {});
 
 
-klass.parse = function parse(bsonExpr, vps, op) {
-    var expr = new CompareExpression(op);
-    var args = NaryExpression.parseArguments(bsonExpr, vps);
-    expr.validateArguments(args);
-    expr.vpOperand = args;
-    return expr;
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var left = this.operands[0].evaluateInternal(vars),
+		right = this.operands[1].evaluateInternal(vars),
+		cmp = Value.compare(left, right);
 
-};
+    // Make cmp one of 1, 0, or -1.
+	if (cmp === 0) {
+		//leave as 0
+	} else if (cmp < 0) {
+		cmp = -1;
+	} else if (cmp > 0) {
+		cmp = 1;
+	}
 
-// PROTOTYPE MEMBERS
-proto.evaluateInternal = function evaluateInternal(vars) {
-	//debugger;
-    var left = this.operands[0].evaluateInternal(vars),
-        right = this.operands[1].evaluateInternal(vars),
-        cmp = Expression.signum(Value.compare(left, right));
-    if (this.cmpOp == Expression.CmpOp.CMP) return cmp;
-    return cmpLookupMap[this.cmpOp].truthValues[cmp + 1] || false;
+	if (this.cmpOp === CmpOp.CMP)
+		return cmp;
+
+	var returnValue = cmpLookupMap[this.cmpOp].truthValues[cmp + 1];
+	return returnValue;
 };
 
-klass.EQ = "$eq";
-klass.NE = "$ne";
-klass.GT = "$gt";
-klass.GTE = "$gte";
-klass.LT = "$lt";
-klass.LTE = "$lte";
-klass.CMP = "$cmp";
 
 proto.getOpName = function getOpName() {
 	return this.cmpOp;
 };
 
-/** Register Expression */
-Expression.registerExpression("$eq", klass.parse);
-Expression.registerExpression("$ne", klass.parse);
-Expression.registerExpression("$gt", klass.parse);
-Expression.registerExpression("$gte", klass.parse);
-Expression.registerExpression("$lt", klass.parse);
-Expression.registerExpression("$lte", klass.parse);
-Expression.registerExpression("$cmp", klass.parse);
+
+function bindLast(fn, lastArg) { // similar to the boost::bind used in the mongo code
+	return function() {
+		return fn.apply(this, Array.prototype.slice.call(arguments).concat([lastArg]));
+	};
+}
+Expression.registerExpression("$cmp", bindLast(klass.parse, CmpOp.CMP));
+Expression.registerExpression("$eq", bindLast(klass.parse, CmpOp.EQ));
+Expression.registerExpression("$gt", bindLast(klass.parse, CmpOp.GT));
+Expression.registerExpression("$gte", bindLast(klass.parse, CmpOp.GTE));
+Expression.registerExpression("$lt", bindLast(klass.parse, CmpOp.LT));
+Expression.registerExpression("$lte", bindLast(klass.parse, CmpOp.LTE));
+Expression.registerExpression("$ne", bindLast(klass.parse, CmpOp.NE));

+ 32 - 87
lib/pipeline/expressions/CondExpression.js

@@ -6,108 +6,53 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var CondExpression = module.exports = function CondExpression(vars) {
-		if (arguments.length !== 0) throw new Error("zero args expected");
+ */
+var CondExpression = module.exports = function CondExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": expected args: NONE");
     base.call(this);
-}, klass = CondExpression,
-	base = require("./FixedArityExpressionT")(klass, 3),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = CondExpression, base = require("./FixedArityExpressionT")(CondExpression, 3), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-    Expression = require("./Expression"),
-	FixedArityExpressionT = require("./FixedArityExpressionT");
+    Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$cond";
-proto.getOpName = function getOpName() {
-    return klass.opName;
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var cond = this.operands[0].evaluateInternal(vars);
+	var idx = Value.coerceToBool(cond) ? 1 : 2;
+	return this.operands[idx].evaluateInternal(vars);
 };
 
-/**
- *
- * @param expr	- I expect this to be the RHS of $cond:{...} or $cond:[,,,]
- * @param vps
- * @returns {*}
- */
 klass.parse = function parse(expr, vps) {
-	// There may only be one argument - an array of 3 items, or a hash containing 3 keys.
-    //this.checkArgLimit(3);
-
-    // if not an object, return;
-	// todo I don't understand why we'd do this.  shouldn't expr be {}, [], or wrong?
-    if (typeof(expr) !== Object || )
-		return FixedArityExpressionT.parse(expr, vps);
-
-	// ...or expr could be the entirety of $cond:{...} or $cond:[,,,].
-	if(!(klass.opName in expr)) {
-		throw new Error("Invalid expression. Expected to see '"+klass.opName+"'");
+    if (Value.getType(expr) !== "Object") {
+		return base.parse(expr, vps);
 	}
+	// verify(str::equals(expr.fieldName(), "$cond")); //NOTE: DEVIATION FROM MONGO: we do not have fieldName any more and not sure this is even possible anyway
 
     var ret = new CondExpression();
-
-	// If this is an Object and not an array, verify all the bits are specified.
-	// If this is an Object that is an array, verify there are three bits.
-	// (My issue here is that we got to this parse function when we parsed the $cond:{...} item, and we're calling
-	// parseOperand (again) without altering the input.)
-//    var args = Expression.parseOperand(expr, vps);
-
-	var args = expr[getOpName()];
-
-	if (typeof args !== 'object') throw new Error("this should not happen");
-	if (args instanceof Array) {
-		// it's the array form. Convert it to the object form.
-		if (args.length !== 3) throw new Error("$cond requires exactly three arguments");
-		args = {if: args[0], then: args[1], else: args[2]};
-	}
-
-	// One way or the other, args is now in object form.
-	Object.keys(args).forEach(function(arg) {
-		if (arg === 'if') {
-			ret.operands[0] = Expression.parseOperand(args['if'], vps);
-		}
-		else if (arg === 'then') {
-			ret.operands[1] = Expression.parseOperand(args['then'], vps);
-		}
-		else if (arg === 'else') {
-			ret.operands[2] = Expression.parseOperand(args['else'], vps);
-		}
-		else {
-			throw new Error("Unrecognized parameter to $cond: '" + arg + "'; code 17083");
+	ret.operands.length = 3;
+
+	var args = expr;
+	for (var argfieldName in args) {
+		if (!args.hasOwnProperty(argfieldName)) continue;
+		if (argfieldName === "if") {
+			ret.operands[0] = Expression.parseOperand(args.if, vps);
+		} else if (argfieldName === "then") {
+			ret.operands[1] = Expression.parseOperand(args.then, vps);
+		} else if (argfieldName === "else") {
+			ret.operands[2] = Expression.parseOperand(args.else, vps);
+		} else {
+			throw new Error("Unrecognized parameter to $cond: '" + argfieldName + "'; uasserted code 17083");
 		}
-	});
+	}
 
-    if (!ret.operands[0]) throw new Error("Missing 'if' parameter to $cond; code 17080");
-    if (!ret.operands[1]) throw new Error("Missing 'then' parameter to $cond; code 17081");
-    if (!ret.operands[2]) throw new Error("Missing 'else' parameter to $cond; code 17082");
+    if (!ret.operands[0]) throw new Error("Missing 'if' parameter to $cond; uassert code 17080");
+    if (!ret.operands[1]) throw new Error("Missing 'then' parameter to $cond; uassert code 17081");
+    if (!ret.operands[2]) throw new Error("Missing 'else' parameter to $cond; uassert code 17082");
 
     return ret;
 };
 
-/**
- * Use the $cond operator with the following syntax:
- * { $cond: { if: <boolean-expression>, then: <true-case>, else: <false-case-> } }
- * -or-
- * { $cond: [ <boolean-expression>, <true-case>, <false-case> ] }
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-		var pCond1 = this.operands[0].evaluateInternal(vars);
-
-		this.idx = 0;
-		if (pCond1.coerceToBool()) {
-			this.idx = 1;
-		} else {
-			this.idx = 2;
-		}
+Expression.registerExpression("$cond", CondExpression.parse);
 
-		return this.operands[this.idx].evaluateInternal(vars);
+proto.getOpName = function getOpName() {
+	return "$cond";
 };
-
-/** Register Expression */
-Expression.registerExpression(klass.opName, klass.parse);

+ 11 - 23
lib/pipeline/expressions/DayOfMonthExpression.js

@@ -6,35 +6,23 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var DayOfMonthExpression = module.exports = function DayOfMonthExpression() {
-	base.call(this);
-}, klass = DayOfMonthExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+    if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
+    base.call(this);
+}, klass = DayOfMonthExpression, base = require("./FixedArityExpressionT")(klass, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCDate();
+};
 
-// PROTOTYPE MEMBERS
 proto.getOpName = function getOpName() {
 	return "$dayOfMonth";
 };
 
-/**
- * Takes a date and returns the day of the month as a number between 1 and 31.
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCDate();
-};
-
-/** Register Expression */
 Expression.registerExpression("$dayOfMonth", base.parse);

+ 13 - 24
lib/pipeline/expressions/DayOfWeekExpression.js

@@ -6,34 +6,23 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var DayOfWeekExpression = module.exports = function DayOfWeekExpression(){
+ */
+var DayOfWeekExpression = module.exports = function DayOfWeekExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = DayOfWeekExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor:{
-			value:klass
-		}
-	});
+}, klass = DayOfWeekExpression, base = require("./FixedArityExpressionT")(DayOfWeekExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$dayOfWeek";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCDay() + 1;
 };
 
-/**
- * Takes a date and returns the day of the week as a number between 1 (Sunday) and 7 (Saturday.)
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCDay()+1;
+proto.getOpName = function getOpName() {
+	return "$dayOfWeek";
 };
 
-/** Register Expression */
-Expression.registerExpression("$dayOfWeek",base.parse);
+Expression.registerExpression("$dayOfWeek", base.parse);

+ 17 - 32
lib/pipeline/expressions/DayOfYearExpression.js

@@ -6,42 +6,27 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var DayOfYearExpression = module.exports = function DayOfYearExpression(){
+ */
+var DayOfYearExpression = module.exports = function DayOfYearExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = DayOfYearExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor:{
-			value:klass
-		}
-	});
+}, klass = DayOfYearExpression, base = require("./FixedArityExpressionT")(DayOfYearExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-    return "$dayOfYear";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate),
+		//NOTE: DEVIATION FROM MONGO: our calculations are a little different but are equivalent
+		y11 = new Date(date.getUTCFullYear(), 0, 1), // same year, first month, first day; time omitted
+		ymd = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + 1), // same y,m,d; time omitted, add 1 because days start at 1
+		yday = Math.ceil((ymd - y11) / 86400000); // count days
+	return yday;
 };
 
-/**
- * Takes a date and returns the day of the year as a number between 1 and 366.
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
-	//NOTE: the below silliness is to deal with the leap year scenario when we should be returning 366
-    var date = this.operands[0].evaluateInternal(vars);
-    return klass.getDateDayOfYear(date);
-};
-
-// STATIC METHODS
-klass.getDateDayOfYear = function getDateDayOfYear(d){
-    var y11 = new Date(d.getUTCFullYear(), 0, 1),       // same year, first month, first day; time omitted
-	ymd = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()+1);  // same y,m,d; time omitted, add 1 because days start at 1
-    return Math.ceil((ymd - y11) / 86400000);   //NOTE: 86400000 ms is 1 day
+proto.getOpName = function getOpName() {
+	return "$dayOfYear";
 };
 
-/** Register Expression */
-Expression.registerExpression("$dayOfYear",base.parse);
+Expression.registerExpression("$dayOfYear", base.parse);

+ 22 - 23
lib/pipeline/expressions/DivideExpression.js

@@ -4,43 +4,42 @@
  * A $divide pipeline expression.
  * @see evaluateInternal
  * @class DivideExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
  **/
 var DivideExpression = module.exports = function DivideExpression(){
+    if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
     base.call(this);
-}, klass = DivideExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor:{
-			value:klass
-		}
-	});
+}, klass = DivideExpression, base = require("./FixedArityExpressionT")(DivideExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){ //TODO: try to move this to a static and/or instance field instead of a getter function
-	return "$divide";
-};
-
 /**
  * Takes an array that contains a pair of numbers and returns the value of the first number divided by the second number.
  * @method evaluateInternal
  **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (!(left instanceof Date) && (!right instanceof Date)) throw new Error("$divide does not support dates; code 16373");
-	right = Value.coerceToDouble(right);
-	if (right === 0) return undefined;
-	left = Value.coerceToDouble(left);
-	return left / right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+        var numer = lhs,
+            denom = rhs;
+        if (denom === 0) throw new Error("can't $divide by zero; uassert code 16608");
+
+        return numer / denom;
+    } else if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+        return null;
+    } else{
+        throw new Error("User assertion: 16609: $divide only supports numeric types, not " + Value.getType(lhs) + " and " + Value.getType(rhs));
+    }
 };
 
-/** Register Expression */
-Expression.registerExpression("$divide",base.parse);
+Expression.registerExpression("$divide", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$divide";
+};

+ 13 - 26
lib/pipeline/expressions/HourExpression.js

@@ -2,40 +2,27 @@
 
 /**
  * An $hour pipeline expression.
- * @see evaluateInternal
  * @class HourExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var HourExpression = module.exports = function HourExpression(){
+ */
+var HourExpression = module.exports = function HourExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = HourExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor:{
-			value:klass
-		}
-	});
+}, klass = HourExpression, base = require("./FixedArityExpressionT")(HourExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$hour";
-};
-
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-/**
- * Takes a date and returns the hour between 0 and 23.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
-	var date = this.operands[0].evaluateInternal(vars);
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
 	return date.getUTCHours();
 };
 
+proto.getOpName = function getOpName() {
+	return "$hour";
+};
 
-/** Register Expression */
-Expression.registerExpression("$hour",base.parse);
+Expression.registerExpression("$hour", base.parse);

+ 9 - 24
lib/pipeline/expressions/IfNullExpression.js

@@ -7,39 +7,24 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var IfNullExpression = module.exports = function IfNullExpression() {
-	if (arguments.length !== 0) throw new Error("zero args expected");
+	if (arguments.length !== 0) throw new Error(klass.name + ": expected args: NONE");
 	base.call(this);
-}, klass = IfNullExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = IfNullExpression, base = require("./FixedArityExpressionT")(IfNullExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$ifNull";
-};
-
-// virtuals from ExpressionNary
-
-/**
- * Use the $ifNull operator with the following syntax: { $ifNull: [ <expression>, <replacement-if-null> ] }
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var left = this.operands[0].evaluateInternal(vars);
-	if (left !== undefined && left !== null) return left;
+	if (left !== undefined && left !== null)
+		return left;
 	var right = this.operands[1].evaluateInternal(vars);
 	return right;
 };
 
-/** Register Expression */
 Expression.registerExpression("$ifNull", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$ifNull";
+};

+ 11 - 25
lib/pipeline/expressions/MillisecondExpression.js

@@ -2,41 +2,27 @@
 
 /**
  * An $millisecond pipeline expression.
- * @see evaluateInternal
  * @class MillisecondExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var MillisecondExpression = module.exports = function MillisecondExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = MillisecondExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = MillisecondExpression, base = require("./FixedArityExpressionT")(MillisecondExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$millisecond";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCMilliseconds(); //NOTE: no leap milliseconds in JS - http://code.google.com/p/v8/issues/detail?id=1944
 };
 
-/**
- * Takes a date and returns the millisecond between 0 and 999, but can be 1000 to account for leap milliseconds.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCMilliseconds(); //TODO: incorrect for last millisecond of leap year, need to fix...
-	// currently leap milliseconds are unsupported in v8
-	// http://code.google.com/p/v8/issues/detail?id=1944
+proto.getOpName = function getOpName() {
+	return "$millisecond";
 };
 
-/** Register Expression */
 Expression.registerExpression("$millisecond", base.parse);

+ 11 - 23
lib/pipeline/expressions/MinuteExpression.js

@@ -2,39 +2,27 @@
 
 /**
  * An $minute pipeline expression.
- * @see evaluateInternal
  * @class MinuteExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var MinuteExpression = module.exports = function MinuteExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = MinuteExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = MinuteExpression, base = require("./FixedArityExpressionT")(MinuteExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$minute";
-};
-
-/**
- * Takes a date and returns the minute between 0 and 59.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
 	return date.getUTCMinutes();
 };
 
-/** Register Expression */
+proto.getOpName = function getOpName() {
+	return "$minute";
+};
+
 Expression.registerExpression("$minute", base.parse);

+ 25 - 36
lib/pipeline/expressions/ModExpression.js

@@ -4,51 +4,40 @@
  * An $mod pipeline expression.
  * @see evaluate
  * @class ModExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var ModExpression = module.exports = function ModExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = ModExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
-
-// DEPENDENCIES
+}, klass = ModExpression, base = require("./FixedArityExpressionT")(ModExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$mod";
-};
-
-/**
- * Takes an array that contains a pair of numbers and returns the remainder of the first number divided by the second number.
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	this.checkArgCount(2);
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (left instanceof Date || right instanceof Date) throw new Error("$mod does not support dates; code 16374");
-
-	// pass along jstNULLs and Undefineds
-	if (left === undefined || left === null) return left;
-	if (right === undefined || right === null) return right;
-
-	// ensure we aren't modding by 0
-	right = Value.coerceToDouble(right);
-	if (right === 0) return undefined;
-
-	left = Value.coerceToDouble(left);
-	return left % right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	var leftType = Value.getType(lhs),
+		rightType = Value.getType(rhs);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+		// ensure we aren't modding by 0
+		if(rhs === 0) throw new Error("can't $mod by 0; uassert code 16610");
+
+		return lhs % rhs;
+	} else if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+		return null;
+	} else {
+		throw new Error("$mod only supports numeric types, not " + Value.getType(lhs) + " and " + Value.getType(rhs));
+	}
 };
 
-/** Register Expression */
 Expression.registerExpression("$mod", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$mod";
+};

+ 11 - 23
lib/pipeline/expressions/MonthExpression.js

@@ -2,39 +2,27 @@
 
 /**
  * A $month pipeline expression.
- * @see evaluate
  * @class MonthExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var MonthExpression = module.exports = function MonthExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = MonthExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = MonthExpression, base = require("./FixedArityExpressionT")(MonthExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$month";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCMonth() + 1;
 };
 
-/**
- * Takes a date and returns the month as a number between 1 and 12.
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCMonth();
+proto.getOpName = function getOpName() {
+	return "$month";
 };
 
-/** Register Expression */
 Expression.registerExpression("$month", base.parse);

+ 11 - 24
lib/pipeline/expressions/NotExpression.js

@@ -7,36 +7,23 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var NotExpression = module.exports = function NotExpression() {
-		if (arguments.length !== 0) throw new Error("zero args expected");
+	if (arguments.length !== 0) throw new Error(klass.name + ": expected args: NONE");
 	base.call(this);
-}, klass = NotExpression,
-	base = require("./FixedArityExpressionT")(klass, 1),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = NotExpression, base = require("./FixedArityExpressionT")(NotExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$not";
-proto.getOpName = function getOpName() {
-	return klass.opName;
-};
-
-/**
- * Returns the boolean opposite value passed to it. When passed a true value, $not returns false; when passed a false value, $not returns true.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var op = this.operands[0].evaluateInternal(vars);
-	return !Value.coerceToBool(op);
+	var op = this.operands[0].evaluateInternal(vars),
+		b = Value.coerceToBool(op);
+	return !b;
 };
 
-/** Register Expression */
-Expression.registerExpression(klass.opName, base.parse);
+Expression.registerExpression("$not", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$not";
+};

+ 11 - 25
lib/pipeline/expressions/SecondExpression.js

@@ -2,41 +2,27 @@
 
 /**
  * An $second pipeline expression.
- * @see evaluateInternal
  * @class SecondExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SecondExpression = module.exports = function SecondExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SecondExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SecondExpression, base = require("./FixedArityExpressionT")(SecondExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$second";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCSeconds();  //NOTE: no leap seconds in JS - http://code.google.com/p/v8/issues/detail?id=1944
 };
 
-/**
- * Takes a date and returns the second between 0 and 59, but can be 60 to account for leap seconds.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCSeconds(); //TODO: incorrect for last second of leap year, need to fix...
-	// currently leap seconds are unsupported in v8
-	// http://code.google.com/p/v8/issues/detail?id=1944
+proto.getOpName = function getOpName() {
+	return "$second";
 };
 
-/** Register Expression */
 Expression.registerExpression("$second", base.parse);

+ 9 - 20
lib/pipeline/expressions/SizeExpression.js

@@ -6,36 +6,25 @@
  * @class SizeExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
+ * @extends mungedb-aggregate.pipeline.FixedArityExpressionT
  * @constructor
- **/
+ */
 var SizeExpression = module.exports = function SizeExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": args expected: value");
 	base.call(this);
-}, klass = SizeExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SizeExpression, base = require("./FixedArityExpressionT")(SizeExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$size";
-};
-
-/**
- * Takes an array and return the size.
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var array = this.operands[0].evaluateInternal(vars);
-	if (array instanceof Date) throw new Error("$size does not support dates; code 16376");
+	if (!(array instanceof Array)) throw new Error("The argument to $size must be an Array but was of type" + Value.getType(array) + "; uassert code 16376");
 	return array.length;
 };
 
-/** Register Expression */
 Expression.registerExpression("$size", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$size";
+};

+ 21 - 27
lib/pipeline/expressions/StrcasecmpExpression.js

@@ -7,40 +7,34 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var StrcasecmpExpression = module.exports = function StrcasecmpExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = StrcasecmpExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = StrcasecmpExpression, base = require("./FixedArityExpressionT")(StrcasecmpExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	NaryExpression = require("./NaryExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$strcasecmp";
-};
-
-/**
- * Takes in two strings. Returns a number. $strcasecmp is positive if the first string is “greater than” the second and negative if the first string is “less than” the second. $strcasecmp returns 0 if the strings are identical.
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var val1 = this.operands[0].evaluateInternal(vars),
-		val2 = this.operands[1].evaluateInternal(vars),
-		str1 = Value.coerceToString(val1).toUpperCase(),
-		str2 = Value.coerceToString(val2).toUpperCase(),
-		cmp = Value.compare(str1, str2);
-	return cmp;
+	var string1 = this.operands[0].evaluateInternal(vars),
+		string2 = this.operands[1].evaluateInternal(vars);
+
+	var str1 = Value.coerceToString(string1).toUpperCase(),
+		str2 = Value.coerceToString(string2).toUpperCase(),
+		result = Value.compare(str1, str2);
+
+	if (result === 0) {
+		return 0;
+	} else if (result > 0) {
+		return 1;
+	} else {
+		return -1;
+	}
 };
 
-/** Register Expression */
 Expression.registerExpression("$strcasecmp", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$strcasecmp";
+};

+ 23 - 30
lib/pipeline/expressions/SubstrExpression.js

@@ -7,43 +7,36 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SubstrExpression = module.exports = function SubstrExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SubstrExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 3),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SubstrExpression, base = require("./FixedArityExpressionT")(klass, 3), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$substr";
-};
-
-/**
- * Takes a string and two numbers. The first number represents the number of bytes in the string to skip, and the second number specifies the number of bytes to return from the string.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var val = this.operands[0].evaluateInternal(vars),
-		idx = this.operands[1].evaluateInternal(vars),
-		len = this.operands[2].evaluateInternal(vars),
-		str = Value.coerceToString(val);
-	if (typeof(idx) != "number") throw new Error(this.getOpName() + ": starting index must be a numeric type; code 16034");
-	if (typeof(len) != "number") throw new Error(this.getOpName() + ": length must be a numeric type; code 16035");
-	if (idx >= str.length) return "";
-	//TODO: Need to handle -1
-	len = (len === -1 ? undefined : len);
-	return str.substr(idx, len);
+	var string = this.operands[0].evaluateInternal(vars),
+		pLower = this.operands[1].evaluateInternal(vars),
+		pLength = this.operands[2].evaluateInternal(vars);
+
+	var str = Value.coerceToString(string);
+	if (typeof pLower !== "number") throw new Error(this.getOpName() + ":  starting index must be a numeric type (is type " + Value.getType(pLower) + "); uassert code 16034");
+	if (typeof pLength !== "number") throw new Error(this.getOpName() + ":  length must be a numeric type (is type " + Value.getType(pLength) + "); uassert code 16035");
+
+	var lower = Value.coerceToLong(pLower),
+		length = Value.coerceToLong(pLength);
+	if (lower >= str.length) {
+		// If lower > str.length() then string::substr() will throw out_of_range, so return an
+		// empty string if lower is not a valid string index.
+		return "";
+	}
+	return str.substr(lower, length);
 };
 
-/** Register Expression */
 Expression.registerExpression("$substr", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$substr";
+};

+ 35 - 24
lib/pipeline/expressions/SubtractExpression.js

@@ -4,39 +4,50 @@
  * A $subtract pipeline expression.
  * @see evaluateInternal
  * @class SubtractExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var SubtractExpression = module.exports = function SubtractExpression(){
+ */
+var SubtractExpression = module.exports = function SubtractExpression() {
 	base.call(this);
-}, klass = SubtractExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SubtractExpression, base = require("./FixedArityExpressionT")(SubtractExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$subtract";
-};
-
-/**
-* Takes an array that contains a pair of numbers and subtracts the second from the first, returning their difference.
-**/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (left instanceof Date || right instanceof Date) throw new Error("$subtract does not support dates; code 16376");
-	return left - right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+		return lhs - rhs;
+	} else if (lhs === null || lhs === undefined || rhs === null || rhs === undefined) {
+		return null;
+	} else if (lhs instanceof Date) {
+		if (rhs instanceof Date) {
+			var timeDelta = lhs - rhs;
+			return timeDelta;
+		} else if (typeof rhs === "number") {
+			var millisSinceEpoch = lhs - Value.coerceToLong(rhs);
+			return millisSinceEpoch;
+		} else {
+			throw new Error("can't $subtract a " +
+				Value.getType(rhs) +
+				" from a Date" +
+				"; uassert code 16613");
+		}
+	} else {
+		throw new Error("can't $subtract a " +
+			Value.getType(rhs) +
+			" from a " +
+			Value.getType(lhs) +
+			"; uassert code 16556");
+	}
 };
 
-/** Register Expression */
 Expression.registerExpression("$subtract", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$subtract";
+};

+ 13 - 30
lib/pipeline/expressions/WeekExpression.js

@@ -2,49 +2,32 @@
 
 /**
  * A $week pipeline expression.
- * @see evaluateInternal
  * @class WeekExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var WeekExpression = module.exports = function WeekExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = WeekExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = WeekExpression, base = require("./FixedArityExpressionT")(WeekExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	DayOfYearExpression = require("./DayOfYearExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$week";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate),
+		//NOTE: DEVIATION FROM MONGO: our calculations are a little different but are equivalent
+		y11 = new Date(date.getUTCFullYear(), 0, 1), // same year, first month, first day; time omitted
+		ymd = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + 1), // same y,m,d; time omitted, add 1 because days start at 1
+		yday = Math.ceil((ymd - y11) / 86400000); // count days
+	return (yday / 7) | 0;
 };
 
-/**
- * Takes a date and returns the week of the year as a number between 0 and 53.
- * Weeks begin on Sundays, and week 1 begins with the first Sunday of the year.
- * Days preceding the first Sunday of the year are in week 0.
- * This behavior is the same as the “%U” operator to the strftime standard library function.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars),
-		dayOfWeek = date.getUTCDay(),
-		dayOfYear = DayOfYearExpression.getDateDayOfYear(date),
-		prevSundayDayOfYear = dayOfYear - dayOfWeek, // may be negative
-		nextSundayDayOfYear = prevSundayDayOfYear + 7; // must be positive
-	// Return the zero based index of the week of the next sunday, equal to the one based index of the week of the previous sunday, which is to be returned.
-	return (nextSundayDayOfYear / 7) | 0; // also, the `| 0` here truncates this so that we return an integer
+proto.getOpName = function getOpName() {
+	return "$week";
 };
 
-/** Register Expression */
 Expression.registerExpression("$week", base.parse);

+ 8 - 23
lib/pipeline/expressions/YearExpression.js

@@ -2,42 +2,27 @@
 
 /**
  * A $year pipeline expression.
- * @see evaluateInternal
  * @class YearExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var YearExpression = module.exports = function YearExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = YearExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = YearExpression, base = require("./FixedArityExpressionT")(YearExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	DayOfYearExpression = require("./DayOfYearExpression"),
 	Expression = require("./Expression");
 
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCFullYear();
+};
 
-// PROTOTYPE MEMBERS
 proto.getOpName = function getOpName() {
 	return "$year";
 };
 
-/**
- * Takes a date and returns the full year.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCFullYear();
-};
-
-/** Register Expression */
 Expression.registerExpression("$year", base.parse);

+ 148 - 49
test/lib/pipeline/expressions/AllElementsTrueExpression.js

@@ -1,71 +1,170 @@
 "use strict";
 var assert = require("assert"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	AllElementsTrueExpression = require("../../../../lib/pipeline/expressions/AllElementsTrueExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
-var allElementsTrueExpression = new AllElementsTrueExpression();
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var ExpectedResultBase = (function() {
+	var klass = function ExpectedResultBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	}, proto = klass.prototype;
+	proto.run = function() {
+		var spec = this.getSpec,
+			args = spec.input;
+		if (spec.expected !== undefined && spec.expected !== null) {
+			var fields = spec.expected;
+			for (var fieldFirst in fields) {
+				var fieldSecond = fields[fieldFirst],
+					expected = fieldSecond;
+					// obj = {<fieldFirst>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					expr = Expression.parseExpression(fieldFirst, args, vps),
+					result = expr.evaluate({}),
+					errMsg = "for expression " + fieldFirst +
+							" with argument " + JSON.stringify(args) +
+							" full tree " + JSON.stringify(expr.serialize(false)) +
+							" expected: " + JSON.stringify(expected) +
+							" but got: " + JSON.stringify(result);
+				assert.deepEqual(result, expected, errMsg);
+				//TODO test optimize here
+			}
+		}
+		if (spec.error !== undefined && spec.error !== null) {
+			var asserters = spec.error,
+				n = asserters.length;
+			for (var i = 0; i < n; ++i) {
+                // var obj2 = {<asserters[i]>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator2 = new VariablesIdGenerator(),
+					vps2 = new VariablesParseState(idGenerator2);
+				assert.throws(function() {
+					// NOTE: parse and evaluatation failures are treated the same
+					expr = Expression.parseExpression(asserters[i], args, vps2);
+					expr.evaluate({});
+				}); // jshint ignore:line
+			}
+		}
+	};
+	return klass;
+})();
+
+exports.AllElementsTrueExpression = {
 
-module.exports = {
+	"constructor()": {
 
-	"AllElementsTrueExpression": {
+		"should construct instance": function() {
+			assert(new AllElementsTrueExpression() instanceof AllElementsTrueExpression);
+			assert(new AllElementsTrueExpression() instanceof Expression);
+		},
 
-		"constructor()": {
+		"should error if given args": function() {
+			assert.throws(function() {
+				new AllElementsTrueExpression("bad stuff");
+			});
+		},
 
-			"should not throw Error when constructing without args": function testConstructor() {
-				assert.doesNotThrow(function() {
-					new AllElementsTrueExpression();
-				});
-			}
+	},
 
+	"#getOpName()": {
+
+		"should return the correct op name; $allElementsTrue": function() {
+			assert.equal(new AllElementsTrueExpression().getOpName(), "$allElementsTrue");
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $allElements": function testOpName() {
-				assert.equal(new AllElementsTrueExpression().getOpName(), "$allElementsTrue");
-			}
+	"#evaluate()": {
 
+		"should return false if just false": function JustFalse() {
+            new ExpectedResultBase({
+				getSpec: {
+					input: [[false]],
+					expected: {
+						$allElementsTrue: false,
+						// $anyElementTrue: false,
+					}
+				}
+			}).run();
 		},
 
-		"#evaluateInternal()": {
+		"should return true if just true": function JustTrue() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"should return error if parameter is empty:": function testEmpty() {
-				assert.throws(function() {
-					allElementsTrueExpression.evaluateInternal("asdf");
-				});
-			},
+		"should return false if one true and one false": function OneTrueOneFalse() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true, false]],
+					expected: {
+						$allElementsTrue: false,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"should return error if parameter is not an array": function testNonArray() {
-				assert.throws(function() {
-					allElementsTrueExpression.evaluateInternal("This is not an array");
-				});
-			},
-
-			"should return false if first element is false; [false, true, true true]": function testFirstFalse() {
-				assert.equal(allElementsTrueExpression.evaluateInternal(
-					Expression.parseOperand({
-						$allElementsTrue: [false, true, true, true]
-					}).evaluate()), false);
-			},
-
-			"should return false if last element is false; [true, true, true, false]": function testLastFalse() {
-				assert.equal(allElementsTrueExpression.evaluateInternal(
-					Expression.parseOperand({
-						$allElementsTrue: [true, true, true, false]
-					}).evaluate()), false);
-			},
-
-			"should return true if all elements are true; [true,true,true,true]": function testAllTrue() {
-				assert.equal(allElementsTrueExpression.evaluateInternal(
-					Expression.parseOperand({
-						$allElementsTrue: [true, true, true, true]
-					}).evaluate()), true);
-			},
+		"should return true if empty": function Empty() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-		}
+		"should return true if truthy int": function TrueViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-	}
+		"should return false if falsy int": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[0]],
+					expected: {
+						$allElementsTrue: false,
+						// $anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-};
+		"should error if given null instead of array": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null],
+					error: [
+						"$allElementsTrue",
+						// "$anyElementTrue",
+					]
+				}
+			}).run();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+	},
+
+};

+ 142 - 96
test/lib/pipeline/expressions/AnyElementTrueExpression.js

@@ -5,120 +5,166 @@ var assert = require("assert"),
 	AnyElementTrueExpression = require("../../../../lib/pipeline/expressions/AnyElementTrueExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
-var anyElementTrueExpression = new AnyElementTrueExpression();
-
-function errMsg(expr, args, tree, expected, result) {
-	return 	"for expression " + expr +
-			" with argument " + args +
-			" full tree: " + JSON.stringify(tree) +
-			" expected: " + expected +
-			" result: " + result;
-}
-
-module.exports = {
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var ExpectedResultBase = (function() {
+	var klass = function ExpectedResultBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	}, proto = klass.prototype;
+	proto.run = function() {
+		var spec = this.getSpec,
+			args = spec.input;
+		if (spec.expected !== undefined && spec.expected !== null) {
+			var fields = spec.expected;
+			for (var fieldFirst in fields) {
+				var fieldSecond = fields[fieldFirst],
+					expected = fieldSecond;
+					// obj = {<fieldFirst>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					expr = Expression.parseExpression(fieldFirst, args, vps),
+					result = expr.evaluate({}),
+					errMsg = "for expression " + fieldFirst +
+							" with argument " + JSON.stringify(args) +
+							" full tree " + JSON.stringify(expr.serialize(false)) +
+							" expected: " + JSON.stringify(expected) +
+							" but got: " + JSON.stringify(result);
+				assert.deepEqual(result, expected, errMsg);
+				//TODO test optimize here
+			}
+		}
+		if (spec.error !== undefined && spec.error !== null) {
+			var asserters = spec.error,
+				n = asserters.length;
+			for (var i = 0; i < n; ++i) {
+				// var obj2 = {<asserters[i]>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator2 = new VariablesIdGenerator(),
+					vps2 = new VariablesParseState(idGenerator2);
+				assert.throws(function() {
+					// NOTE: parse and evaluatation failures are treated the same
+					expr = Expression.parseExpression(asserters[i], args, vps2);
+					expr.evaluate({});
+				}); // jshint ignore:line
+			}
+		}
+	};
+	return klass;
+})();
 
-	"AnyElementTrueExpression": {
+exports.AnyElementTrueExpression = {
 
-		"constructor()": {
+	"constructor()": {
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AnyElementTrueExpression();
-				});
-			}
+		"should construct instance": function() {
+			assert(new AnyElementTrueExpression() instanceof AnyElementTrueExpression);
+			assert(new AnyElementTrueExpression() instanceof Expression);
+		},
 
+		"should error if given args": function() {
+			assert.throws(function() {
+				new AnyElementTrueExpression("bad stuff");
+			});
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $anyElement": function testOpName(){
-				assert.equal(new AnyElementTrueExpression().getOpName(), "$anyElementTrue");
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $anyElementTrue": function() {
+			assert.equal(new AnyElementTrueExpression().getOpName(), "$anyElementTrue");
 		},
 
-		"integration": {
+	},
 
-			"JustFalse": function JustFalse(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[false]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = false,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+	"#evaluate()": {
 
-			"JustTrue": function JustTrue(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[true]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = true,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return false if just false": function JustFalse() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[false]],
+					expected: {
+						// $allElementsTrue: false,
+						$anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-			"OneTrueOneFalse": function OneTrueOneFalse(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[true, false]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = true,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return true if just true": function JustTrue() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true]],
+					expected: {
+						// $allElementsTrue: true,
+						$anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"Empty": function Empty(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = false,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return false if one true and one false": function OneTrueOneFalse() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true, false]],
+					expected: {
+						// $allElementsTrue: false,
+						$anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"TrueViaInt": function TrueViaInt(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[1]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = true,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return true if empty": function Empty() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						// $allElementsTrue: true,
+						$anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-			"FalseViaInt": function FalseViaInt(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[0]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = false,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return true if truthy int": function TrueViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1]],
+					expected: {
+						// $allElementsTrue: true,
+						$anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"Null": function FalseViaInt(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [null],
-					expr = Expression.parseExpression("$anyElementTrue", input);
-				assert.throws(function() {
-					var result = expr.evaluate({});
-				});
-			}
+		"should return false if falsy int": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[0]],
+					expected: {
+						// $allElementsTrue: false,
+						$anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-		}
+		"should error if given null instead of array": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null],
+					error: [
+						// "$allElementsTrue",
+						"$anyElementTrue",
+					]
+				}
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 409 - 308
test/lib/pipeline/expressions/CompareExpression.js

@@ -1,367 +1,468 @@
 "use strict";
 var assert = require("assert"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	pipeline = require("../../../../lib/pipeline"),
+	expressions = pipeline.expressions,
+	Expression = expressions.Expression,
 	CompareExpression = require("../../../../lib/pipeline/expressions/CompareExpression"),
-	FieldRangeExpression = require("../../../../lib/pipeline/expressions/FieldRangeExpression"),
 	VariablesParseState = require("../../../../Lib/pipeline/expressions/VariablesParseState"),
 	VariablesIdGenerator = require("../../../../Lib/pipeline/expressions/VariablesIdGenerator"),
-	ConstantExpression = require("../../../../Lib/pipeline/expressions/ConstantExpression");
-
-module.exports = {
-
-	"CompareExpression": {
-
-		"constructor()": {
-
-			"should throw Error if no args": function testConstructor() {
-				assert.throws(function() {
-					new CompareExpression();
-				});
-			}
-
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	OptimizeBase = (function() {
+		var klass = function OptimizeBase() {
+				base.apply(this, arguments);
+			},
+			base = TestBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expression = Expression.parseOperand(specElement, vps),
+				optimized = expression.optimize();
+			assert.deepEqual(constify(this.expectedOptimized()), expressionToJson(optimized));
+		};
+		return klass;
+	})(),
+	FieldRangeOptimize = (function() {
+		var klass = function FieldRangeOptimize() {
+				base.apply(this, arguments);
+			},
+			base = OptimizeBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedOptimized = function(){
+			return this.spec;
+		};
+		return klass;
+	})(),
+	NoOptimize = (function() {
+		var klass = function NoOptimize() {
+				base.apply(this, arguments);
+			},
+			base = OptimizeBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedOptimized = function(){
+			return this.spec;
+		};
+		return klass;
+	})(),
+	ExpectedResultBase = (function() {
+		/** Check expected result for expressions depending on constants. */
+		var klass = function ExpectedResultBase() {
+				base.apply(this, arguments);
+			},
+			base = OptimizeBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			base.prototype.run.call(this);
+			var specElement = this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expression = Expression.parseOperand(specElement, vps);
+			// Check expression spec round trip.
+			assert.deepEqual(expressionToJson(expression), constify(specElement));
+			// Check evaluation result.
+			assert.strictEqual(expression.evaluate({}), this.expectedResult);
+			// Check that the result is the same after optimizing.
+			var optimized = expression.optimize();
+			assert.strictEqual(optimized.evaluate({}), this.expectedResult);
+		};
+		proto.expectedOptimized = function() {
+			return {$const:this.expectedResult};
+		};
+		return klass;
+	})(),
+	ExpectedTrue = (function(){
+		var klass = function ExpectedTrue() {
+				base.apply(this, arguments);
+			},
+			base = ExpectedResultBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedResult = true;
+		return klass;
+	})(),
+	ExpectedFalse = (function(){
+		var klass = function ExpectedFalse() {
+				base.apply(this, arguments);
+			},
+			base = ExpectedResultBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedResult = false;
+		return klass;
+	})(),
+	ParseError = (function(){
+		var klass = function ParseError() {
+				base.apply(this, arguments);
+			},
+			base = TestBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator);
+			assert.throws(function() {
+				Expression.parseOperand(specElement, vps);
+			});
+		};
+		return klass;
+	})();
+
+exports.CompareExpression = {
+
+	"constructor()": {
+
+		"should throw Error if no args": function() {
+			assert.throws(function() {
+				new CompareExpression();
+			});
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $eq, $ne, $gt, $gte, $lt, $lte, $cmp": function testOpName() {
-				assert.equal((new CompareExpression(CompareExpression.EQ)).getOpName(), "$eq");
-				assert.equal((new CompareExpression(CompareExpression.NE)).getOpName(), "$ne");
-				assert.equal((new CompareExpression(CompareExpression.GT)).getOpName(), "$gt");
-				assert.equal((new CompareExpression(CompareExpression.GTE)).getOpName(), "$gte");
-				assert.equal((new CompareExpression(CompareExpression.LT)).getOpName(), "$lt");
-				assert.equal((new CompareExpression(CompareExpression.LTE)).getOpName(), "$lte");
-				assert.equal((new CompareExpression(CompareExpression.CMP)).getOpName(), "$cmp");
-			}
-
+		"should throw if more than 1 args": function() {
+			assert.throws(function() {
+				new CompareExpression(1,2);
+			});
 		},
 
-		"#evaluateInternal()": {
-
-			"$eq": {
-
-				"should return false if first < second; {$eq:[1,2]}": function testEqLt() {
-					//debugger;
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					var parseOp = Expression.parseOperand({
-						$eq: [{
-							$const: 1
-						}, {
-							$const: 2
-						}]
-					}, vps);
-					var result = parseOp.evaluateInternal({});
-
-					//assert.equal(new CompareExpression( CompareExpression.EQ).evaluateInternal({"$eq":[1,2]}), false);
-					assert.equal(result, false);
-
-				},
-
-				"should return true if first == second; {$eq:[1,1]}": function testEqEq() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-
-					assert.equal(Expression.parseOperand({
-						$eq: [1, 1]
-					}, vps).evaluateInternal({}), true);
-				},
-
-				"should return false if first > second {$eq:[1,0]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [1, 0]
-					}).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different types {$eq:[null,0]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [null, 0]
-					}, vps).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different types {$eq:[undefined,0]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [undefined, 0]
-					}, vps).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different arrays {$eq:[[1],[null]]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [
-							[1],
-							[null]
-						]
-					}, vps).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different arrays {$eq:[[1],[]]}": function testEqGt() {
-					assert.equal(Expression.parseOperand({
-						$eq: [
-							[1],
-							[]
-						]
-					}, vps).evaluateInternal({}), false);
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-				},
-
-				"should return true if first and second are the same arrays {$eq:[[1],[1]]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [
-							[1],
-							[1]
-						]
-					}, vps).evaluateInternal({}), true);
-				}
-			},
-
-			//      "$ne": {
-
-			//              "should return true if first < second; {$ne:[1,2]}": function testNeLt(){
-			//	      assert.equal(Expression.parseOperand({$ne:[1,2]}).evaluateInternal({}), true);
-			//              },
-
-			//              "should return false if first == second; {$ne:[1,1]}": function testNeLt(){
-			//	      assert.equal(Expression.parseOperand({$ne:[1,1]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return true if first > second; {$ne:[1,0]}": function testNeGt(){
-			//	      assert.equal(Expression.parseOperand({$ne:[1,0]}).evaluateInternal({}), true);
-			//              }
-
-			//      },
-
-			//      "$gt": {
-
-			//              "should return false if first < second; {$gt:[1,2]}": function testGtLt(){
-			//	      assert.equal(Expression.parseOperand({$gt:[1,2]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return false if first == second; {$gt:[1,1]}": function testGtLt(){
-			//	      assert.equal(Expression.parseOperand({$gt:[1,1]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return true if first > second; {$gt:[1,0]}": function testGtGt(){
-			//	      assert.equal(Expression.parseOperand({$gt:[1,0]}).evaluateInternal({}), true);
-			//              }
-
-			//      },
-
-			//      "$gte": {
-
-			//              "should return false if first < second; {$gte:[1,2]}": function testGteLt(){
-			//	      assert.equal(Expression.parseOperand({$gte:[1,2]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return true if first == second; {$gte:[1,1]}": function testGteLt(){
-			//	      assert.equal(Expression.parseOperand({$gte:[1,1]}).evaluateInternal({}), true);
-			//              },
-
-			//              "should return true if first > second; {$gte:[1,0]}": function testGteGt(){
-			//	      assert.equal(Expression.parseOperand({$gte:[1,0]}).evaluateInternal({}), true);
-			//              }
-
-			//      },
-
-			//      "$lt": {
-
-			//              "should return true if first < second; {$lt:[1,2]}": function testLtLt(){
-			//	      assert.equal(Expression.parseOperand({$lt:[1,2]}).evaluateInternal({}), true);
-			//              },
-
-			//              "should return false if first == second; {$lt:[1,1]}": function testLtLt(){
-			//	      assert.equal(Expression.parseOperand({$lt:[1,1]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return false if first > second; {$lt:[1,0]}": function testLtGt(){
-			//	      assert.equal(Expression.parseOperand({$lt:[1,0]}).evaluateInternal({}), false);
-			//              }
-
-			//      },
+		"should not throw if 1 arg and arg is string": function() {
+			assert.doesNotThrow(function() {
+				new CompareExpression("a");
+			});
+		},
 
-			//      "$lte": {
+	},
 
-			//              "should return true if first < second; {$lte:[1,2]}": function testLteLt(){
-			//	      assert.equal(Expression.parseOperand({$lte:[1,2]}).evaluateInternal({}), true);
-			//              },
+	"#getOpName()": {
 
-			//              "should return true if first == second; {$lte:[1,1]}": function testLteLt(){
-			//	      assert.equal(Expression.parseOperand({$lte:[1,1]}).evaluateInternal({}), true);
-			//              },
+		"should return the correct op name; $eq, $ne, $gt, $gte, $lt, $lte, $cmp": function() {
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.EQ).getOpName(), "$eq");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.NE).getOpName(), "$ne");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.GT).getOpName(), "$gt");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.GTE).getOpName(), "$gte");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.LT).getOpName(), "$lt");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.LTE).getOpName(), "$lte");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.CMP).getOpName(), "$cmp");
+		},
 
-			//              "should return false if first > second; {$lte:[1,0]}": function testLteGt(){
-			//	      assert.equal(Expression.parseOperand({$lte:[1,0]}).evaluateInternal({}), false);
-			//              }
+	},
 
-			//      },
+	"#evaluate()": {
 
-			//      "$cmp": {
+		/** $eq with first < second. */
+		"EqLt": function EqLt() {
+			new ExpectedFalse({
+				spec: {$eq:[1,2]},
+			}).run();
+		},
 
-			//              "should return -1 if first < second; {$cmp:[1,2]}": function testCmpLt(){
-			//	      assert.equal(Expression.parseOperand({$cmp:[1,2]}).evaluateInternal({}), -1);
-			//              },
+		/** $eq with first == second. */
+		"EqEq": function EqEq() {
+			new ExpectedTrue({
+				spec: {$eq:[1,1]},
+			}).run();
+		},
 
-			//              "should return 0 if first < second; {$cmp:[1,1]}": function testCmpLt(){
-			//	      assert.equal(Expression.parseOperand({$cmp:[1,1]}).evaluateInternal({}), 0);
-			//              },
+		/** $eq with first > second. */
+		"EqGt": function EqEq() {
+			new ExpectedFalse({
+				spec: {$eq:[1,0]},
+			}).run();
+		},
 
-			//              "should return 1 if first < second; {$cmp:[1,0]}": function testCmpLt(){
-			//	      assert.equal(Expression.parseOperand({$cmp:[1,0]}).evaluateInternal({}), 1);
-			//              },
+		/** $ne with first < second. */
+		"NeLt": function NeLt() {
+			new ExpectedTrue({
+				spec: {$ne:[1,2]},
+			}).run();
+		},
 
-			//              "should return 1 even if comparison is larger; {$cmp:['z','a']}": function testCmpBracketed(){
-			//	      assert.equal(Expression.parseOperand({$cmp:['z','a']}).evaluateInternal({}), 1);
-			//              }
+		/** $ne with first == second. */
+		"NeEq": function NeEq() {
+			new ExpectedFalse({
+				spec: {$ne:[1,1]},
+			}).run();
+		},
 
-			//      },
+		/** $ne with first > second. */
+		"NeGt": function NeGt() {
+			new ExpectedTrue({
+				spec: {$ne:[1,0]},
+			}).run();
+		},
 
-			//      "should throw Error": {
+		/** $gt with first < second. */
+		"GtLt": function GtLt() {
+			new ExpectedFalse({
+				spec: {$gt:[1,2]},
+			}).run();
+		},
 
-			//              "if zero operands are provided; {$ne:[]}": function testZeroOperands(){
-			//	      assert.throws(function(){
-			//	              Expression.parseOperand({$ne:[]}).evaluateInternal({});
-			//	      });
-			//              },
+		/** $gt with first == second. */
+		"GtEq": function GtEq() {
+			new ExpectedFalse({
+				spec: {$gt:[1,1]},
+			}).run();
+		},
 
-			//              "if one operand is provided; {$eq:[1]}": function testOneOperand(){
-			//	      assert.throws(function(){
-			//	              Expression.parseOperand({$eq:[1]}).evaluateInternal({});
-			//	      });
-			//              },
+		/** $gt with first > second. */
+		"GtGt": function GtGt() {
+			new ExpectedTrue({
+				spec: {$gt:[1,0]},
+			}).run();
+		},
 
-			//              "if three operands are provided; {$gt:[2,3,4]}": function testThreeOperands(){
-			//	      assert.throws(function(){
-			//	              Expression.parseOperand({$gt:[2,3,4]}).evaluateInternal({});
-			//	      });
-			//              }
-			//      }
+		/** $gte with first < second. */
+		"GteLt": function GteLt() {
+			new ExpectedFalse({
+				spec: {$gte:[1,2]},
+			}).run();
+		},
 
-			// },
+		/** $gte with first == second. */
+		"GteEq": function GteEq() {
+			new ExpectedTrue({
+				spec: {$gte:[1,1]},
+			}).run();
+		},
 
-			// "#optimize()": {
+		/** $gte with first > second. */
+		"GteGt": function GteGt() {
+			new ExpectedTrue({
+				spec: {$gte:[1,0]},
+			}).run();
+		},
 
-			//      "should optimize constants; {$eq:[1,1]}": function testOptimizeConstants(){
-			//              assert.deepEqual(Expression.parseOperand({$eq:[1,1]}).optimize().toJSON(true), {$const:true});
-			//      },
+		/** $gte with first > second. */
+		"LtLt": function LtLt() {
+			new ExpectedTrue({
+				spec: {$lt:[1,2]},
+			}).run();
+		},
 
-			//      "should not optimize if $cmp op; {$cmp:[1,'$a']}": function testNoOptimizeCmp(){
-			//              assert.deepEqual(Expression.parseOperand({$cmp:[1,'$a']}).optimize().toJSON(), {$cmp:[1,'$a']});
-			//      },
+		/** $lt with first == second. */
+		"LtEq": function LtEq() {
+			new ExpectedFalse({
+				spec: {$lt:[1,1]},
+			}).run();
+		},
 
-			//      "should not optimize if $ne op; {$ne:[1,'$a']}": function testNoOptimizeNe(){
-			//              assert.deepEqual(Expression.parseOperand({$ne:[1,'$a']}).optimize().toJSON(), {$ne:[1,'$a']});
-			//      },
+		/** $lt with first > second. */
+		"LtGt": function LtGt() {
+			new ExpectedFalse({
+				spec: {$lt:[1,0]},
+			}).run();
+		},
 
-			//      "should not optimize if no constants; {$ne:['$a','$b']}": function testNoOptimizeNoConstant(){
-			//              assert.deepEqual(Expression.parseOperand({$ne:['$a','$b']}).optimize().toJSON(), {$ne:['$a','$b']});
-			//      },
+		/** $lte with first < second. */
+		"LteLt": function LteLt() {
+			new ExpectedTrue({
+				spec: {$lte:[1,2]},
+			}).run();
+		},
 
-			//      "should not optimize without an immediate field path;": {
+		/** $lte with first == second. */
+		"LteEq": function LteEq() {
+			new ExpectedTrue({
+				spec: {$lte:[1,1]},
+			}).run();
+		},
 
-			//              "{$eq:[{$and:['$a']},1]}": function testNoOptimizeWithoutFieldPath(){
-			//	      assert.deepEqual(Expression.parseOperand({$eq:[{$and:['$a']},1]}).optimize().toJSON(), {$eq:[{$and:['$a']},1]});
-			//              },
+		/** $lte with first > second. */
+		"LteGt": function LteGt() {
+			new ExpectedFalse({
+				spec: {$lte:[1,0]},
+			}).run();
+		},
 
-			//              "(reversed); {$eq:[1,{$and:['$a']}]}": function testNoOptimizeWithoutFieldPathReverse(){
-			//	      assert.deepEqual(Expression.parseOperand({$eq:[1,{$and:['$a']}]}).optimize().toJSON(), {$eq:[1,{$and:['$a']}]});
-			//              }
+		/** $cmp with first < second. */
+		"CmpLt": function CmpLt() {
+			new ExpectedResultBase({
+				spec: {$cmp:[1,2]},
+				expectedResult: -1,
+			}).run();
+		},
 
-			//      },
+		/** $cmp with first == second. */
+		"CmpEq": function CmpEq() {
+			new ExpectedResultBase({
+				spec: {$cmp:[1,1]},
+				expectedResult: 0,
+			}).run();
+		},
 
-			//      "should optimize $eq expressions;": {
+		/** $cmp with first > second. */
+		"CmpGt": function CmpGt() {
+			new ExpectedResultBase({
+				spec: {$cmp:[1,0]},
+				expectedResult: 1,
+			}).run();
+		},
 
-			//              "{$eq:['$a',1]}": function testOptimizeEq(){
-			//	      var expr = Expression.parseOperand({$eq:['$a',1]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$eq:['$a',1]});
-			//              },
+		/** $cmp results are bracketed to an absolute value of 1. */
+		"CmpBracketed": function CmpBracketed() {
+			var test = new ExpectedResultBase({
+				spec: {$cmp:["z","a"]},
+				expectedResult: 1,
+			}).run();
+		},
 
-			//              "{$eq:[1,'$a']} (reversed)": function testOptimizeEqReverse(){
-			//	      var expr = Expression.parseOperand({$eq:[1,'$a']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$eq:['$a',1]});
-			//              }
+		/** Zero operands provided. */
+		"ZeroOperands": function ZeroOperands() {
+			new ParseError({
+				spec: {$ne:[]},
+			}).run();
+		},
 
-			//      },
+		/** One operands provided. */
+		"OneOperand": function OneOperand() {
+			new ParseError({
+				spec: {$eq:[1]},
+			}).run();
+		},
 
-			//      "should optimize $lt expressions;": {
+        /** Incompatible types can be compared. */
+		"IncompatibleTypes": function IncompatibleTypes() {
+			var specElement = {$ne:["a",1]},
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(expr.evaluate({}), true);
+		},
 
-			//              "{$lt:['$a',1]}": function testOptimizeLt(){
-			//	      var expr = Expression.parseOperand({$lt:['$a',1]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lt:['$a',1]});
-			//              },
+		/** Three operands provided. */
+		"ThreeOperands": function ThreeOperands() {
+			new ParseError({
+				spec: {$gt:[2,3,4]},
+			}).run();
+		},
 
-			//              "{$lt:[1,'$a']} (reversed)": function testOptimizeLtReverse(){
-			//	      var expr = Expression.parseOperand({$lt:[1,'$a']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gt:['$a',1]});
-			//              }
+		/**
+		 * An expression depending on constants is optimized to a constant via
+		 * ExpressionNary::optimize().
+		 */
+		"OptimizeConstants": function OptimizeConstants() {
+			new OptimizeBase({
+				spec: {$eq:[1,1]},
+				expectedOptimized: function() {
+					return {$const: true};
+				},
+			}).run();
+		},
 
-			//      },
+		/** $cmp is not optimized. */
+		"NoOptimizeCmp": function NoOptimizeCmp() {
+			new NoOptimize({
+				spec: {$cmp:[1,"$a"]},
+			}).run();
+		},
 
-			//      "should optimize $lte expressions;": {
+		/** $ne is not optimized. */
+		"NoOptimizeNe": function NoOptimizeNe() {
+			new NoOptimize({
+				spec: {$ne:[1,"$a"]},
+			}).run();
+		},
 
-			//              "{$lte:['$b',2]}": function testOptimizeLte(){
-			//	      var expr = Expression.parseOperand({$lte:['$b',2]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lte:['$b',2]});
-			//              },
+		/** No optimization is performend without a constant. */
+		"NoOptimizeNoConstant": function NoOptimizeNoConstant() {
+			new NoOptimize({
+				spec: {$ne:["$a", "$b"]},
+			}).run();
+		},
 
-			//              "{$lte:[2,'$b']} (reversed)": function testOptimizeLteReverse(){
-			//	      var expr = Expression.parseOperand({$lte:[2,'$b']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gte:['$b',2]});
-			//              }
+		/** No optimization is performend without an immediate field path. */
+		"NoOptimizeWithoutFieldPath": function NoOptimizeWithoutFieldPath() {
+			new NoOptimize({
+				spec: {$eq:[{$and:["$a"]},1]},
+			}).run();
+		},
 
-			//      },
+		/** No optimization is performend without an immediate field path. */
+		"NoOptimizeWithoutFieldPathReverse": function NoOptimizeWithoutFieldPathReverse() {
+			new NoOptimize({
+				spec: {$eq:[1,{$and:["$a"]}]},
+			}).run();
+		},
 
-			//      "should optimize $gt expressions;": {
+		/** An equality expression is optimized. */
+		"OptimizeEq": function OptimizeEq() {
+			new FieldRangeOptimize({
+				spec: {$eq:["$a",1]},
+			}).run();
+		},
 
-			//              "{$gt:['$b',2]}": function testOptimizeGt(){
-			//	      var expr = Expression.parseOperand({$gt:['$b',2]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gt:['$b',2]});
-			//              },
+		/** A reverse sense equality expression is optimized. */
+		"OptimizeEqReverse": function OptimizeEqReverse() {
+			new FieldRangeOptimize({
+				spec: {$eq:[1,"$a"]},
+			}).run();
+		},
 
-			//              "{$gt:[2,'$b']} (reversed)": function testOptimizeGtReverse(){
-			//	      var expr = Expression.parseOperand({$gt:[2,'$b']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lt:['$b',2]});
-			//              }
+		/** A $lt expression is optimized. */
+		"OptimizeLt": function OptimizeLt() {
+			new FieldRangeOptimize({
+				spec: {$lt:["$a",1]},
+			}).run();
+		},
 
-			//      },
+		/** A reverse sense $lt expression is optimized. */
+		"OptimizeLtReverse": function OptimizeLtReverse() {
+			new FieldRangeOptimize({
+				spec: {$lt:[1,"$a"]},
+			}).run();
+		},
 
-			//      "should optimize $gte expressions;": {
+		/** A $lte expression is optimized. */
+		"OptimizeLte": function OptimizeLte() {
+			new FieldRangeOptimize({
+				spec: {$lte:["$b",2]},
+			}).run();
+		},
 
-			//              "{$gte:['$b',2]}": function testOptimizeGte(){
-			//	      var expr = Expression.parseOperand({$gte:['$b',2]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gte:['$b',2]});
-			//              },
+		/** A reverse sense $lte expression is optimized. */
+		"OptimizeLteReverse": function OptimizeLteReverse() {
+			new FieldRangeOptimize({
+				spec: {$lte:[2,"$b"]},
+			}).run();
+		},
 
-			//              "{$gte:[2,'$b']} (reversed)": function testOptimizeGteReverse(){
-			//	      var expr = Expression.parseOperand({$gte:[2,'$b']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lte:['$b',2]});
-			//              }
+		/** A $gt expression is optimized. */
+		"OptimizeGt": function OptimizeGt() {
+			new FieldRangeOptimize({
+				spec: {$gt:["$b",2]},
+			}).run();
+		},
 
-			//      },
+		/** A reverse sense $gt expression is optimized. */
+		"OptimizeGtReverse": function OptimizeGtReverse() {
+			new FieldRangeOptimize({
+				spec: {$gt:["$b",2]},
+			}).run();
+		},
 
+		/** A $gte expression is optimized. */
+		"OptimizeGte": function OptimizeGte() {
+			new FieldRangeOptimize({
+				spec: {$gte:["$b",2]},
+			}).run();
+		},
 
-		}
+		/** A reverse sense $gte expression is optimized. */
+		"OptimizeGteReverse": function OptimizeGteReverse() {
+			new FieldRangeOptimize({
+				spec: {$gte:[2,"$b"]},
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 72
test/lib/pipeline/expressions/CondExpression.js

@@ -1,72 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	CondExpression = require("../../../../lib/pipeline/expressions/CondExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-	"CondExpression": {
-
-		"constructor()": {
-
-			"should throw Error when constructing without args": function testConstructor(){
-				assert.throws(function(){
-					new CondExpression();
-				});
-			},
-
-			"should throw Error when constructing with 1 arg": function testConstructor1(){
-				assert.throws(function(){
-					new CondExpression({if:true === true});
-				});
-			},
-			"should throw Error when constructing with 2 args": function testConstructor2(){
-				assert.throws(function(){
-					new CondExpression(true === true,1);
-				});
-			},
-			"should now throw Error when constructing with 3 args": function testConstructor3(){
-				assert.doesNotThrow(function(){
-					//new CondExpression({$cond:[{"if":"true === true"},{"then":"1"},{"else":"0"}]});
-					new CondExpression({$cond:[ true === true, 1, 0 ]});
-				});
-			},
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $cond": function testOpName(){
-				assert.equal(new CondExpression().getOpName(), "$cond");
-			}
-
-		},
-
-		"#evaluateInternal()": {
-
-			"should evaluate boolean expression as true, then return 1; [ true === true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ true === true, 1, 0 ]}).evaluateInternal({}), 1);
-			},
-
-			"should evaluate boolean expression as false, then return 0; [ false === true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ false === true, 1, 0 ]}).evaluateInternal({}), 0);
-			}, 
-
-			"should evaluate boolean expression as true, then return 1; [ (true === true) && true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ (true === true) && true , 1, 0 ]}).evaluateInternal({}), 1);
-			},
-
-			"should evaluate boolean expression as false, then return 0; [ (false === true) && true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ (false === true) && true, 1, 0 ]}).evaluateInternal({}), 0);
-			},
-
-			"should evaluate complex boolean expression as true, then return 1; [ ( 1 > 0 ) && (( 'a' == 'b' ) || ( 3 <= 5 )), 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ ( 1 > 0 ) && (( 'a' == 'b' ) || ( 3 <= 5 )), 1, 0 ]}).evaluate({}), 1);
-			},
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 93 - 104
test/lib/pipeline/expressions/CondExpression_test.js

@@ -1,128 +1,117 @@
 "use strict";
 var assert = require("assert"),
 	CondExpression = require("../../../../lib/pipeline/expressions/CondExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.CondExpression = {
 
-	"CondExpression": {
+	"constructor()": {
 
-		"constructor()": {
+		"should not throw an Error when constructing without args": function testConstructor(){
+			assert.doesNotThrow(function(){
+				new CondExpression();
+			});
+		},
+
+		"should throw Error when constructing with 1 arg": function testConstructor1(){
+			assert.throws(function(){
+				new CondExpression(1);
+			});
+		},
+
+	},
+
+	"#getOpName()": {
 
-			"should not throw an Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new CondExpression();
+		"should return the correct op name; $cond": function testOpName(){
+			assert.equal(new CondExpression().getOpName(), "$cond");
+		},
+
+	},
+
+	"#evaluate()": {
+		"array style": {
+
+			"should fail if there aren't enough arguments": function() {
+				assert.throws(function(){
+					Expression.parseOperand({$cond:[1,2]}, {});
 				});
 			},
 
-			"should throw Error when constructing with 1 arg": function testConstructor1(){
+			"should fail if there are too many arguments": function() {
 				assert.throws(function(){
-					new CondExpression(1);
+					Expression.parseOperand({$cond:[1, 2, 3, 4]}, {});
 				});
-			}
-		},
+			},
 
-		"#getOpName()": {
+			"should evaluate boolean expression as true, then return 1; [ true === true, 1, 0 ]": function () {
+				assert.strictEqual(Expression.parseOperand({$cond: [ true, 1, 0 ]}, {}).evaluate({}), 1);
+			},
 
-			"should return the correct op name; $cond": function testOpName(){
-				assert.equal(new CondExpression().getOpName(), "$cond");
-			}
+			"should evaluate boolean expression as false, then return 0; [ false === true, 1, 0 ]": function () {
+				assert.strictEqual(Expression.parseOperand({$cond: [ false, 1, 0 ]}, {}).evaluate({}), 0);
+			},
 
 		},
 
-		"#evaluateInternal()": {
-			"array style": {
+		"object style": {
 
-				"should fail if there aren't enough arguments": function() {
-					assert.throws(function(){
-						Expression.parseOperand({$cond:[1,2]}, {});
-					})
-				},
-				"should fail if there are too many arguments": function() {
+			beforeEach: function(){
+				this.shouldFail = function(expr) {
 					assert.throws(function(){
-						Expression.parseOperand({$cond:[1, 2, 3, 4]}, {});
-					})
-				},
-				"should evaluate boolean expression as true, then return 1; [ true === true, 1, 0 ]": function () {
-					assert.strictEqual(Expression.parseOperand({$cond: [ true, 1, 0 ]}, {}).evaluateInternal({}), 1);
-				},
-
-				"should evaluate boolean expression as false, then return 0; [ false === true, 1, 0 ]": function () {
-					assert.strictEqual(Expression.parseOperand({$cond: [ false, 1, 0 ]}, {}).evaluateInternal({}), 0);
-				},
-				"should fail when the 'if' position is empty": function(){
-					assert.throws(function(){
-						Expression.parseOperand({$cond:[undefined, 2, 3]}, {});
-					})
-				},
-				"should fail when the 'then' position is empty": function(){
-					assert.throws(function(){
-						Expression.parseOperand({$cond:[1, undefined, 3]}, {});
-					})
-				},
-				"should fail when the 'else' position is empty": function(){
-					assert.throws(function(){
-						Expression.parseOperand({$cond:[1, 2, undefined]}, {});
-					})
-				}
+						Expression.parseOperand(expr, {});
+					});
+				};
+				this.vps = new VariablesParseState(new VariablesIdGenerator());
 			},
 
-			"object style": {
-				beforeEach: function(){
-					this.shouldFail = function(expr) {
-						assert.throws(function(){
-							Expression.parseOperand(expr, {});
-						});
-					}
-				},
-				"should fail because the $cond is missing": function(){
-					this.shouldFail({$zoot:[true, 1, 0 ]}, {});
-				},
-				"should fail because of missing if": function(){
-					this.shouldFail({$cond:{xif:1, then:2, else:3}});
-				},
-				"should fail because of missing then": function(){
-					this.shouldFail({$cond:{if:1, xthen:2, else:3}});
-				},
-				"should fail because of missing else": function(){
-					this.shouldFail({$cond:{if:1, then:2, xelse:3}});
-				},
-				"should fail because of empty if": function(){
-					this.shouldFail({$cond:{if:undefined, then:2, else:3}});
-				},
-				"should fail because of empty then": function(){
-					this.shouldFail({$cond:{if:1, then:undefined, else:3}});
-				},
-				"should fail because of empty else": function(){
-					this.shouldFail({$cond:{if:1, then:2, else:undefined}});
-				},
-				"should fail because of mystery args": function(){
-					this.shouldFail({$cond:{if:1, then:2, else:3, zoot:4}});
-				},
-				"should evaluate true": function(){
-					assert.strictEqual(
-						Expression.parseOperand({$cond:{ if: true, then: 1, else: 0}}, {}).evaluate({}),
-						1);
-				},
-				"should evaluate true even with mixed up args": function(){
-					assert.strictEqual(
-						Expression.parseOperand({$cond:{ else: 0, then: 1, if: "$a" }}, {}).evaluate({$a: 1}),
-						1);
-				},
-				"should evaluate false": function(){
-					assert.strictEqual(
-						Expression.parseOperand({$cond:{ if: "$a", then: 0, else: 1}}, {}).evaluate({$a: 0}),
-						1);
-				},
-				"should evaluate false even with mixed up args": function() {
-					assert.strictEqual(
-						Expression.parseOperand({$cond: { else: 1, then: 0, if: "$a"}}, {}).evaluate({$a: 0}),
-						1);
-				}
-			}
-		}
-	}
-};
+			"should fail because of missing if": function(){
+				this.shouldFail({$cond:{ then:2, else:3}});
+			},
+
+			"should fail because of missing then": function(){
+				this.shouldFail({$cond:{if:1,  else:3}});
+			},
+
+			"should fail because of missing else": function(){
+				this.shouldFail({$cond:{if:1, then:2 }});
+			},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+			"should fail because of mystery args": function(){
+				this.shouldFail({$cond:{if:1, then:2, else:3, zoot:4}});
+			},
+
+			"should evaluate true": function(){
+				assert.strictEqual(
+					Expression.parseOperand({$cond:{ if: true, then: 1, else: 0}}, {}).evaluate({}),
+					1);
+			},
+
+			"should evaluate true even with mixed up args": function(){
+				assert.strictEqual(
+					Expression.parseOperand({$cond:{ else: 0, then: 1, if: "$a" }}, this.vps).evaluate({a: 1}),
+					1);
+			},
+
+			"should evaluate false": function(){
+				assert.strictEqual(
+					Expression.parseOperand({$cond:{ if: "$a", then: 0, else: 1}}, this.vps).evaluate({a: 0}),
+					1);
+			},
+
+			"should evaluate false even with mixed up args": function() {
+				assert.strictEqual(
+					Expression.parseOperand({$cond: { else: 1, then: 0, if: "$a"}}, this.vps).evaluate({a: 0}),
+					1);
+			},
+
+		},
+
+	},
+
+};

+ 29 - 59
test/lib/pipeline/expressions/DayOfMonthExpression.js

@@ -1,74 +1,44 @@
 "use strict";
 var assert = require("assert"),
-		DayOfMonthExpression = require("../../../../lib/pipeline/expressions/DayOfMonthExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression"),
-		VariablesParseState = require("../../../../Lib/pipeline/expressions/VariablesParseState"),
-		VariablesIdGenerator = require("../../../../Lib/pipeline/expressions/VariablesIdGenerator");
+	DayOfMonthExpression = require("../../../../lib/pipeline/expressions/DayOfMonthExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.DayOfMonthExpression = {
 
-		"DayOfMonthExpression": {
+	"constructor()": {
 
-				"constructor()": {
+		"should create instance": function() {
+			assert(new DayOfMonthExpression() instanceof DayOfMonthExpression);
+			assert(new DayOfMonthExpression() instanceof Expression);
+		},
 
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new DayOfMonthExpression();
-								});
-						},
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new DayOfMonthExpression("bad stuff");
+			});
+		},
 
-						"should not throw Error when constructing with an arg": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new DayOfMonthExpression("1/1/2014");
-								});
-						}
+	},
 
-				},
+	"#getOpName()": {
 
-				"#getOpName()": {
+		"should return the correct op name; $dayOfMonth": function() {
+			assert.equal(new DayOfMonthExpression().getOpName(), "$dayOfMonth");
+		},
 
-						"should return the correct op name; $dayOfMonth": function testOpName() {
-								assert.equal(new DayOfMonthExpression("1/1/2014").getOpName(), "$dayOfMonth");
-						}
+	},
 
-				},
+	"#evaluate()": {
 
-				"#evaulateInternal1()": {
+		"should return day of month; 10 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$dayOfMonth", operands);
+			assert.strictEqual(expr.evaluate({}), 1);
+		},
 
-						"should return day of month; 10 for 2013-03-10": function testOpName() {
-								assert.equal(new DayOfMonthExpression("2013-03-10T00:00:00.000Z").evaluateInternal(), "10");
-						}
+	},
 
-				},
-
-				"#evaluateInternal2()": {
-
-						"should return day of month; 18 for 2013-02-18": function testStuff() {
-
-								var idGenerator = new VariablesIdGenerator();
-								var vps = new VariablesParseState(idGenerator);
-								var parseOp = Expression.parseOperand({
-										$dayOfMonth: "$someDate"
-								}, vps);
-
-								var result = parseOp.evaluateInternal({
-										$someDate: new Date("2013-02-18T00:00:00.000Z")
-								});
-
-								assert.strictEqual(result, "2");
-
-										// assert.strictEqual(Expression.parseOperand({
-										// $dayOfMonth: "$someDate"
-										// }, vps).evaluate({
-										// someDate: new Date("2013-02-18T00:00:00.000Z")
-										// }), 18);
-								}
-
-						}
-
-				}
-
-		};
-
-		if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+};

+ 23 - 28
test/lib/pipeline/expressions/DayOfWeekExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	DayOfWeekExpression = require("../../../../lib/pipeline/expressions/DayOfWeekExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.DayOfWeekExpression = {
 
-	"DayOfWeekExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new DayOfWeekExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new DayOfWeekExpression() instanceof DayOfWeekExpression);
+			assert(new DayOfWeekExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $dayOfWeek": function testOpName(){
-				assert.equal(new DayOfWeekExpression().getOpName(), "$dayOfWeek");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new DayOfWeekExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new DayOfWeekExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $dayOfWeek": function() {
+			assert.equal(new DayOfWeekExpression().getOpName(), "$dayOfWeek");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return day of week; 2 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$dayOfWeek:"$someDate"}).evaluateInternal({someDate:new Date("2013-02-18T00:00:00.000Z")}), 2);
-			}
+	"#evaluate()": {
 
-		}
+		"should return day of week; 7 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$dayOfWeek", operands);
+			assert.strictEqual(expr.evaluate({}), 7);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 23 - 28
test/lib/pipeline/expressions/DayOfYearExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	DayOfYearExpression = require("../../../../lib/pipeline/expressions/DayOfYearExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.DayOfYearExpression = {
 
-	"DayOfYearExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new DayOfYearExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new DayOfYearExpression() instanceof DayOfYearExpression);
+			assert(new DayOfYearExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $dayOfYear": function testOpName(){
-				assert.equal(new DayOfYearExpression().getOpName(), "$dayOfYear");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new DayOfYearExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new DayOfYearExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $dayOfYear": function() {
+			assert.equal(new DayOfYearExpression().getOpName(), "$dayOfYear");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return day of year; 49 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$dayOfYear:"$someDate"}).evaluate({someDate:new Date("2013-02-18T00:00:00.000Z")}), 49);
-			}
+	"#evaluate()": {
 
-		}
+		"should return day of year; 305 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$dayOfYear", operands);
+			assert.strictEqual(expr.evaluate({}), 305);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 47 - 0
test/lib/pipeline/expressions/DivideExpression_test.js

@@ -0,0 +1,47 @@
+"use strict";
+var assert = require("assert"),
+	DivideExpression = require("../../../../lib/pipeline/expressions/DivideExpression"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.DivideExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new DivideExpression() instanceof DivideExpression);
+			assert(new DivideExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new DivideExpression("bad stuff");
+			});
+		}
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $size": function() {
+			assert.equal(new DivideExpression().getOpName(), "$divide");
+		}
+
+	},
+
+	"#evaluate()": {
+
+		"should divide two numbers": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$divide: ["$a", "$b"]}, vps),
+				input = {a: 6, b: 2};
+			assert.strictEqual(expr.evaluate(input), 3);
+		}
+
+	}
+
+};

+ 23 - 28
test/lib/pipeline/expressions/HourExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	HourExpression = require("../../../../lib/pipeline/expressions/HourExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.HourExpression = {
 
-	"HourExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new HourExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new HourExpression() instanceof HourExpression);
+			assert(new HourExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $hour": function testOpName(){
-				assert.equal(new HourExpression().getOpName(), "$hour");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new HourExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new HourExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $hour": function() {
+			assert.equal(new HourExpression().getOpName(), "$hour");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return hour; 15 for 2013-02-18 3:00pm": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$hour:"$someDate"}).evaluate({someDate:new Date("2013-02-18T15:00:00.000Z")}), 15);
-			}
+	"#evaluate()": {
 
-		}
+		"should return hour; 19 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$hour", operands);
+			assert.strictEqual(expr.evaluate({}), 19);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 58
test/lib/pipeline/expressions/IfNullExpression.js

@@ -1,58 +0,0 @@
-"use strict";
-var assert = require("assert"),
-		IfNullExpression = require("../../../../lib/pipeline/expressions/IfNullExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"IfNullExpression": {
-
-				"constructor()": {
-
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new IfNullExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $ifNull": function testOpName() {
-								assert.equal(new IfNullExpression().getOpName(), "$ifNull");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"should return the left hand side if the left hand side is not null or undefined": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$ifNull: ["$a", "$b"]
-								}).evaluateInternal({
-										a: 1,
-										b: 2
-								}), 1);
-						},
-						"should return the right hand side if the left hand side is null or undefined": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$ifNull: ["$a", "$b"]
-								}).evaluateInternal({
-										a: null,
-										b: 2
-								}), 2);
-								assert.strictEqual(Expression.parseOperand({
-										$ifNull: ["$a", "$b"]
-								}).evaluateInternal({
-										b: 2
-								}), 2);
-						}
-				}
-
-		}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 40 - 45
test/lib/pipeline/expressions/IfNullExpression_test.js

@@ -6,59 +6,54 @@ var assert = require("assert"),
 	Variables = require("../../../../lib/pipeline/expressions/Variables"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.IfNullExpression = {
 
-	"IfNullExpression": {
+	"constructor()": {
 
-		"constructor()": {
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function () {
+				new IfNullExpression();
+			});
+		},
 
-			"should not throw Error when constructing without args": function() {
-				assert.doesNotThrow(function () {
-					new IfNullExpression();
-				});
-			},
-			"should throw Error when constructing with args": function () {
-				assert.throws(function () {
-					new IfNullExpression(1);
-				});
-			}
+		"should throw Error when constructing with args": function () {
+			assert.throws(function () {
+				new IfNullExpression(1);
+			});
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $ifNull": function() {
-				assert.equal(new IfNullExpression().getOpName(), "$ifNull");
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $ifNull": function() {
+			assert.equal(new IfNullExpression().getOpName(), "$ifNull");
 		},
 
-		"#evaluateInternal()": {
-			beforeEach: function () {
-				this.vps = new VariablesParseState(new VariablesIdGenerator());
-				this.parsed = Expression.parseExpression("$ifNull", ["$a", "$b"], this.vps);
-				this.vars = new Variables(2);
-				this.vars.setValue(0, "a");
-				this.vars.setValue(1, "b");
-				this.makeParsed = function(a, b) {
-					return Expression.parseExpression("$ifNull", [a, b], this.vps);
-				}
-			},
-
-			"should return the left hand side if the left hand side is not null or undefined": function() {
-				//assert.strictEqual(this.parsed.evaluate(this.vars), 1);
-				assert.strictEqual(this.makeParsed(1, 2).evaluate(this.vars), 1);
-			},
-			"should return the right hand side if the left hand side is null": function() {
-				//assert.strictEqual(this.parsed.evaluate({a: null, b: 2}), 2);
-				assert.strictEqual(this.makeParsed(null, 2).evaluate(this.vars), 2);
-			},
-			"should return the right hand side if the left hand side is undefined": function() {
-				//assert.strictEqual(this.parsed.evaluate({b: 2}), 2);
-				assert.strictEqual(this.makeParsed(undefined, 2).evaluate(this.vars), 2);
-			}
-		}
-	}
-};
+	},
+
+	"#evaluate()": {
+
+		beforeEach: function () {
+			this.vps = new VariablesParseState(new VariablesIdGenerator());
+			this.parsed = Expression.parseExpression("$ifNull", ["$a", "$b"], this.vps);
+		},
+
+		"should return the left hand side if the left hand side is not null or undefined": function() {
+			assert.strictEqual(this.parsed.evaluate({a: 1, b: 2}), 1);
+		},
 
-if (!module.parent)(new (require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should return the right hand side if the left hand side is null": function() {
+			assert.strictEqual(this.parsed.evaluate({a: null, b: 2}), 2);
+		},
+
+		"should return the right hand side if the left hand side is undefined": function() {
+			assert.strictEqual(this.parsed.evaluate({b: 2}), 2);
+		},
+
+	},
+
+};

+ 28 - 43
test/lib/pipeline/expressions/MillisecondExpression.js

@@ -1,59 +1,44 @@
 "use strict";
 var assert = require("assert"),
-		MillisecondExpression = require("../../../../lib/pipeline/expressions/MillisecondExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
+	MillisecondExpression = require("../../../../lib/pipeline/expressions/MillisecondExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.MillisecondExpression = {
 
-		"MillisecondExpression": {
+	"constructor()": {
 
-				"constructor()": {
+		"should create instance": function() {
+			assert(new MillisecondExpression() instanceof MillisecondExpression);
+			assert(new MillisecondExpression() instanceof Expression);
+		},
 
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new MillisecondExpression();
-								});
-						}
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new MillisecondExpression("bad stuff");
+			});
+		},
 
-				},
+	},
 
-				"#getOpName()": {
+	"#getOpName()": {
 
-						"should return the correct op name; $millisecond": function testOpName() {
-								assert.equal(new MillisecondExpression().getOpName(), "$millisecond");
-						}
+		"should return the correct op name; $millisecond": function() {
+			assert.equal(new MillisecondExpression().getOpName(), "$millisecond");
+		},
 
-				},
+	},
 
-				"#getFactory()": {
+	"#evaluate()": {
 
-						"should return the constructor for this class": function factoryIsConstructor() {
-								assert.strictEqual(new MillisecondExpression().getFactory(), undefined);
-						}
+		"should return millisecond; 819 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$millisecond", operands);
+			assert.strictEqual(expr.evaluate({}), 819);
+		},
 
-				},
-
-				"#evaluate()": {
-
-						"should return the current millisecond in the date; 19 for 2013-02-18 11:24:19 EST": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$millisecond: "$someDate"
-								}).evaluate({
-										someDate: new Date("2013-02-18T11:24:19.456Z")
-								}), 456);
-						}
-
-						/*
-			"should return the leap millisecond in the date; 60 for June 30, 2012 at 23:59:60 UTC": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$millisecond:"$someDate"}).evaluate({someDate:new Date("June 30, 2012 at 23:59:60 UTC")}), 60);
-			}
-
-				*/
-				}
-
-		}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 23 - 28
test/lib/pipeline/expressions/MinuteExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	MinuteExpression = require("../../../../lib/pipeline/expressions/MinuteExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.MinuteExpression = {
 
-	"MinuteExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new MinuteExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new MinuteExpression() instanceof MinuteExpression);
+			assert(new MinuteExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $minute": function testOpName(){
-				assert.equal(new MinuteExpression().getOpName(), "$minute");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new MinuteExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new MinuteExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $minute": function() {
+			assert.equal(new MinuteExpression().getOpName(), "$minute");
 		},
 
-		"#evaluateInternal()": {
+	},
 
-			"should return minute; 47 for 2013-02-18 3:47 pm": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$minute:"$someDate"}).evaluateInternal({someDate:new Date("2013-02-18T15:47:00.000Z")}), 47);
-			}
+	"#evaluate()": {
 
-		}
+		"should return minute; 31 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$minute", operands);
+			assert.strictEqual(expr.evaluate({}), 31);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 62 - 35
test/lib/pipeline/expressions/ModExpression.js

@@ -1,53 +1,80 @@
 "use strict";
 var assert = require("assert"),
 	ModExpression = require("../../../../lib/pipeline/expressions/ModExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression"),
-	VariablesParseState = require("../../../../lib/pipeline/expressions/Expression");
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.ModExpression = {
 
-	"ModExpression": {
+	"constructor()": {
 
-		"constructor()": {
+		"should construct instance": function() {
+			assert(new ModExpression() instanceof ModExpression);
+			assert(new ModExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new ModExpression("bad stuff");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $mod": function() {
+			assert.equal(new ModExpression().getOpName(), "$mod");
+		},
+
+	},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new ModExpression();
-				});
-			}
+	"#evaluate()": {
 
+		"should return modulus of two numbers": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 6, b: 2};
+			assert.strictEqual(expr.evaluate(input), 0);
 		},
 
-		"#getOpName()": {
-			"should return the correct op name; $mod": function testOpName(){
-				assert.equal(new ModExpression().getOpName(), "$mod");
-			}
+		"should return null if first is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: null, b: 2};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
+		"should return null if first is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: undefined, b: 2};
+			assert.strictEqual(expr.evaluate(input), null);
 		},
 
-		"#evaluateInternal()": {
-			"should return rhs if rhs is undefined or null": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}, new VariablesParseState()).evaluate({lhs:20.453, rhs:null}), null);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:20.453}), undefined);
-			},
-			"should return lhs if lhs is undefined or null": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:null, rhs:20.453}), null);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({rhs:20.453}), undefined);
-			},
-			"should return undefined if rhs is 0": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:20.453, rhs:0}), undefined);
-			},
-			"should return proper mod of rhs and lhs if both are numbers": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:234.4234, rhs:45}), 234.4234 % 45);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:0, rhs:45}), 0 % 45);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:-6, rhs:-0.5}), -6 % -0.5);
-			}
+		"should return null if second is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 11, b: null};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
-		}
+		"should return null if second is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 42, b: undefined};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 23 - 28
test/lib/pipeline/expressions/MonthExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	MonthExpression = require("../../../../lib/pipeline/expressions/MonthExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.MonthExpression = {
 
-	"MonthExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new MonthExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new MonthExpression() instanceof MonthExpression);
+			assert(new MonthExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $month": function testOpName(){
-				assert.equal(new MonthExpression().getOpName(), "$month");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new MonthExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new MonthExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $month": function() {
+			assert.equal(new MonthExpression().getOpName(), "$month");
 		},
 
-		"#evaluateInternal()": {
+	},
 
-			"should return month; 2 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$month:"$someDate"}).evaluate({someDate:new Date("2013-02-18T00:00:00.000Z")}), 2);
-			}
+	"#evaluate()": {
 
-		}
+		"should return month; 11 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$month", operands);
+			assert.strictEqual(expr.evaluate({}), 11);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 45
test/lib/pipeline/expressions/NotExpression.js

@@ -1,45 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	NotExpression = require("../../../../lib/pipeline/expressions/NotExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-	"NotExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new NotExpression();
-				});
-			}
-
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $not": function testOpName(){
-				assert.equal(new NotExpression().getOpName(), "$not");
-			}
-
-		},
-
-		"#evaluateInternal()": {
-
-			"should return false for a true input; false for true": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$not:true}).evaluateInternal({}), false);
-			},
-
-			"should return true for a false input; true for false": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$not:false}).evaluateInternal({}), true);
-			}
-
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 47 - 0
test/lib/pipeline/expressions/NotExpression_test.js

@@ -0,0 +1,47 @@
+"use strict";
+var assert = require("assert"),
+	NotExpression = require("../../../../lib/pipeline/expressions/NotExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.NotExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function(){
+				new NotExpression();
+			});
+		},
+
+		"should throw when constructing with args": function() {
+			assert.throws(function(){
+				new NotExpression(1);
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $not": function() {
+			assert.equal(new NotExpression().getOpName(), "$not");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should return false for a true input; false for true": function() {
+			assert.strictEqual(Expression.parseOperand({$not:true}, {}).evaluateInternal({}), false);
+		},
+
+		"should return true for a false input; true for false": function() {
+			assert.strictEqual(Expression.parseOperand({$not:false}, {}).evaluateInternal({}), true);
+		},
+
+	},
+
+};

+ 23 - 34
test/lib/pipeline/expressions/SecondExpression.js

@@ -3,53 +3,42 @@ var assert = require("assert"),
 	SecondExpression = require("../../../../lib/pipeline/expressions/SecondExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.SecondExpression = {
 
-	"SecondExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new SecondExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new SecondExpression() instanceof SecondExpression);
+			assert(new SecondExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $second": function testOpName(){
-				assert.equal(new SecondExpression().getOpName(), "$second");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new SecondExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new SecondExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $second": function() {
+			assert.equal(new SecondExpression().getOpName(), "$second");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return the current second in the date; 19 for 2013-02-18 11:24:19 EST": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$second:"$someDate"}).evaluate({someDate:new Date("2013-02-18T11:24:19.000Z")}), 19);
-			}
+	"#evaluate()": {
 
-				/*
-			"should return the leap second in the date; 60 for June 30, 2012 at 23:59:60 UTC": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$second:"$someDate"}).evaluate({someDate:new Date("June 30, 2012 at 23:59:60 UTC")}), 60);
-			}
-
-				*/
-		}
+		"should return second; 53 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$second", operands);
+			assert.strictEqual(expr.evaluate({}), 53);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 36 - 30
test/lib/pipeline/expressions/SizeExpression.js

@@ -1,46 +1,52 @@
 "use strict";
-var assert = require("assert"),
-		SizeExpression = require("../../../../lib/pipeline/expressions/SizeExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+var assert = require("assert"),
+	SizeExpression = require("../../../../lib/pipeline/expressions/SizeExpression"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
-module.exports = {
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-		"SizeExpression": {
+exports.SizeExpression = {
 
-				"constructor()": {
+	"constructor()": {
 
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new SizeExpression();
-								});
-						}
+		"should construct instance": function testConstructor() {
+			assert(new SizeExpression() instanceof SizeExpression);
+			assert(new SizeExpression() instanceof Expression);
+		},
 
-				},
+		"should error if given args": function testConstructor() {
+			assert.throws(function() {
+				new SizeExpression("bad stuff");
+			});
+		}
 
-				"#getOpName()": {
+	},
 
-						"should return the correct op name; $size": function testOpName() {
-								assert.equal(new SizeExpression("test").getOpName(), "$size");
-						}
+	"#evaluate()": {
 
-				},
+		"should return the size": function testSize() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$size: ["$a"]}, vps),
+				input = {
+					a: [{a:1},{b:2}],
+					b: [{c:3}]
+				};
+			assert.strictEqual(expr.evaluate(input), 2);
+		}
 
-				"#evaluateInternal()": {
+	},
 
-						// New test not working
-						"should return the size": function testSize() {
-								assert.strictEqual(Expression.parseOperand({
-										$size: ["$a"]
-								}).evaluateInternal({
-										a: [{a:1},{b:2}],
-										b: [{c:3}]
-								}), 4);
-						}
-				}
+	"#getOpName()": {
 
+		"should return the correct op name; $size": function testOpName() {
+			assert.equal(new SizeExpression().getOpName(), "$size");
 		}
 
-};
+	}
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+};

+ 89 - 40
test/lib/pipeline/expressions/StrcasecmpExpression.js

@@ -1,60 +1,109 @@
 "use strict";
 var assert = require("assert"),
 	StrcasecmpExpression = require("../../../../lib/pipeline/expressions/StrcasecmpExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	ExpectedResultBase = (function() {
+		var klass = function ExpectedResultBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function(){
+			this.assertResult(this.expectedResult, this.spec());
+			this.assertResult(-this.expectedResult, this.reverseSpec());
+		};
+		proto.spec = function() { return {$strcasecmp:[this.a, this.b]}; };
+		proto.reverseSpec = function() { return {$strcasecmp:[this.b, this.a]}; };
+		proto.assertResult = function(expectedResult, spec) {
+			var specElement = spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expression = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(spec), expressionToJson(expression));
+			assert.equal(expectedResult, expression.evaluate({}));
+		};
+		return klass;
+	})();
+
+exports.StrcasecmpExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new StrcasecmpExpression() instanceof StrcasecmpExpression);
+			assert(new StrcasecmpExpression() instanceof Expression);
+		},
 
-	"StrcasecmpExpression": {
+		"should error if given args": function() {
+			assert.throws(function() {
+				new StrcasecmpExpression("bad stuff");
+			});
+		},
 
-		"constructor()": {
+	},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new StrcasecmpExpression();
-				});
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $strcasecmp": function(){
+			assert.equal(new StrcasecmpExpression().getOpName(), "$strcasecmp");
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $strcasecmp": function testOpName(){
-				assert.equal(new StrcasecmpExpression().getOpName(), "$strcasecmp");
-			}
+	"#evaluate()": {
 
+		"should return '_ab' == '_AB' (w/ null begin)": function NullBegin() {
+			new ExpectedResultBase({
+				a: "\0ab",
+				b: "\0AB",
+				expectedResult: 0,
+			}).run();
 		},
 
-		"#getFactory()": {
-
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new StrcasecmpExpression().getFactory(), undefined);
-			}
+		"should return 'ab_' == 'aB_' (w/ null end)": function NullEnd() {
+			new ExpectedResultBase({
+				a: "ab\0",
+				b: "aB\0",
+				expectedResult: 0,
+			}).run();
+		},
 
+		"should return 'a_a' < 'a_B' (w/ null middle)": function NullMiddleLt() {
+			new ExpectedResultBase({
+				a: "a\0a",
+				b: "a\0B",
+				expectedResult: -1,
+			}).run();
 		},
 
-		"#evaluateInternal()": {
+		"should return 'a_b' == 'a_B' (w/ null middle)": function NullMiddleEq() {
+			new ExpectedResultBase({
+				a: "a\0b",
+				b: "a\0B",
+				expectedResult: 0,
+			}).run();
+		},
 
-			"should return 0 if the strings are equivalent and begin with a null character": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$strcasecmp:["$a", "$b"]}).evaluateInternal({a:"\0ab", b:"\0AB"}), 0);
-			},
-			"should return 0 if the strings are equivalent and end with a null character": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$strcasecmp:["$a", "$b"]}).evaluateInternal({a:"ab\0", b:"AB\0"}), 0);
-			},
-			"should return -1 if the left hand side is less than the right hand side and both contain a null character": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$strcasecmp:["$a", "$b"]}).evaluateInternal({a:"a\0a", b:"A\0B"}), -1);
-			},
-			"should return 0 if the strings are equivalent and both contain a null character": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$strcasecmp:["$a", "$b"]}).evaluateInternal({a:"a\0b", b:"A\0B"}), 0);
-			},
-			"should return 1 if the left hand side is greater than the right hand side and both contain a null character": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$strcasecmp:["$a", "$b"]}).evaluateInternal({a:"a\0c", b:"A\0B"}), 1);
-			}
-		}
+		"should return 'a_c' > 'a_B' (w/ null middle)": function NullMiddleGt() {
+			new ExpectedResultBase({
+				a: "a\0c",
+				b: "a\0B",
+				expectedResult: 1,
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 161
test/lib/pipeline/expressions/SubstrExpression.js

@@ -1,161 +0,0 @@
-"use strict";
-var assert = require("assert"),
-		SubstrExpression = require("../../../../lib/pipeline/expressions/SubstrExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"SubstrExpression": {
-
-				"constructor()": {
-
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new SubstrExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $substr": function testOpName() {
-								assert.equal(new SubstrExpression().getOpName(), "$substr");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"Should fail if no end argument is given": function testMissing3rdArg() {
-								var s = "mystring",
-										start = 0,
-										end = s.length;
-								assert.throws(function() {
-										Expression.parseOperand({
-												$substr: ["$s", "$start"]
-										}).evaluateInternal({
-												s: s,
-												start: start
-										});
-								});
-						},
-
-						"Should return entire string when called with 0 and length": function testWholeString() {
-								var s = "mystring",
-										start = 0,
-										end = s.length;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end
-								}), "mystring");
-						},
-
-						"Should return entire string less the last character when called with 0 and length-1": function testLastCharacter() {
-								var s = "mystring",
-										start = 0,
-										end = s.length;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end - 1
-								}), "mystrin");
-						},
-
-						"Should return empty string when 0 and 0 are given as indexes": function test00Indexes() {
-								var s = "mystring",
-										start = 0,
-										end = 0;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end
-								}), "");
-						},
-
-						"Should first character when 0 and 1 are given as indexes": function testFirstCharacter() {
-								var s = "mystring",
-										start = 0,
-										end = 1;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end
-								}), "m");
-						},
-
-						"Should return empty string when empty string is given": function testEmptyString() {
-								var s = "",
-										start = 0,
-										end = 0;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end
-								}), "");
-						},
-
-						"Should return the entire string if end is -1": function testIndexTooLarge() {
-								var s = "mystring",
-										start = 0,
-										end = -1;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end
-								}), "mystring");
-						},
-
-
-						"Should fail if end is before begin": function testUnorderedIndexes() {
-								var s = "mystring",
-										start = s.length,
-										end = 0;
-								assert.throws(function() {
-										Expression.parseOperand({
-												$substr: ["$s", "$start"]
-										}).evaluateInternal({
-												s: s,
-												start: start,
-												end: end
-										});
-								});
-						},
-
-						"Should fail if end is greater than length": function testIndexTooLarge() {
-								var s = "mystring",
-										start = 0,
-										end = s.length + 1;
-								assert.throws(function() {
-										Expression.parseOperand({
-												$substr: ["$s", "$start"]
-										}).evaluateInternal({
-												s: s,
-												start: start,
-												end: end
-										});
-								});
-						},
-
-
-				}
-
-		}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 109 - 0
test/lib/pipeline/expressions/SubstrExpression_test.js

@@ -0,0 +1,109 @@
+"use strict";
+var assert = require("assert"),
+	SubstrExpression = require("../../../../lib/pipeline/expressions/SubstrExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	ExpectedResultBase = (function() {
+		var klass = function ExpectedResultBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function(){
+			var specElement = this.spec(),
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(specElement), expressionToJson(expr));
+			assert.deepEqual(this.expectedResult, expr.evaluate({}));
+		};
+		proto.spec = function() { return {$substr:[this.str, this.offset, this.length]}; };
+		return klass;
+	})();
+
+exports.SubstrExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new SubstrExpression() instanceof SubstrExpression);
+			assert(new SubstrExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new SubstrExpression("bad stuff");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $substr": function() {
+			assert.equal(new SubstrExpression().getOpName(), "$substr");
+		},
+
+	},
+
+	"evaluate": {
+
+		"should return full string (if contains null)": function FullNull() {
+			new ExpectedResultBase({
+				str: "a\0b",
+				offset: 0,
+				length: 3,
+				get expectedResult(){ return this.str; },
+			}).run();
+		},
+
+		"should return tail of string (if begin at null)": function BeginAtNull() {
+			new ExpectedResultBase({
+				str: "a\0b",
+				offset: 1,
+				length: 2,
+				expectedResult: "\0b",
+			}).run();
+		},
+
+		"should return head of string (if end at null)": function EndAtNull() {
+			new ExpectedResultBase({
+				str: "a\0b",
+				offset: 0,
+				length: 2,
+				expectedResult: "a\0",
+			}).run();
+		},
+
+		"should return tail of string (if head has null) ": function DropBeginningNull() {
+			new ExpectedResultBase({
+				str: "\0b",
+				offset: 1,
+				length: 1,
+				expectedResult: "b",
+			}).run();
+		},
+
+		"should return head of string (if tail has null)": function DropEndingNull() {
+			new ExpectedResultBase({
+				str: "a\0",
+				offset: 0,
+				length: 1,
+				expectedResult: "a",
+			}).run();
+		},
+
+	},
+
+};

+ 111 - 27
test/lib/pipeline/expressions/SubtractExpression.js

@@ -1,45 +1,129 @@
 "use strict";
 var assert = require("assert"),
 		SubtractExpression = require("../../../../lib/pipeline/expressions/SubtractExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
+		Expression = require("../../../../lib/pipeline/expressions/Expression"),
+		VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+		VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.SubtractExpression = {
 
-		"SubtractExpression": {
+	"constructor()": {
 
-				"constructor()": {
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SubtractExpression();
+			});
+		},
 
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new SubtractExpression();
-								});
-						}
+	},
 
-				},
+	"#getOpName()": {
 
-				"#getOpName()": {
+		"should return the correct op name; $subtract": function() {
+			assert.equal(new SubtractExpression().getOpName(), "$subtract");
+		},
 
-						"should return the correct op name; $subtract": function testOpName() {
-								assert.equal(new SubtractExpression().getOpName(), "$subtract");
-						}
+	},
 
-				},
+	"#evaluateInternal()": {
 
-				"#evaluateInternal()": {
+		"should return the result of subtraction between two numbers": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:2, b:1}),
+				expected = 1;
+			assert.strictEqual(result, expected);
+		},
 
-						"should return the result of subtraction between two numbers": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$subtract: ["$a", "$b"]
-								}).evaluateInternal({
-										a: 35636364,
-										b: -0.5656
-								}), 35636364 - (-0.5656));
-						}
-				}
+		"should return null if left is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:null, b:1}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
 
-		}
+		"should return null if left is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:undefined, b:1}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
+
+		"should return null if right is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:2, b:null}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
+
+		"should return null if right is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:2, b:undefined}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
+
+		"should subtract 2 dates": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = new Date("Jan 3 1990"),
+				date1 = new Date("Jan 1 1990"),
+				result = expr.evaluate({a:date2, b:date1}),
+				expected = date2 - date1;
+			assert.strictEqual(result, expected);
+		},
+
+		"should subtract a number of millis from a date": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = new Date("Jan 3 1990"),
+				millis = 24 * 60 * 60 * 1000,
+				result = expr.evaluate({a:date2, b:millis}),
+				expected = date2 - millis;
+			assert.strictEqual(
+				JSON.stringify(result),
+				JSON.stringify(expected)
+			);
+		},
+
+		"should throw if left is not a date or number": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = {},
+				date1 = new Date();
+			assert.throws(function() {
+				expr.evaluate({a:date2, b:date1});
+			});
+		},
+
+		"should throw if right is not a date or number": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = new Date(),
+				date1 = {};
+			assert.throws(function() {
+				expr.evaluate({a:date2, b:date1});
+			});
+		},
+
+	},
 
 };
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 23 - 28
test/lib/pipeline/expressions/WeekExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	WeekExpression = require("../../../../lib/pipeline/expressions/WeekExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.WeekExpression = {
 
-	"WeekExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new WeekExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new WeekExpression() instanceof WeekExpression);
+			assert(new WeekExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $week": function testOpName(){
-				assert.equal(new WeekExpression().getOpName(), "$week");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new WeekExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new WeekExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $week": function() {
+			assert.equal(new WeekExpression().getOpName(), "$week");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return week; 8 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$week:"$someDate"}).evaluate({someDate:new Date("2013-02-18T00:00:00.000Z")}), 7);
-			}
+	"#evaluate()": {
 
-		}
+		"should return week; 7 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$week", operands);
+			assert.strictEqual(expr.evaluate({}), 43);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 24 - 21
test/lib/pipeline/expressions/YearExpression.js

@@ -3,39 +3,42 @@ var assert = require("assert"),
 	YearExpression = require("../../../../lib/pipeline/expressions/YearExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.YearExpression = {
 
-	"YearExpression": {
+	"constructor()": {
 
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new YearExpression();
-				});
-			}
+		"should create instance": function() {
+			assert(new YearExpression() instanceof YearExpression);
+			assert(new YearExpression() instanceof Expression);
+		},
 
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new YearExpression("bad stuff");
+			});
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $year": function testOpName(){
-				assert.equal(new YearExpression().getOpName(), "$year");
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $year": function() {
+			assert.equal(new YearExpression().getOpName(), "$year");
 		},
 
-		"#evaluateInternal()": {
+	},
 
-			"should return year; 2013 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$year:"$someDate"}).evaluate({someDate:new Date("Mon Feb 18 2013 00:00:00 GMT-0500 (EST)")}), 2013);
-			}
+	"#evaluate()": {
 
-		}
+		"should return year; 2014 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$year", operands);
+			assert.strictEqual(expr.evaluate({}), 2014);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);