Ver código fonte

Merge pull request #14 from RiveraGroup/feature/mongo_2.6.5_accumulators_Avg

Feature/mongo 2.6.5 accumulators Avg
Kyle P Davis 11 anos atrás
pai
commit
06666bd99a

+ 29 - 33
lib/pipeline/accumulators/AvgAccumulator.js

@@ -8,57 +8,53 @@
  * @constructor
  **/
 var AvgAccumulator = module.exports = function AvgAccumulator(){
-	this.subTotalName = "subTotal";
-	this.countName = "count";
-	this.totalIsANumber = true;
-	this.total = 0;
-	this.count = 0;
+	this.reset();
 	base.call(this);
 }, klass = AvgAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// NOTE: Skipping the create function, using the constructor instead
-
-// DEPENDENCIES
 var Value = require("../Value");
 
-// MEMBER FUNCTIONS
+var SUB_TOTAL_NAME = "subTotal";
+var COUNT_NAME = "count";
+
 proto.processInternal = function processInternal(input, merging) {
 	if (!merging) {
-		if (typeof input !== "number") {
-			return;
-		}
-		this.total += input;
-		this.count += 1;
+		// non numeric types have no impact on average
+		if (typeof input != "number") return;
+
+		this._total += input;
+		this._count += 1;
 	} else {
-		Value.verifyDocument(input);
-		this.total += input[this.subTotalName];
-		this.count += input[this.countName];
+		// We expect an object that contains both a subtotal and a count.
+		// This is what getValue(true) produced below.
+		if (!(input instanceof Object)) throw new Error("Assertion error");
+		this._total += input[SUB_TOTAL_NAME];
+		this._count += input[COUNT_NAME];
 	}
 };
 
