From 0bfc402eaa0dcacad1424f0af1829608c511d23c Mon Sep 17 00:00:00 2001 From: James Kyle Date: Wed, 22 Oct 2014 12:00:19 -0700 Subject: [PATCH] Add _.transform method --- test/collections.js | 47 +++++++++++++++++++++++++++++++++++++++++++++ underscore.js | 25 ++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/test/collections.js b/test/collections.js index 2c14f7ec8..184157fa9 100644 --- a/test/collections.js +++ b/test/collections.js @@ -187,6 +187,53 @@ strictEqual(_.reduceRight, _.foldr, 'alias for reduceRight'); }); + test('transform', function() { + var list = _.transform(['foo', 'bar', 'baz'], function(accumulator, value, index){ accumulator[index] = value + '2'; }); + deepEqual(list, ['foo2', 'bar2', 'baz2'], 'handles arrays with no accumulator'); + + list = _.transform(['foo', 'bar', 'baz'], function(accumulator, value, index){ accumulator[index] = value + '2'; }, []); + deepEqual(list, ['foo2', 'bar2', 'baz2'], 'handles arrays with array accumulator'); + + list = _.transform(['foo', 'bar', 'baz'], function(accumulator, value, index){ accumulator[index] = value + '2'; }, {}); + deepEqual(list, {0: 'foo2', 1: 'bar2', 2: 'baz2'}, 'handles arrays with object accumulator'); + + var obj = _.transform({foo: 1, bar: 2, baz: 3}, function(accumulator, value, key){ accumulator[key] = value + 1; }); + deepEqual(obj, {foo: 2, bar: 3, baz: 4}, 'handles objects with no accumulator'); + + obj = _.transform({foo: 1, bar: 2, baz: 3}, function(accumulator, value, key){ accumulator[key] = value + 1; }, {}); + deepEqual(obj, {foo: 2, bar: 3, baz: 4}, 'handles objects with array accumulator'); + + obj = _.transform({0: 'foo', 1: 'bar', 2: 'baz'}, function(accumulator, value, key){ accumulator[key] = value + '2'; }, []); + deepEqual(obj, ['foo2', 'bar2', 'baz2'], 'handles objects with object accumulator'); + + function Obj(props) { _.extend(this, props); } + var instance = new Obj({foo: 1, bar: 2, baz: 3}); + obj = _.transform(instance, function(accumulator, value, key){ accumulator[key] = value + 1; }); + ok(obj !== instance && obj instanceof Obj, 'creates a new instance of the object'); + deepEqual(obj, new Obj({foo: 2, bar: 3, baz: 4}), 'handles instances with no accumulator'); + + var context = {}, actualContext; + _.transform([1], function() { actualContext = this; }, {}, context); + strictEqual(actualContext, context, 'iterates with the correct context'); + + obj = {foo: 1}; + var accumulator = {}; + var args = [accumulator, 'foo', obj.foo, obj], actualArgs; + _.transform(obj, function() { actualArgs = _.toArray(args); }, accumulator); + deepEqual(args, actualArgs, 'iterates with the correct arguments'); + + accumulator = {}; + strictEqual(_.transform([1], function() {}, accumulator), accumulator); + + deepEqual(_.transform(), {}, 'should return an empty object when no obj or accumulator is passed'); + + accumulator = []; + strictEqual(_.transform(null, null, accumulator), accumulator, 'should return the accumulator when no obj is passed'); + + list = _.transform([1, 2, 3, 4], function(accumulator, value) { return value < 3 && accumulator.push(value); }); + deepEqual(list, [1, 2], 'transform should stop when false is returned.'); + }); + test('find', function() { var array = [1, 2, 3, 4]; strictEqual(_.find(array, function(n) { return n > 2; }), 3, 'should return first found `value`'); diff --git a/underscore.js b/underscore.js index 64eff7b7e..9dda00a04 100644 --- a/underscore.js +++ b/underscore.js @@ -199,6 +199,31 @@ return memo; }; + // **Transform** is an alternative to reduce that transforms `obj` to a new + // `accumulator` object. + _.transform = function(obj, iteratee, accumulator, context) { + if (accumulator == null) { + if (_.isArray(obj)) { + accumulator = []; + } else if (_.isObject(obj)) { + var Ctor = obj.constructor; + accumulator = baseCreate(typeof Ctor == 'function' && Ctor.prototype); + } else { + accumulator = {}; + } + } + if (obj == null) return accumulator; + iteratee = optimizeCb(iteratee, context, 4); + var keys = obj.length !== +obj.length && _.keys(obj), + length = (keys || obj).length, + index, currentKey; + for (index = 0; index < length; index++) { + currentKey = keys ? keys[index] : index; + if (iteratee(accumulator, obj[currentKey], currentKey, obj) === false) break; + } + return accumulator; + }; + // Return the first value which passes a truth test. Aliased as `detect`. _.find = _.detect = function(obj, predicate, context) { var key;