Browse Source

EAGLESIX-2651: SetIntersection: better sync w/ 2.6.5 code and tests, add more methods to ValueSet for this

Kyle P Davis 11 years ago
parent
commit
bb5c917503

+ 27 - 37
lib/pipeline/expressions/SetIntersectionExpression.js

@@ -2,70 +2,60 @@
 
 /**
  * A $setintersection pipeline expression.
- * @see evaluateInternal
  * @class SetIntersectionExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetIntersectionExpression = module.exports = function SetIntersectionExpression() {
-	if(arguments.length !== 0) throw new Error("SetIntersectionExpression constructor takes no args");
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
 }, klass = SetIntersectionExpression, base = require("./VariadicExpressionT")(SetIntersectionExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression"),
-	Helpers = require("./Helpers");
+	ValueSet = require("./ValueSet");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setIntersection";
-};
-
-/**
- * Takes any number of arrays. Returns the intersection of the arrays (again, duplicates are ignored).
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var n = this.operands.length,
-		currentIntersection = {};
-
+		currentIntersection = new ValueSet();
 	for (var i = 0; i < n; i++){
-
 		var nextEntry = this.operands[i].evaluateInternal(vars);
-		if (nextEntry == null || nextEntry == undefined){
+		if (nextEntry === undefined || nextEntry === null){
 			return null;
 		}
-		if (! (nextEntry instanceof Array )) throw new Error("Uassert 17047: All operands of " + this.getOpName() + "must be arrays. One argument is of type: " + typeof array1);
+		if (!(nextEntry instanceof Array))
+			 throw new Error("All operands of " + this.getOpName() + "must be arrays. One " +
+				"argument is of type: " + Value.getType(nextEntry) + "; uassert code 17047");
 
 		if (i === 0){
-			currentIntersection = Helpers.arrayToSet(nextEntry);
+			currentIntersection.insertRange(nextEntry);
 		} else {
-
-			var nextSet = Helpers.arrayToSet(nextEntry);
-			if (Object.keys(currentIntersection).length > Object.keys(nextSet).length){
-				var temp = currentIntersection;
-					currentIntersection = nextSet;
-					nextSet = temp;
+			var nextSet = new ValueSet(nextEntry);
+			if (currentIntersection.size() > nextSet.size()) {
+				// to iterate over whichever is the smaller set
+				nextSet.swap(currentIntersection);
 			}
-
-			Object.keys(currentIntersection).forEach(function (key){
-				if (Object.keys(nextSet).indexOf(key) < 0){
-					delete currentIntersection[key]
+			for (var itKey in currentIntersection.set) {
+				if (!nextSet.hasKey(itKey)) {
+					currentIntersection.eraseKey(itKey);
 				}
-			});
+			}
 		}
-
-		if(currentIntersection === {}){
+		if (currentIntersection.empty()) {
 			break;
 		}
 	}
+	var result = currentIntersection.values();
+	return result;
+};
 
-	var result = Helpers.setToArray(currentIntersection);
+Expression.registerExpression("$setIntersection", base.parse);
 
-	return Value.consume(result);
+proto.getOpName = function getOpName() {
+	return "$setIntersection";
 };
 
-/** Register Expression */
-Expression.registerExpression("$setIntersection", base.parse);
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

+ 38 - 0
lib/pipeline/expressions/ValueSet.js