-proto.getValue = function getValue(toBeMerged){
+klass.create = function create() {
+	return new AvgAccumulator();
+};
+
+proto.getValue = function getValue(toBeMerged) {
 	if (!toBeMerged) {
-		if (this.totalIsANumber && this.count > 0) {
-			return this.total / this.count;
-		} else if (this.count === 0) {
-			return 0;
-		} else {
-			throw new Error("$sum resulted in a non-numeric type");
-		}
+		if (this._count === 0)
+			return 0.0;
+		return this._total / this._count;
 	} else {
-		var ret = {};
-		ret[this.subTotalName] = this.total;
-		ret[this.countName] = this.count;
-
-		return ret;
+		var doc = {};
+		doc[SUB_TOTAL_NAME] = this._total;
+		doc[COUNT_NAME] = this._count;
+		return doc;
 	}
 };
 
 proto.reset = function reset() {
-	this.total = 0;
-	this.count = 0;
+	this._total = 0;
+	this._count = 0;
 };
 
-proto.getOpName = function getOpName(){
+proto.getOpName = function getOpName() {
 	return "$avg";
 };

+ 192 - 73
test/lib/pipeline/accumulators/AvgAccumulator.js

@@ -2,111 +2,230 @@
 var assert = require("assert"),
 	AvgAccumulator = require("../../../../lib/pipeline/accumulators/AvgAccumulator");
 
-function createAccumulator(){
-	return new AvgAccumulator();
-}
+// 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.AvgAccumulator = {
 
-	"AvgAccumulator": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AvgAccumulator();
-				});
-			}
+	".constructor()": {
 
+		"should not throw Error when constructing without args": function() {
+			new AvgAccumulator();
 		},
 
-		"#getOpName()": {
+	},
+
+	"#process()": {
 
-			"should return the correct op name; $avg": function testOpName(){
-				assert.strictEqual(new AvgAccumulator().getOpName(), "$avg");
-			}
+		"should allow numbers": function() {
+			assert.doesNotThrow(function() {
+				var acc = AvgAccumulator.create();
+				acc.process(1);
+			});
+		},
 
+		"should ignore non-numbers": function() {
+			assert.doesNotThrow(function() {
+				var acc = AvgAccumulator.create();
+				acc.process(true);
+				acc.process("Foo");
+				acc.process(new Date());
+				acc.process({});
+				acc.process([]);
+			});
 		},
 
-		"#processInternal()": {
+		"router": {
 
-			"should evaluate no documents": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				assert.strictEqual(avgAccumulator.getValue(), 0);
+			"should handle result from one shard": function testOneShard() {
+				var acc = AvgAccumulator.create();
+				acc.process({subTotal:3.0, count:2}, true);
+				assert.deepEqual(acc.getValue(), 3.0 / 2);
 			},
 
-			"should evaluate one document with a field that is NaN": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(Number("foo"));
-				// NaN is unequal to itself
-				assert.notStrictEqual(avgAccumulator.getValue(), avgAccumulator.getValue());
+			"should handle result from two shards": function testTwoShards() {
+				var acc = AvgAccumulator.create();
+				acc.process({subTotal:6.0, count:1}, true);
+				acc.process({subTotal:5.0, count:2}, true);
+				assert.deepEqual(acc.getValue(), 11.0 / 3);
 			},
 
+		},
+
+	},
+
+	".create()": {
+
+		"should create an instance": function() {
+			assert(AvgAccumulator.create() instanceof AvgAccumulator);
+		},
+
+	},
+
+	"#getValue()": {
 
-			"should evaluate one document and avg it's value": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(5);
-				assert.strictEqual(avgAccumulator.getValue(), 5);
+		"should return 0 if no inputs evaluated": function testNoDocsEvaluated() {
+			var acc = AvgAccumulator.create();
+			assert.equal(acc.getValue(), 0);
+		},
+
+		"should return one int": function testOneInt() {
+			var acc = AvgAccumulator.create();
+			acc.process(3);
+			assert.equal(acc.getValue(), 3);
+		},
+
+		"should return one long": function testOneLong() {
+			var acc = AvgAccumulator.create();
+			acc.process(-4e24);
+			assert.equal(acc.getValue(), -4e24);
+		},
+
+		"should return one double": function testOneDouble() {
+			var acc = AvgAccumulator.create();
+			acc.process(22.6);
+			assert.equal(acc.getValue(), 22.6);
+		},
+
+		"should return avg for two ints": function testIntInt() {
+			var acc = AvgAccumulator.create();
+			acc.process(10);
+			acc.process(11);
+			assert.equal(acc.getValue(), 10.5);
+		},
+
+		"should return avg for int and double": function testIntDouble() {
+			var acc = AvgAccumulator.create();
+			acc.process(10);
+			acc.process(11.0);
+			assert.equal(acc.getValue(), 10.5);
+		},
 
+		"should return avg for two ints w/o overflow": function testIntIntNoOverflow() {
+			var acc = AvgAccumulator.create();
+			acc.process(32767);
+			acc.process(32767);
+			assert.equal(acc.getValue(), 32767);
+		},
+
+		"should return avg for two longs w/o overflow": function testLongLongOverflow() {
+			var acc = AvgAccumulator.create();
+			acc.process(2147483647);
+			acc.process(2147483647);
+			assert.equal(acc.getValue(), (2147483647 + 2147483647) / 2);
+		},
+
+		"shard": {
+
+			"should return avg info for int": function testShardInt() {
+				var acc = AvgAccumulator.create();
+				acc.process(3);
+				assert.deepEqual(acc.getValue(true), {subTotal:3.0, count:1});
 			},
 
+			"should return avg info for long": function testShardLong() {
+				var acc = AvgAccumulator.create();
+				acc.process(5);
+				assert.deepEqual(acc.getValue(true), {subTotal:5.0, count:1});
+			},
 
-			"should evaluate and avg two ints": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(5);
-				avgAccumulator.processInternal(7);
-				assert.strictEqual(avgAccumulator.getValue(), 6);
+			"should return avg info for double": function testShardDouble() {
+				var acc = AvgAccumulator.create();
+				acc.process(116.0);
+				assert.deepEqual(acc.getValue(true), {subTotal:116.0, count:1});
 			},
 
-			"should evaluate and avg two ints overflow": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(Number.MAX_VALUE);
-				avgAccumulator.processInternal(Number.MAX_VALUE);
-				assert.strictEqual(Number.isFinite(avgAccumulator.getValue()), false);
+			beforeEach: function() { // used in the tests below
+				this.getAvgValueFor = function(a, b) { // kind of like TwoOperandBase
+					var acc = AvgAccumulator.create();
+					for (var i = 0, l = arguments.length; i < l; i++) {
+						acc.process(arguments[i]);
+					}
+					return acc.getValue(true);
+				};
 			},
 
+			"should return avg info for two ints w/ overflow": function testShardIntIntOverflow() {
+				var operand1 = 32767,
+					operand2 = 3,
+					expected = {subTotal: 32767 + 3.0, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
+			},
 
-			"should evaluate and avg two negative ints": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(-5);
-				avgAccumulator.processInternal(-7);
-				assert.strictEqual(avgAccumulator.getValue(), -6);
+			"should return avg info for int and long": function testShardIntLong() {
+				var operand1 = 5,
+					operand2 = 3e24,
+					expected = {subTotal: 5 + 3e24, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
 			},
 
-//TODO Not sure how to do this in Javascript
-//			"should evaluate and avg two negative ints overflow": function testStuff(){
-//				var avgAccumulator = createAccumulator();
-//				avgAccumulator.processInternal(Number.MIN_VALUE);
-//				avgAccumulator.processInternal(7);
-//				assert.strictEqual(avgAccumulator.getValue(), Number.MAX_VALUE);
-//			},
-//
-
-			"should evaluate and avg int and float": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(8.5);
-				avgAccumulator.processInternal(7);
-				assert.strictEqual(avgAccumulator.getValue(), 7.75);
+			"should return avg info for int and double": function testShardIntDouble() {
+				var operand1 = 5,
+					operand2 = 6.2,
+					expected = {subTotal: 5 + 6.2, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
 			},
 
-			"should evaluate and avg one Number and a NaN sum to NaN": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(8);
-				avgAccumulator.processInternal(Number("bar"));
-				// NaN is unequal to itself
-				assert.notStrictEqual(avgAccumulator.getValue(), avgAccumulator.getValue());
+			"should return avg info for long and double": function testShardLongDouble() {
+				var operand1 = 5e24,
+					operand2 = 1.0,
+					expected = {subTotal: 5e24 + 1.0, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
 			},
 
-			"should evaluate and avg a null value to 0": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(null);
-				assert.strictEqual(avgAccumulator.getValue(), 0);
-			}
+			"should return avg info for int and long and double": function testShardIntLongDouble() {
+				var operand1 = 1,
+					operand2 = 2e24,
+					operand3 = 4.0,
+					expected = {subTotal: 1 + 2e24 + 4.0, count: 3};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2, operand3), expected);
+			},
 
+		},
+
+		"should handle NaN": function() {
+			var acc = AvgAccumulator.create();
+			acc.process(NaN);
+			acc.process(1);
+			assert(isNaN(acc.getValue()));
+			acc = AvgAccumulator.create();
+			acc.process(1);
+			acc.process(NaN);
+			assert(isNaN(acc.getValue()));
+		},
+
+		"should handle null as 0": function() {
+			var acc = AvgAccumulator.create();
+			acc.process(null);
+			assert.equal(acc.getValue(), 0);
 		}
 
-	}
+	},
 
-};
+	"#reset()": {
+
+		"should reset to zero": function() {
+			var acc = AvgAccumulator.create();
+			assert.equal(acc.getValue(), 0);
+			acc.process(123);
+			assert.notEqual(acc.getValue(), 0);
+			acc.reset();
+			assert.equal(acc.getValue(), 0);
+			assert.deepEqual(acc.getValue(true), {subTotal:0, count:0});
+		}
+
+	},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+	"#getOpName()": {
+
+		"should return the correct op name; $avg": function() {
+			assert.equal(new AvgAccumulator().getOpName(), "$avg");
+		}
+
+	},
+
+};