Skip to content

Commit

Permalink
Merge pull request #286 from brentpayne/feature/pick-o-nk
Browse files Browse the repository at this point in the history
reduce the runtime of `pick`, fixes #285
  • Loading branch information
vqvu committed May 25, 2015
2 parents 61278fb + ed98365 commit 30a8638
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 42 deletions.
87 changes: 53 additions & 34 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ var _ = exports;
var slice = Array.prototype.slice;
var hasOwn = Object.prototype.hasOwnProperty;

// ES5 detected value, used for switch between ES5 and ES3 code
var isES5 = (function () {
'use strict';
return Function.prototype.bind && !this;
}());


_.isUndefined = function (x) {
return typeof x === 'undefined';
};
Expand Down Expand Up @@ -1698,10 +1705,29 @@ Stream.prototype.pluck = function (prop) {
};
exposeMethod('pluck');

/**
* Only applies the transformation strategy on Objects.
* This helper is used in `pick` and `pickBy`
**/

var objectOnly = _.curry(function(strategy, x) {
if (_.isObject(x)) {
return strategy(x);
}
else {
throw new Error(
'Expected Object, got ' + (typeof x)
);
}
});


/**
*
* Retrieves copies of all the enumerable elements in the collection
* that satisfy a given predicate.
* Retrieves copies of all the elements in the collection
* that satisfy a given predicate. Note: When using ES3,
* only enumerable elements are selected. Both enumerable
* and non-enumerable elements are selected when using ES5.
*
* @id pickBy
* @section Transforms
Expand All @@ -1728,38 +1754,35 @@ exposeMethod('pluck');
*/

