Просмотр исходного кода

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

Kyle P Davis 11 лет назад
Родитель
Сommit
89689aca3b
42 измененных файлов с 1537 добавлено и 1653 удалено
  1. 2 2
      lib/pipeline/Document.js
  2. 12 5
      lib/pipeline/expressions/AllElementsTrueExpression.js
  3. 83 78
      lib/pipeline/expressions/CompareExpression.js
  4. 32 87
      lib/pipeline/expressions/CondExpression.js
  5. 11 23
      lib/pipeline/expressions/DayOfMonthExpression.js
  6. 13 24
      lib/pipeline/expressions/DayOfWeekExpression.js
  7. 17 32
      lib/pipeline/expressions/DayOfYearExpression.js
  8. 22 23
      lib/pipeline/expressions/DivideExpression.js
  9. 13 26
      lib/pipeline/expressions/HourExpression.js
  10. 9 24
      lib/pipeline/expressions/IfNullExpression.js
  11. 11 25
      lib/pipeline/expressions/MillisecondExpression.js
  12. 11 23
      lib/pipeline/expressions/MinuteExpression.js
  13. 25 36
      lib/pipeline/expressions/ModExpression.js
  14. 11 23
      lib/pipeline/expressions/MonthExpression.js
  15. 11 24
      lib/pipeline/expressions/NotExpression.js
  16. 11 25
      lib/pipeline/expressions/SecondExpression.js
  17. 9 20
      lib/pipeline/expressions/SizeExpression.js
  18. 35 24
      lib/pipeline/expressions/SubtractExpression.js
  19. 13 30
      lib/pipeline/expressions/WeekExpression.js
  20. 8 23
      lib/pipeline/expressions/YearExpression.js
  21. 91 27
      test/lib/pipeline/expressions/AllElementsTrueExpression.js
  22. 409 308
      test/lib/pipeline/expressions/CompareExpression.js
  23. 0 72
      test/lib/pipeline/expressions/CondExpression.js
  24. 93 104
      test/lib/pipeline/expressions/CondExpression_test.js
  25. 29 59
      test/lib/pipeline/expressions/DayOfMonthExpression.js
  26. 23 28
      test/lib/pipeline/expressions/DayOfWeekExpression.js
  27. 23 28
      test/lib/pipeline/expressions/DayOfYearExpression.js
  28. 47 0
      test/lib/pipeline/expressions/DivideExpression_test.js
  29. 23 28
      test/lib/pipeline/expressions/HourExpression.js
  30. 0 58
      test/lib/pipeline/expressions/IfNullExpression.js
  31. 40 45
      test/lib/pipeline/expressions/IfNullExpression_test.js
  32. 28 43
      test/lib/pipeline/expressions/MillisecondExpression.js
  33. 23 28
      test/lib/pipeline/expressions/MinuteExpression.js
  34. 62 35
      test/lib/pipeline/expressions/ModExpression.js
  35. 23 28
      test/lib/pipeline/expressions/MonthExpression.js
  36. 0 45
      test/lib/pipeline/expressions/NotExpression.js
  37. 47 0
      test/lib/pipeline/expressions/NotExpression_test.js
  38. 23 34
      test/lib/pipeline/expressions/SecondExpression.js
  39. 36 30
      test/lib/pipeline/expressions/SizeExpression.js
  40. 111 27
      test/lib/pipeline/expressions/SubtractExpression.js
  41. 23 28
      test/lib/pipeline/expressions/WeekExpression.js
  42. 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]];

+ 12 - 5
lib/pipeline/expressions/AllElementsTrueExpression.js

@@ -34,12 +34,19 @@ proto.getOpName = function getOpName() {
  * @method @evaluateInternal
  **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var value = evaluateInternal(vars);
-	if (!vars instanceof Array) throw new Error("$allElementsTrue requires an array");
+	var arr = this.operands[0].evaluateInternal(vars);
 
-	for (var i = 0, n = this.operands.length; i < n; ++i) {
-		var checkValue = this.operands[i].evaluateInternal(vars);
-		if (!checkValue.coerceToBool()) return false;
+	if (!(arr instanceof Array)) throw new Error("uassert 17040: argument of " + this.getOpName() + "  must be an array, but is " + typeof arr);
+
+	//Deviation from mongo. This is needed so that empty array is handled properly.
+	if (arr.length === 0){
+		return false;
+	}
+
+	for (var i = 0, n = arr.length; i < n; ++i) {
+		if (! Value.coerceToBool(arr[i])){
+			return false;
+		}
 	}
 	return true;
 };

+ 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";
+};

+ 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);

+ 91 - 27
test/lib/pipeline/expressions/AllElementsTrueExpression.js

@@ -1,18 +1,28 @@
 "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();
 
+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 = {
 
 	"AllElementsTrueExpression": {
 
 		"constructor()": {
 
-			"should not throw Error when constructing without args": function testConstructor() {
-				assert.doesNotThrow(function() {
+			"should not throw Error when constructing without args": function testConstructor(){
+				assert.doesNotThrow(function(){
 					new AllElementsTrueExpression();
 				});
 			}
@@ -21,47 +31,101 @@ module.exports = {
 
 		"#getOpName()": {
 
-			"should return the correct op name; $allElements": function testOpName() {
+			"should return the correct op name; $allElements": function testOpName(){
 				assert.equal(new AllElementsTrueExpression().getOpName(), "$allElementsTrue");
 			}
 
 		},
 
-		"#evaluateInternal()": {
+		"integration": {
+
+			"JustFalse": function JustFalse(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[false]],
+				 	expr = Expression.parseExpression("$allElementsTrue", input),
+					result = expr.evaluate({}),
+					expected = false,
+					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
+			},
 
-			"should return error if parameter is empty:": function testEmpty() {
-				assert.throws(function() {
-					allElementsTrueExpression.evaluateInternal("asdf");
-				});
+			"JustTrue": function JustTrue(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[true]],
+					expr = Expression.parseExpression("$allElementsTrue", input),
+					result = expr.evaluate({}),
+					expected = true,
+					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
 			},
 
-			"should return error if parameter is not an array": function testNonArray() {
-				assert.throws(function() {
-					allElementsTrueExpression.evaluateInternal("This is not an array");
-				});
+			"OneTrueOneFalse": function OneTrueOneFalse(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[true, false]],
+					expr = Expression.parseExpression("$allElementsTrue", input),
+					result = expr.evaluate({}),
+					expected = false,
+					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
 			},
 
-			"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);
+			"OneFalseOneTrue": function OneTrueOneFalse(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[false, true]],
+					expr = Expression.parseExpression("$allElementsTrue", input),
+					result = expr.evaluate({}),
+					expected = false,
+					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
 			},
 
-			"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);
+			"Empty": function Empty(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[]],
+					expr = Expression.parseExpression("$allElementsTrue", input),
+					result = expr.evaluate({}),
+					expected = false,
+					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
 			},
 
-			"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);
+			"TrueViaInt": function TrueViaInt(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[1]],
+					expr = Expression.parseExpression("$allElementsTrue", input),
+					result = expr.evaluate({}),
+					expected = true,
+					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
 			},
 
+			"FalseViaInt": function FalseViaInt(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[0]],
+					expr = Expression.parseExpression("$allElementsTrue", input),
+					result = expr.evaluate({}),
+					expected = false,
+					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
+			},
+
+			"Null": function FalseViaInt(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [null],
+					expr = Expression.parseExpression("$allElementsTrue", input);
+				assert.throws(function() {
+					var result = expr.evaluate({});
+				});
+			}
+
 		}
 
 	}

+ 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);
+};

+ 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);