瀏覽代碼

EAGLESIX-2651: SetDifference: better sync w/ 2.6.5 code and tests

Kyle P Davis 11 年之前
父節點
當前提交
15282caab4
共有 2 個文件被更改,包括 353 次插入183 次删除
  1. 35 62
      lib/pipeline/expressions/SetDifferenceExpression.js
  2. 318 121
      test/lib/pipeline/expressions/SetDifferenceExpression.js

+ 35 - 62
lib/pipeline/expressions/SetDifferenceExpression.js

@@ -2,80 +2,53 @@
 
 /**
  * A $setDifference pipeline expression.
- * @see evaluateInternal
  * @class SetDifferenceExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetDifferenceExpression = module.exports = function SetDifferenceExpression() {
-
-	if (arguments.length !== 0) throw new Error("SetDifference constructor must be called with no args");
-
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetDifferenceExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SetDifferenceExpression, base = require("./FixedArityExpressionT")(SetDifferenceExpression, 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 "$setDifference";
-};
-
-/**
- * Stolen from Value.
- * @param val
- * @returns {boolean}
- */
-proto.nullish = function(val){
-	return val == null || val == undefined;
-}
-
-//NOTE: DEVIATION FROM MONGO: This probably has already been written in a different branch.
-proto.arrayToSet = function(a) {
-	var tmp = {},
-		b = [];
-	a.forEach(function(item){
-		tmp[item] = item;
-	});
-	Object.keys(tmp).forEach(function(item) {
-		b.push(tmp[item]);
-	});
-	return b;
-};
-
-/**
- * Takes 2 arrays. Returns the items in the first array which are not in the second.
- * @method evaluateInternal
- **/
+	Expression = require("./Expression"),
+	Helpers = require("./Helpers");
 
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var array1 = this.operands[0].evaluateInternal(vars),
-		array2 = this.operands[1].evaluateInternal(vars);
-
-	if (this.nullish(array1) || this.nullish(array2)) return null;
-
-	if (!(array1 instanceof Array)) throw new Error(this.getOpName() + ": object 1 must be an array; code 17048");
-	if (!(array2 instanceof Array)) throw new Error(this.getOpName() + ": object 2 must be an array; code 17049");
-
-	var returnVec = [],
-		dedupped = this.arrayToSet(array1);
-	dedupped.forEach(function(key) {
-		if (-1 === array2.indexOf(key)) {
-			returnVec.push(key);
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+		return null;
+	}
+
+	if (!(lhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + " must be arrays. First " +
+			"argument is of type: " + Value.getType(lhs) + "; uassert code 17048");
+	if (!(rhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + " must be arrays. Second " +
+			"argument is of type: " + Value.getType(rhs) + "; uassert code 17049");
+
+	var rhsSet = Helpers.arrayToSet(rhs),
+		lhsArray = lhs,
+		returnVec = [];
+	for (var i = 0, l = lhsArray.length; i < l; ++i) {
+		// rhsSet serves the dual role of filtering out elements that were originally present
+		// in RHS and of eleminating duplicates from LHS
+		var it = lhsArray[i],
+			itHash = JSON.stringify(it);
+		if (!(itHash in rhsSet)) {
+			rhsSet[itHash] = it;
+			returnVec.push(it);
 		}
-	}, this);
+	}
 	return returnVec;
 };
 
-/** Register Expression */
 Expression.registerExpression("$setDifference", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$setDifference";
+};

+ 318 - 121
test/lib/pipeline/expressions/SetDifferenceExpression.js

@@ -1,128 +1,325 @@
 "use strict";
 var assert = require("assert"),
-		SetDifferenceExpression = require("../../../../lib/pipeline/expressions/SetDifferenceExpression"),
-		VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
-		VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-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 = {
-
-	"SetDifferenceExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor() {
-					assert.doesNotThrow(function() {
-							new SetDifferenceExpression();
-					});
-			},
-
-			"should throw Error when constructing with args": function testConstructor() {
-					assert.throws(function() {
-							new SetDifferenceExpression("someArg");
-					});
-			}
-
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $setDifference": function testOpName() {
-					assert.equal(new SetDifferenceExpression().getOpName(), "$setDifference");
-			}
-
-		},
-
-		"#evaluateInternal()": {
-
-			beforeEach: function(){
-				this.compare = function(array1, array2, expected) {
-					var input = [array1,array2],
-						idGenerator = new VariablesIdGenerator(),
-						vps = new VariablesParseState(idGenerator),
-						expr = Expression.parseExpression("$setDifference", input, vps),
-						result = expr.evaluate({}),
-						msg = errMsg("$setDifference", input, expr.serialize(false), expected, result);
-					assert.deepEqual(JSON.stringify(result), JSON.stringify(expected), msg);
-				}
-			},
-
-			"Should fail if array1 is not an array": function testArg2() {
-				var array1 = "not an array",
-					array2 = [1, 2, 3, 4],
-					input = [array1,array2],
-					idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					expr = Expression.parseExpression("$setDifference", input, vps);
-				assert.throws(function() {
-					expr.evaluate({});
-				});
-			},
-
-			"Should fail if array2 is not an array": function testArg2() {
-				var array1 = [1, 2, 3, 4],
-					array2 = "not an array",
-					input = [array1,array2],
-					idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					expr = Expression.parseExpression("$setDifference", input, vps);
-				assert.throws(function() {
-					expr.evaluate({});
-				});
-			},
-
-			"Should fail if both are not an arrays": function testArg1andArg2() {
-				var array1 = "not an array",
-					array2 = "not an array",
-					input = [array1,array2],
-					idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					expr = Expression.parseExpression("$setDifference", input, vps);
-				assert.throws(function() {
-					expr.evaluate({});
-				});
-			},
-
-			"Should pass and return difference between the arrays": function testBasicAssignment(){
-				this.compare([1, 9, 2, 3, 4, 5], [5, 6, 7, 2, 8, 9], [1, 3, 4]);
-			},
-
-			"Should handle an empty array 1": function(){
-				this.compare([], [5, 6, 7, 2, 8, 9], []);
-			},
-
-			"should handle an empty array 2": function() {
-				this.compare([1, 2, 3, "4"], [], [1, 2, 3, "4"]);
-			},
-
-			"should know the difference between a string and a number": function(){
-				this.compare([1, 2], [1, "2"], [2]);
-			},
-
-			"should handle a null for Array1": function() {
-				this.compare(null, [], null);
-			},
-
-			"should handle a null for Array2": function() {
-				this.compare([], null, null);
-			},
-
-			"should handle duplicates in array1": function() {
-				this.compare([1,1], [], [1]);
-			}
+	SetDifferenceExpression = require("../../../../lib/pipeline/expressions/SetDifferenceExpression"),
+	ExpectedResultBase = require("./SetExpectedResultBase");
 
+// 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.SetDifferenceExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SetDifferenceExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new SetDifferenceExpression("someArg");
+			});
 		}
 