Stream.prototype.pickBy = function (f) {

return this.consume(function (err, x, push, next) {
return this.map(objectOnly(function (x) {
var out = {};
if (err) {
push(err);
next();
var seen = Object.create(null); // prevents testing overridden properties multiple times.
var obj = x; // variable used to traverse prototype chain
function testAndAdd (prop) {
if (!(prop in seen) && f(prop, x[prop])) {
out[prop] = x[prop];
}
seen[prop] = true;
}
else if (x === nil) {
push(err, x);
if (isES5) {
do {
Object.getOwnPropertyNames(obj).forEach(testAndAdd);
obj = Object.getPrototypeOf(obj);
} while (obj);
}
else if (_.isObject(x)) {
else {
for (var k in x) {
if (f(k, x[k])) {
out[k] = x[k];
}
testAndAdd(k);
}
push(null, out);
next();
}
else {
push(new Error(
'Expected Object, got ' + (typeof x)
));
next();
}
});
return out;
}));
};
exposeMethod('pickBy');

/**
*
* Retrieves copies of all enumerable elements in the collection,
* Retrieves copies of all elements in the collection,
* with only the whitelisted keys. If one of the whitelisted
* keys does not exist, it will be ignored.
*
Expand Down Expand Up @@ -1794,14 +1817,16 @@ exposeMethod('pickBy');
* });*/

Stream.prototype.pick = function (properties) {
return this.pickBy(function (key) {
return this.map(objectOnly(function(x) {
var out = {};
for (var i = 0, length = properties.length; i < length; i++) {
if (properties[i] === key) {
return true;
var p = properties[i];
if (p in x) {
out[p] = x[p];
}
}
return false;
});
return out;
}));
};
exposeMethod('pick');

Expand Down Expand Up @@ -4027,12 +4052,6 @@ _.wrapCallback = function (f) {
* });
*/


var isES5 = (function () {
'use strict';
return Function.prototype.bind && !this;
}());

function isClass (fn) {
if (!(typeof fn === 'function' && fn.prototype)) { return false; }
var getKeys = isES5 ? Object.getOwnPropertyNames : keys;
Expand Down
129 changes: 121 additions & 8 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3389,13 +3389,42 @@ exports['pick - non-existant property'] = function (test) {
test.done();
};

exports['pick - non-enumerable properties'] = function (test) {
test.expect(5);
var aObj = {breed: 'labrador',
name: 'Rocky',
owner: 'Adrian',
color: 'chocolate'
}
Object.defineProperty(aObj, 'age', {enumerable:false, value:12});
delete aObj.owner;
aObj.name = undefined;

var a = _([
aObj // <- owner delete, name undefined, age non-enumerable
]);


a.pick(['breed', 'age', 'name', 'owner']).toArray(function (xs) {
test.equal(xs[0].breed, 'labrador');
test.equal(xs[0].age, 12);
test.ok(xs[0].hasOwnProperty('name'));
test.ok(typeof(xs[0].name) === 'undefined');
// neither owner nor color was selected
test.ok(Object.keys(xs[0]).length === 3);
});


test.done();
};

exports['pickBy'] = function (test) {
test.expect(4);

var objs = [{a: 1, _a: 2}, {a: 1, _c: 3}];

_(objs).pickBy(function (key) {
return key.indexOf('_') === 0;
_(objs).pickBy(function (key, value) {
return key.indexOf('_') === 0 && typeof value !== 'function';
}).toArray(function (xs) {
test.deepEqual(xs, [{_a: 2}, {_c: 3}]);
});
Expand Down Expand Up @@ -3428,8 +3457,8 @@ exports['pickBy'] = function (test) {

var objs4 = [Object.create({a: 1, _a: 2}), {a: 1, _c: 3}];

_(objs4).pickBy(function (key) {
return key.indexOf('_') === 0;
_(objs4).pickBy(function (key, value) {
return key.indexOf('_') === 0 && typeof value !== 'function';
}).toArray(function (xs) {
test.deepEqual(xs, [{_a: 2}, {_c: 3}]);
});
Expand All @@ -3444,8 +3473,8 @@ exports['pickBy - non-existant property'] = function (test) {

var objs = [{a: 1, b: 2}, {a: 1, d: 3}];

_(objs).pickBy(function (key) {
return key.indexOf('_') === 0;
_(objs).pickBy(function (key, value) {
return key.indexOf('_') === 0 && typeof value !== 'function';
}).toArray(function (xs) {
test.deepEqual(xs, [{}, {}]);
});
Expand All @@ -3463,15 +3492,99 @@ exports['pickBy - non-existant property'] = function (test) {

var objs3 = [{}, {}];

_(objs3).pickBy(function (key) {
return key.indexOf('_') === 0;
_(objs3).pickBy(function (key, value) {
return key.indexOf('_') === 0 && typeof value !== 'function';
}).toArray(function (xs) {
test.deepEqual(xs, [{}, {}]);
});

test.done();
};

var isES5 = (function () {
'use strict';
return Function.prototype.bind && !this;
}());

exports['pickBy - non-enumerable properties'] = function (test) {
test.expect(5);
var aObj = {a: 5,
c: 5,
d: 10,
e: 10
}
Object.defineProperty(aObj, 'b', {enumerable:false, value:15});
delete aObj.c;
aObj.d = undefined;

var a = _([
aObj // <- c delete, d undefined, b non-enumerable but valid
]);


a.pickBy(function (key, value) {
if (key === 'b' || value === 5 || typeof value === 'undefined') {
return true
}
return false
}).toArray(function (xs) {
test.equal(xs[0].a, 5);
if (isES5) {
test.equal(xs[0].b, 15);
} else {
test.ok(typeof(xs[0].b) === 'undefined');
}
test.ok(xs[0].hasOwnProperty('d'));
test.ok(typeof(xs[0].d) === 'undefined');
// neither c nor e was selected, b is not selected by keys
if (isES5) {
test.ok(Object.keys(xs[0]).length === 3);
} else {
test.ok(Object.keys(xs[0]).length === 2);
}
});

test.done();
};

exports['pickBy - overridden properties'] = function (test) {
test.expect(7);
var aObj = {
a: 5,
c: 5,
d: 10,
e: 10,
valueOf: 10
}
var bObj = Object.create(aObj);
bObj.b = 10;
bObj.c = 10;
bObj.d = 5;

var a = _([
bObj
]);


a.pickBy(function (key, value) {
if (value > 7) {
return true
}
return false
}).toArray(function (xs) {
test.ok(typeof(xs[0].a) === 'undefined');
test.equal(xs[0].b, 10);
test.equal(xs[0].c, 10);
test.ok(typeof(xs[0].d) === 'undefined');
test.equal(xs[0].e, 10);
test.equal(xs[0].valueOf, 10);
test.ok(Object.keys(xs[0]).length === 4);
});

test.done();
};


exports['filter'] = function (test) {
test.expect(2);
function isEven(x) {
Expand Down

0 comments on commit 30a8638

Please sign in to comment.