From b4329167b3d978cec9ebe59a9d6af2e39db88dab Mon Sep 17 00:00:00 2001 From: Francis Asante Date: Mon, 4 Jul 2016 00:39:14 +0200 Subject: [PATCH] fix aggregate sort operator --- CHANGELOG.md | 5 ++- bower.json | 2 +- mingo.js | 92 ++++++++++++++++++++++++++++++++------------- mingo.min.js | 2 +- mingo.min.map | 2 +- package.json | 2 +- test/aggregation.js | 19 +++++++++- test/custom.js | 2 +- 8 files changed, 91 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cecf034b..569e9e166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ Changelog ========= -Changes between releases are kept here beginning from v0.5.0 +Changes starting from v0.5.0 are tracked here + +## v0.6.5 / 2016-07-04 +- Fix incorrect de-duping of Date types in $sort aggregate operator. See [issue#23](https://github.com/kofrasa/mingo/pull/23) ## v0.6.4 / 2016-05-19 - Support matching against user-defined types. See [issue#22](https://github.com/kofrasa/mingo/issues/22) diff --git a/bower.json b/bower.json index 2bd10342c..688dc14b9 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "mingo", "main": "mingo.js", - "version": "0.6.4", + "version": "0.6.5", "homepage": "https://github.com/kofrasa/mingo", "authors": [ "Francis Asante " diff --git a/mingo.js b/mingo.js index 7d90e1a1c..c1044f839 100644 --- a/mingo.js +++ b/mingo.js @@ -1,4 +1,4 @@ -// Mingo.js 0.6.4 +// Mingo.js 0.6.5 // Copyright (c) 2016 Francis Asante // MIT @@ -11,7 +11,7 @@ var Mingo = {}, previousMingo; var _; - Mingo.VERSION = '0.6.3'; + Mingo.VERSION = '0.6.5'; // backup previous Mingo if (root != null) { @@ -860,21 +860,23 @@ if (!_.isEmpty(sortKeys) && _.isObject(sortKeys)) { var modifiers = _.keys(sortKeys); modifiers.reverse().forEach(function (key) { - var indexes = []; - var grouped = _.groupBy(collection, function (obj) { - var value = resolve(obj, key); - indexes.push(value); - return value; + var grouped = groupBy(collection, function (obj) { + return resolve(obj, key); }); - indexes = _.sortBy(_.uniq(indexes), function (item) { + var sortedIndex = {}; + var findIndex = function (k) { return sortedIndex[hashcode(k)]; } + + var indexKeys = _.sortBy(grouped.keys, function (item, i) { + sortedIndex[hashcode(item)] = i; return item; }); + if (sortKeys[key] === -1) { - indexes.reverse(); + indexKeys.reverse(); } collection = []; - _.each(indexes, function (item) { - Array.prototype.push.apply(collection, grouped[item]); + _.each(indexKeys, function (item) { + Array.prototype.push.apply(collection, grouped.groups[findIndex(item)]); }); }); } @@ -2099,7 +2101,7 @@ * Groups the collection into sets by the returned key * * @param collection - * @param fn + * @param fn {function} to compute the group key of an item in the collection */ function groupBy(collection, fn) { @@ -2108,28 +2110,22 @@ 'groups': [] }; + var lookup = {}; + _.each(collection, function (obj) { var key = fn(obj); + var h = hashcode(key); var index = -1; - if (_.isObject(key)) { - for (var i = 0; i < result.keys.length; i++) { - if (_.isEqual(key, result.keys[i])) { - index = i; - break; - } - } - } else { - index = _.indexOf(result.keys, key); - } - - if (index > -1) { - result.groups[index].push(obj); - } else { + if (_.isUndefined(lookup[h])) { + index = result.keys.length; + lookup[h] = index; result.keys.push(key); - result.groups.push([obj]); + result.groups.push([]); } + index = lookup[h]; + result.groups[index].push(obj); }); // assert this @@ -2140,6 +2136,48 @@ return result; } + // encode value to a unique string form that is easily reversable + function encode(value) { + if (_.isNull(value)) { + return "null"; + } else if (_.isUndefined(value)) { + return "undef"; + } else { + var type = value.constructor.name; + switch (type) { + case "Boolean": + return "b|" + value.toString(); + case "String": + return "s|" + value.toString(); + case "RegExp": + return "r|" + value.toString(); + case "Number": + return "n|" + value.toString(); + case "Date": + return "d|" + value.toISOString(); + case "Array": + return "a|" + JSON.stringify(_.map(value, function (v) { return encode(v); })); + case "Object": + return "o|" + JSON.stringify(_.mapObject(value, function (v) { return encode(v); })); + default: + return type + "|" + JSON.stringify(_.mapObject(value, function (v) { return encode(v); })); + } + } + } + + // http://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery + // http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ + function hashcode(value) { + var hash = 0, i, chr, len, s = encode(value); + if (s.length === 0) return hash; + for (i = 0, len = s.length; i < len; i++) { + chr = s.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + return hash.toString(); + } + /** * Returns the result of evaluating a $group operation over a collection * diff --git a/mingo.min.js b/mingo.min.js index 4ae6f33ec..f424cdb3b 100644 --- a/mingo.min.js +++ b/mingo.min.js @@ -1,2 +1,2 @@ -!function(r,n){"use strict";function t(r){for(var n=0;n=0?"i":"",o+=i.multiline||u.indexOf("m")>=0?"m":"",o+=i.global||u.indexOf("g")>=0?"g":"",i=new RegExp(i,o)),r.$regex=i,delete r.$options}}return r}function u(r,n){return p.result(r,n)}function o(r,t){if(!t)return n;for(var e,i=t.split("."),s=r,a=0;a-1?t.groups[i].push(r):(t.keys.push(e),t.groups.push([r]))}),t.keys.length!==t.groups.length)throw new Error("assert groupBy(): keys.length !== groups.length");return t}function f(r,t,e){if(p.contains(a(P),t))return O[t](r,e);if(p.isObject(e)){var i={};for(var u in e)if(p.has(e,u)&&(i[u]=f(r,u,e[u]),p.contains(a(P),u))){if(i=i[u],p.keys(e).length>1)throw new Error("Invalid $group expression '"+JSON.stringify(e)+"'");break}return i}return n}function l(r,n,t){if(p.contains(a(T),t))return C[t](r,n);if(p.isString(n)&&n.length>0&&"$"===n[0])return o(r,n.slice(1));var i;if(e(n))return n;if(p.isArray(n))i=p.map(n,function(n){return l(r,n,null)});else if(p.isObject(n)){i={};for(var u in n)if(p.has(n,u)&&(i[u]=l(r,n[u],u),p.contains(a(T),u))){if(i=i[u],p.keys(n).length>1)throw new Error("Invalid aggregation expression '"+JSON.stringify(n)+"'");break}}return i}var h,p,y={};y.VERSION="0.6.3",null!=r&&(h=r.Mingo),y.noConflict=function(){return r.Mingo=h,y};var g="undefined"!=typeof module&&"undefined"!=typeof require;g?("undefined"!=typeof module&&(module.exports=y),p=require("underscore")):(r.Mingo=y,p=r._);var v=[p.isString,p.isBoolean,p.isNumber,p.isDate,p.isNull,p.isRegExp,p.isUndefined],$=["Object","Array"],d={key:"_id"};if(y.setup=function(r){p.extend(d,r||{})},y.Query=function(r,n){return this instanceof y.Query?(this._criteria=r,this._projection=n,this._compiled=[],void this._compile()):new y.Query(r,n)},y.Query.prototype={_compile:function(){if(!p.isEmpty(this._criteria)){if(p.isArray(this._criteria)||p.isFunction(this._criteria)||!p.isObject(this._criteria))throw new Error("Invalid type for criteria");for(var r in this._criteria)if(p.has(this._criteria,r)){var n=this._criteria[r];if(p.contains(["$and","$or","$nor","$where"],r))this._processOperator(r,r,n);else{n=i(n);for(var t in n)p.has(n,t)&&this._processOperator(r,t,n[t])}}}},_processOperator:function(r,n,t){if(!p.contains(a(R),n))throw new Error("Invalid query operator '"+n+"' detected");this._compiled.push(E[n](r,t))},test:function(r){for(var n=0;n0){var t=new y.Aggregator(n);this._result=t.run(this._result,this._query)}return this._result},all:function(){return this._fetch()},first:function(){return this.count()>0?this._fetch()[0]:null},last:function(){return this.count()>0?this._fetch()[this.count()-1]:null},count:function(){return this._fetch().length},skip:function(r){return p.extend(this._operators,{$skip:r}),this},limit:function(r){return p.extend(this._operators,{$limit:r}),this},sort:function(r){return p.extend(this._operators,{$sort:r}),this},next:function(){return this.hasNext()?this._fetch()[this._position++]:null},hasNext:function(){return this.count()>this._position},max:function(r){return O.$max(this._fetch(),r)},min:function(r){return O.$min(this._fetch(),r)},map:function(r){return p.map(this._fetch(),r)},forEach:function(r){p.each(this._fetch(),r)}},y.Aggregator=function(r){return this instanceof y.Aggregator?void(this._operators=r):new y.Aggregator(r)},y.Aggregator.prototype={run:function(r,n){if(!p.isEmpty(this._operators))for(var t=0;t1?!1:i[0],i!==!1&&p.contains(a(B),i)){var o=x[i](c,e[i],r);p.isUndefined(o)||(t=o),"$slice"==i&&(h=!0)}else t=l(c,e,r)}else g.push(r);p.isUndefined(t)||(f[r]=t)}),(h||y||i)&&(f=p.defaults(f,p.omit(c,g))),t.push(f)}return t},$limit:function(r,n){return p.first(r,n)},$skip:function(r,n){return p.rest(r,n)},$unwind:function(r,n){for(var t=[],e=n.substr(1),i=0;i0},$nin:function(r,n){return p.isUndefined(r)||!this.$in(r,n)},$lt:function(r,t){return r=p.isArray(r)?r:[r],r=p.find(r,function(r){return t>r}),r!==n},$lte:function(r,t){return r=p.isArray(r)?r:[r],r=p.find(r,function(r){return t>=r}),r!==n},$gt:function(r,t){return r=p.isArray(r)?r:[r],r=p.find(r,function(r){return r>t}),r!==n},$gte:function(r,t){return r=p.isArray(r)?r:[r],r=p.find(r,function(r){return r>=t}),r!==n},$mod:function(r,t){return r=p.isArray(r)?r:[r],r=p.find(r,function(r){return p.isNumber(r)&&p.isArray(t)&&2===t.length&&r%t[0]===t[1]}),r!==n},$regex:function(r,t){return r=p.isArray(r)?r:[r],r=p.find(r,function(r){return p.isString(r)&&p.isRegExp(t)&&!!r.match(t)}),r!==n},$exists:function(r,n){return n===!1&&p.isUndefined(r)||n===!0&&!p.isUndefined(r)},$all:function(r,n){var t=this,e=!1;if(p.isArray(r)&&p.isArray(n))for(var i=0;i=r&&-1===(r+"").indexOf(".");case 18:return p.isNumeric(r)&&r>2147483647&&0x8000000000000000>=r&&-1===(r+"").indexOf(".");default:return!1}}};p.each(p.keys(A),function(r){E[r]=function(r,n){return function(t,e){return{test:function(i){var u=o(i,t);return r.call(n,u,e)}}}}(A[r],A)});var x={$:function(r,n,t){throw new Error("$ not implemented")},$elemMatch:function(r,t,e){var i=o(r,e),u=new y.Query(t);if(p.isUndefined(i)||!p.isArray(i))return n;for(var s=0;sn?[n]:[0,n]}return Array.prototype.slice.apply(e,n)}},O={$addToSet:function(r,n){var t=p.map(r,function(r){return l(r,n,null)});return p.uniq(t)},$sum:function(r,n){return p.isArray(r)?p.isNumber(n)?r.length*n:p.reduce(r,function(r,t){var e=l(t,n,null);return p.isNumber(e)?r+e:r},0):0},$max:function(r,n){var t=p.max(r,function(r){return l(r,n,null)});return l(t,n,null)},$min:function(r,n){var t=p.min(r,function(r){return l(r,n,null)});return l(t,n,null)},$avg:function(r,n){return this.$sum(r,n)/(r.length||1)},$push:function(r,n){return p.map(r,function(r){return l(r,n,null)})},$first:function(r,t){return r.length>0?l(r[0],t):n},$last:function(r,t){return r.length>0?l(r[r.length-1],t):n}},b={$add:function(r,n){var t=l(r,n,null);return p.reduce(t,function(r,n){return r+n},0)},$subtract:function(r,n){var t=l(r,n,null);return t[0]-t[1]},$divide:function(r,n){var t=l(r,n,null);return t[0]/t[1]},$multiply:function(r,n){var t=l(r,n,null);return p.reduce(t,function(r,n){return r*n},1)},$mod:function(r,n){var t=l(r,n,null);return t[0]%t[1]}},j={$concat:function(r,t){var e=l(r,t,null);return p.contains(e,null)||p.contains(e,n)?null:e.join("")},$strcasecmp:function(r,n){var t=l(r,n,null);return t[0]=p.isEmpty(t[0])?"":t[0].toUpperCase(),t[1]=p.isEmpty(t[1])?"":t[1].toUpperCase(),t[0]>t[1]?1:t[0]u;u++){var a=J[i[u]],c=a;if(p.isArray(a)){var f=this[a[0]],h=a[1];c=s(f.call(this,r,e),h)}t=t.replace(i[u],c)}return t}},S={$setEquals:function(r,n){var t=l(r,n,null),e=p.uniq(t[0]),i=p.uniq(t[1]);return e.length!==i.length?!1:0==p.difference(e,i).length},$setIntersection:function(r,n){var t=l(r,n,null);return p.intersection(t[0],t[1])},$setDifference:function(r,n){var t=l(r,n,null);return p.difference(t[0],t[1])},$setUnion:function(r,n){var t=l(r,n,null);return p.union(t[0],t[1])},$setIsSubset:function(r,n){var t=l(r,n,null);return p.intersection(t[0],t[1]).length===t[0].length},$anyElementTrue:function(r,n){for(var t=l(r,n,null)[0],e=0;et[1]?1:t[0]=0?"i":"",o+=i.multiline||u.indexOf("m")>=0?"m":"",o+=i.global||u.indexOf("g")>=0?"g":"",i=new RegExp(i,o)),r.$regex=i,delete r.$options}}return r}function u(r,n){return y.result(r,n)}function o(r,t){if(!t)return n;for(var e,i=t.split("."),s=r,a=0;an;n++)t=u.charCodeAt(n),i=(i<<5)-i+t,i|=0;return i.toString()}function h(r,t,e){if(y.contains(a(T),t))return S[t](r,e);if(y.isObject(e)){var i={};for(var u in e)if(y.has(e,u)&&(i[u]=h(r,u,e[u]),y.contains(a(T),u))){if(i=i[u],y.keys(e).length>1)throw new Error("Invalid $group expression '"+JSON.stringify(e)+"'");break}return i}return n}function p(r,n,t){if(y.contains(a(Y),t))return P[t](r,n);if(y.isString(n)&&n.length>0&&"$"===n[0])return o(r,n.slice(1));var i;if(e(n))return n;if(y.isArray(n))i=y.map(n,function(n){return p(r,n,null)});else if(y.isObject(n)){i={};for(var u in n)if(y.has(n,u)&&(i[u]=p(r,n[u],u),y.contains(a(Y),u))){if(i=i[u],y.keys(n).length>1)throw new Error("Invalid aggregation expression '"+JSON.stringify(n)+"'");break}}return i}var g,y,v={};v.VERSION="0.6.5",null!=r&&(g=r.Mingo),v.noConflict=function(){return r.Mingo=g,v};var d="undefined"!=typeof module&&"undefined"!=typeof require;d?("undefined"!=typeof module&&(module.exports=v),y=require("underscore")):(r.Mingo=v,y=r._);var $=[y.isString,y.isBoolean,y.isNumber,y.isDate,y.isNull,y.isRegExp,y.isUndefined],m=["Object","Array"],w={key:"_id"};if(v.setup=function(r){y.extend(w,r||{})},v.Query=function(r,n){return this instanceof v.Query?(this._criteria=r,this._projection=n,this._compiled=[],void this._compile()):new v.Query(r,n)},v.Query.prototype={_compile:function(){if(!y.isEmpty(this._criteria)){if(y.isArray(this._criteria)||y.isFunction(this._criteria)||!y.isObject(this._criteria))throw new Error("Invalid type for criteria");for(var r in this._criteria)if(y.has(this._criteria,r)){var n=this._criteria[r];if(y.contains(["$and","$or","$nor","$where"],r))this._processOperator(r,r,n);else{n=i(n);for(var t in n)y.has(n,t)&&this._processOperator(r,t,n[t])}}}},_processOperator:function(r,n,t){if(!y.contains(a(J),n))throw new Error("Invalid query operator '"+n+"' detected");this._compiled.push(A[n](r,t))},test:function(r){for(var n=0;n0){var t=new v.Aggregator(n);this._result=t.run(this._result,this._query)}return this._result},all:function(){return this._fetch()},first:function(){return this.count()>0?this._fetch()[0]:null},last:function(){return this.count()>0?this._fetch()[this.count()-1]:null},count:function(){return this._fetch().length},skip:function(r){return y.extend(this._operators,{$skip:r}),this},limit:function(r){return y.extend(this._operators,{$limit:r}),this},sort:function(r){return y.extend(this._operators,{$sort:r}),this},next:function(){return this.hasNext()?this._fetch()[this._position++]:null},hasNext:function(){return this.count()>this._position},max:function(r){return S.$max(this._fetch(),r)},min:function(r){return S.$min(this._fetch(),r)},map:function(r){return y.map(this._fetch(),r)},forEach:function(r){y.each(this._fetch(),r)}},v.Aggregator=function(r){return this instanceof v.Aggregator?void(this._operators=r):new v.Aggregator(r)},v.Aggregator.prototype={run:function(r,n){if(!y.isEmpty(this._operators))for(var t=0;t1?!1:i[0],i!==!1&&y.contains(a(F),i)){var o=b[i](c,e[i],r);y.isUndefined(o)||(t=o),"$slice"==i&&(l=!0)}else t=p(c,e,r)}else g.push(r);y.isUndefined(t)||(f[r]=t)}),(l||h||i)&&(f=y.defaults(f,y.omit(c,g))),t.push(f)}return t},$limit:function(r,n){return y.first(r,n)},$skip:function(r,n){return y.rest(r,n)},$unwind:function(r,n){for(var t=[],e=n.substr(1),i=0;i0},$nin:function(r,n){return y.isUndefined(r)||!this.$in(r,n)},$lt:function(r,t){return r=y.isArray(r)?r:[r],r=y.find(r,function(r){return t>r}),r!==n},$lte:function(r,t){return r=y.isArray(r)?r:[r],r=y.find(r,function(r){return t>=r}),r!==n},$gt:function(r,t){return r=y.isArray(r)?r:[r],r=y.find(r,function(r){return r>t}),r!==n},$gte:function(r,t){return r=y.isArray(r)?r:[r],r=y.find(r,function(r){return r>=t}),r!==n},$mod:function(r,t){return r=y.isArray(r)?r:[r],r=y.find(r,function(r){return y.isNumber(r)&&y.isArray(t)&&2===t.length&&r%t[0]===t[1]}),r!==n},$regex:function(r,t){return r=y.isArray(r)?r:[r],r=y.find(r,function(r){return y.isString(r)&&y.isRegExp(t)&&!!r.match(t)}),r!==n},$exists:function(r,n){return n===!1&&y.isUndefined(r)||n===!0&&!y.isUndefined(r)},$all:function(r,n){var t=this,e=!1;if(y.isArray(r)&&y.isArray(n))for(var i=0;i=r&&-1===(r+"").indexOf(".");case 18:return y.isNumeric(r)&&r>2147483647&&0x8000000000000000>=r&&-1===(r+"").indexOf(".");default:return!1}}};y.each(y.keys(x),function(r){A[r]=function(r,n){return function(t,e){return{test:function(i){var u=o(i,t);return r.call(n,u,e)}}}}(x[r],x)});var b={$:function(r,n,t){throw new Error("$ not implemented")},$elemMatch:function(r,t,e){var i=o(r,e),u=new v.Query(t);if(y.isUndefined(i)||!y.isArray(i))return n;for(var s=0;sn?[n]:[0,n]}return Array.prototype.slice.apply(e,n)}},S={$addToSet:function(r,n){var t=y.map(r,function(r){return p(r,n,null)});return y.uniq(t)},$sum:function(r,n){return y.isArray(r)?y.isNumber(n)?r.length*n:y.reduce(r,function(r,t){var e=p(t,n,null);return y.isNumber(e)?r+e:r},0):0},$max:function(r,n){var t=y.max(r,function(r){return p(r,n,null)});return p(t,n,null)},$min:function(r,n){var t=y.min(r,function(r){return p(r,n,null)});return p(t,n,null)},$avg:function(r,n){return this.$sum(r,n)/(r.length||1)},$push:function(r,n){return y.map(r,function(r){return p(r,n,null)})},$first:function(r,t){return r.length>0?p(r[0],t):n},$last:function(r,t){return r.length>0?p(r[r.length-1],t):n}},j={$add:function(r,n){var t=p(r,n,null);return y.reduce(t,function(r,n){return r+n},0)},$subtract:function(r,n){var t=p(r,n,null);return t[0]-t[1]},$divide:function(r,n){var t=p(r,n,null);return t[0]/t[1]},$multiply:function(r,n){var t=p(r,n,null);return y.reduce(t,function(r,n){return r*n},1)},$mod:function(r,n){var t=p(r,n,null);return t[0]%t[1]}},N={$concat:function(r,t){var e=p(r,t,null);return y.contains(e,null)||y.contains(e,n)?null:e.join("")},$strcasecmp:function(r,n){var t=p(r,n,null);return t[0]=y.isEmpty(t[0])?"":t[0].toUpperCase(),t[1]=y.isEmpty(t[1])?"":t[1].toUpperCase(),t[0]>t[1]?1:t[0]u;u++){var a=G[i[u]],c=a;if(y.isArray(a)){var f=this[a[0]],l=a[1];c=s(f.call(this,r,e),l)}t=t.replace(i[u],c)}return t}},M={$setEquals:function(r,n){var t=p(r,n,null),e=y.uniq(t[0]),i=y.uniq(t[1]);return e.length!==i.length?!1:0==y.difference(e,i).length},$setIntersection:function(r,n){var t=p(r,n,null);return y.intersection(t[0],t[1])},$setDifference:function(r,n){var t=p(r,n,null);return y.difference(t[0],t[1])},$setUnion:function(r,n){var t=p(r,n,null);return y.union(t[0],t[1])},$setIsSubset:function(r,n){var t=p(r,n,null);return y.intersection(t[0],t[1]).length===t[0].length},$anyElementTrue:function(r,n){for(var t=p(r,n,null)[0],e=0;et[1]?1:t[0]