diff --git a/lib/index.js b/lib/index.js index 3bfa308..59ba1a5 100755 --- a/lib/index.js +++ b/lib/index.js @@ -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'; }; @@ -1693,10 +1700,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 @@ -1723,38 +1749,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. * @@ -1789,14 +1812,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'); @@ -3973,12 +3998,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; diff --git a/test/test.js b/test/test.js index c192bd1..799805c 100755 --- a/test/test.js +++ b/test/test.js @@ -3369,13 +3369,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}]); }); @@ -3408,8 +3437,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}]); }); @@ -3424,8 +3453,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, [{}, {}]); }); @@ -3443,8 +3472,8 @@ 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, [{}, {}]); }); @@ -3452,6 +3481,90 @@ exports['pickBy - non-existant property'] = function (test) { 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) {