-	}
+	},
 
-};
+	"#getOpName()": {
+
+		"should return the correct op name; $setDifference": function() {
+			assert.equal(new SetDifferenceExpression().getOpName(), "$setDifference");
+		}
+
+	},
+
+	"#evaluate()": {
+
+		"should handle when sets are the same": function Same(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set has redundant items": function Redundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the both sets have redundant items": function DoubleRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a superset": function Super(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						$setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is a superset and has redundant items": function SuperWithRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						$setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a subset": function Sub(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets are the same but backwards": function SameBackwards(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [2, 1]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should handle when the sets do not overlap": function NoOverlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 4]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [],
+						// $setUnion: [1, 2, 4, 8],
+						$setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do overlap": function Overlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 2, 4]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [2],
+						// $setUnion: [1, 2, 4, 8],
+						$setDifference: [1],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is null": function LastNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], null],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						$setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is null": function FirstNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null, [1, 2]],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						$setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has no args": function NoArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has one arg": function OneArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty arg": function EmptyArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty left arg": function LeftArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty right arg": function RightArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], []],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [1, 2],
+						// $setIsSubset: false,
+						// $setEquals: false,
+						$setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args": function ManyArgs(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[8, 3], ["asdf", "foo"], [80.3, 34], [], [80.3, "foo", 11, "yay"]],
+					expected: {
+						// $setIntersection: [],
+						// $setEquals: false,
+						// $setUnion: [3, 8, 11, 34, 80.3, "asdf", "foo", "yay"],
+					},
+					error: [
+						// "$setIsSubset",
+						"$setDifference",
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args that are equal sets": function ManyArgsEqual(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 4], [1, 2, 2, 4], [4, 1, 2], [2, 1, 1, 4]],
+					expected: {
+						// $setIntersection: [1, 2, 4],
+						// $setEquals: false,
+						// $setUnion: [1, 2, 4],
+					},
+					error: [
+						// "$setIsSubset",
+						"$setDifference",
+					],
+				},
+			}).run();
+		},
+
+	},
+
+};