@@ -1,3 +1,12 @@
+"use strict";
+
+/**
+ * Somewhat mimics the ValueSet in mongo (std::set<Value>)
+ * @class valueSet
+ * @namespace mungedb-aggregate.pipeline.expressions
+ * @module mungedb-aggregate
+ * @constructor
+ */
 var ValueSet = module.exports = function ValueSet(vals) {
 	this.set = {};
 	if (vals instanceof Array)
@@ -9,6 +18,7 @@ proto._getKey = JSON.stringify;
 proto.hasKey = function hasKey(key) {
 	return key in this.set;
 };
+//SKIPPED: proto.count -- use hasKey instead
 
 proto.has = function has(val) {
 	return this._getKey(val) in this.set;
@@ -48,3 +58,31 @@ proto.values = function values() {
 		vals.push(this.set[key]);
 	return vals;
 };
+
+proto.size = function values() {
+	var n = 0;
+	for (var key in this.set) //jshint ignore:line
+		n++;
+	return n;
+};
+
+proto.swap = function swap(other) {
+	var tmp = this.set;
+	this.set = other.set;
+	other.set = tmp;
+};
+
+proto.eraseKey = function eraseKey(key) {
+	delete this.set[key];
+};
+
+proto.erase = function erase(val) {
+	var key = this._getKey(val);
+	this.eraseKey(key);
+};
+
+proto.empty = function empty() {
+	for (var key in this.set) //jshint ignore:line
+		return false;
+	return true;
+};

+ 297 - 80
test/lib/pipeline/expressions/SetIntersectionExpression.js

@@ -1,108 +1,325 @@
 "use strict";
 var assert = require("assert"),
-	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
-	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	SetIntersectionExpression = require("../../../../lib/pipeline/expressions/SetIntersectionExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
+	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));
 
-function errMsg(expr, args, tree, expected, result) {
-	return 	"for expression " + expr +
-		" with argument " + args +
-		" full tree: " + JSON.stringify(tree) +
-		" expected: " + expected +
-		" result: " + result;
-}
+exports.SetIntersectionExpression = {
 
-module.exports = {
+	"constructor()": {
 
-	"SetIntersectionExpression": {
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SetIntersectionExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new SetIntersectionExpression("someArg");
+			});
+		},
+
+	},
 
-		"constructor()": {
+	"#getOpName()": {
 
-			"should not throw Error when constructing without args": function testConstructor() {
-				assert.doesNotThrow(function() {
-						new SetIntersectionExpression();
-				});
-			},
+		"should return the correct op name; $setIntersection": function() {
+			assert.equal(new SetIntersectionExpression().getOpName(), "$setIntersection");
+		}
+
+	},
 
-			"should throw Error when constructing with args": function testConstructor() {
-				assert.throws(function() {
-						new SetIntersectionExpression("someArg");
-				});
-			}
+	"#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();
 		},
 
-		"#getOpName()": {
+		"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 return the correct op name; $setIntersection": function testOpName() {
-				assert.equal(new SetIntersectionExpression().getOpName(), "$setIntersection");
-			}
+		"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();
 		},
 
-		"#evaluateInternal()": {
+		"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();
+		},
 
-			beforeEach: function(){
-				this.vps = new VariablesParseState(new VariablesIdGenerator());
-				this.checkNotArray = function(array1, array2) {
-					var input = [array1,array2],
-						expr = Expression.parseExpression("$setIntersection", input, this.vps);
-					assert.throws(function() {
-						expr.evaluate({});
-					});
-				};
-				this.checkIntersection = function(array1, array2, expected) {
-					var input = [array1,array2],
-						expr = Expression.parseExpression("$setIntersection", input, this.vps),
-						result = expr.evaluate({}),
-						msg = errMsg("$setIntersection", input, expr.serialize(false), expected, result);
-					assert.deepEqual(result, expected, msg);
-				};
-				this.checkIntersectionBothWays = function(array1, array2, expected) {
-					this.checkIntersection(array1, array2, expected);
-					this.checkIntersection(array2, array1, expected);
-				};
-			},
+		"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 fail if array1 is not an array": function testArg1() {
-				this.checkNotArray("not an array", [6, 7, 8, 9]);
-			},
+		"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();
+		},
 
-			"Should fail if array2 is not an array": function testArg2() {
-				this.checkNotArray([1, 2, 3, 4], "not an array");
-			},
+		"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 fail if both are not an array": function testArg1andArg2() {
-				this.checkNotArray("not an array","not an array");
-			},
+		"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 pass and return [2, 3]": function testBasicAssignment(){
-				this.checkIntersectionBothWays([2,3], [1, 2, 3, 4, 5], [2, 3]);
-			},
+		"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 pass and return []": function() {
-				this.checkIntersectionBothWays([1, 2, 3, 4, 5], [7, 8, 9], []);
-			},
+		"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 a set is empty": function() {
-				this.checkIntersectionBothWays([], [7, 8, 9], []);
-			},
+		"should handle when the input has no args": function NoArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [],
+					expected: {
+						$setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
 
-			"Should work when we touch the ends": function() {
-				this.checkIntersectionBothWays([7, 9], [7, 8, 9], [7, 9]);
-			},
+		"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 work when both sets are empty": function() {
-				this.checkIntersectionBothWays([], [], []);
-			},
+		"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 return a null when an array is null": function() {
-				this.checkIntersectionBothWays(null, [1], null);
-			}
-		}
-	}
-};
+		"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();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"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: true,
+						// $setUnion: [1, 2, 4],
+					},
+					error: [
+						// "$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+	},
+
+};