From 345dff499ac5f1995061aeeb2ab6300454dcaf7f Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Tue, 31 May 2016 15:45:22 -0700 Subject: [PATCH 1/5] added mapValues --- lib/index.js | 9 ++++ lib/mapValues.js | 4 ++ lib/mapValuesLimit.js | 14 +++++++ lib/mapValuesSeries.js | 4 ++ mocha_test/mapValues.js | 92 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+) create mode 100644 lib/mapValues.js create mode 100644 lib/mapValuesLimit.js create mode 100644 lib/mapValuesSeries.js create mode 100644 mocha_test/mapValues.js diff --git a/lib/index.js b/lib/index.js index 1180dcf00..091f65742 100644 --- a/lib/index.js +++ b/lib/index.js @@ -43,6 +43,9 @@ import log from './log'; import map from './map'; import mapLimit from './mapLimit'; import mapSeries from './mapSeries'; +import mapValues from './mapValues'; +import mapValuesLimit from './mapValuesLimit'; +import mapValuesSeries from './mapValuesSeries'; import memoize from './memoize'; import nextTick from './nextTick'; import parallel from './parallel'; @@ -115,6 +118,9 @@ export default { map: map, mapLimit: mapLimit, mapSeries: mapSeries, + mapValues: mapValues, + mapValuesLimit: mapValuesLimit, + mapValuesSeries: mapValuesSeries, memoize: memoize, nextTick: nextTick, parallel: parallel, @@ -205,6 +211,9 @@ export { map as map, mapLimit as mapLimit, mapSeries as mapSeries, + mapValues as mapValues, + mapValuesLimit as mapValuesLimit, + mapValuesSeries as mapValuesSeries, memoize as memoize, nextTick as nextTick, parallel as parallel, diff --git a/lib/mapValues.js b/lib/mapValues.js new file mode 100644 index 000000000..60b82355b --- /dev/null +++ b/lib/mapValues.js @@ -0,0 +1,4 @@ +import mapValuesLimit from './mapValuesLimit'; +import doLimit from './internal/doLimit'; + +export default doLimit(mapValuesLimit, Infinity); diff --git a/lib/mapValuesLimit.js b/lib/mapValuesLimit.js new file mode 100644 index 000000000..f2698f04a --- /dev/null +++ b/lib/mapValuesLimit.js @@ -0,0 +1,14 @@ +import eachOfLimit from './eachOfLimit'; + +export default function mapValuesLimit(obj, limit, iteratee, callback) { + var newObj = {}; + eachOfLimit(obj, limit, function(val, key, next) { + iteratee(val, key, function (err, result) { + if (err) return next(err); + newObj[key] = result; + next(); + }) + }, function (err) { + callback(err, newObj); + }) +} diff --git a/lib/mapValuesSeries.js b/lib/mapValuesSeries.js new file mode 100644 index 000000000..f093a9e33 --- /dev/null +++ b/lib/mapValuesSeries.js @@ -0,0 +1,4 @@ +import mapValuesLimit from './mapValuesLimit'; +import doLimit from './internal/doLimit'; + +export default doLimit(mapValuesLimit, 1); diff --git a/mocha_test/mapValues.js b/mocha_test/mapValues.js new file mode 100644 index 000000000..3938be2d3 --- /dev/null +++ b/mocha_test/mapValues.js @@ -0,0 +1,92 @@ +var async = require('../lib'); +var expect = require('chai').expect; +var assert = require('assert'); + +describe('mapValues', function () { + var obj = {a: 1, b: 2, c: 3}; + + context('mapValuesLimit', function () { + it('basics', function (done) { + var running = 0 + var concurrency = { + a: 2, + b: 2, + c: 1 + } + async.mapValuesLimit(obj, 2, function (val, key, next) { + running++; + async.setImmediate(function () { + expect(running).to.equal(concurrency[key]); + running--; + next(null, key + val); + }); + }, function (err, result) { + expect(running).to.equal(0) + expect(err).to.eql(null); + expect(result).to.eql({a: 'a1', b: 'b2', c: 'c3'}) + done(); + }); + }); + + it('error', function (done) { + async.mapValuesLimit(obj, 1, function(val, key, next) { + if (key === 'b') { + return next(new Error("fail")); + } + next(null, val); + }, function (err, result) { + expect(err).to.not.eql(null); + expect(result).to.eql({a: 1}); + done(); + }); + }); + }); + + context('mapValues', function () { + it('basics', function (done) { + var running = 0 + var concurrency = { + a: 3, + b: 2, + c: 1 + } + async.mapValues(obj, function (val, key, next) { + running++; + async.setImmediate(function () { + expect(running).to.equal(concurrency[key]); + running--; + next(null, key + val); + }); + }, function (err, result) { + expect(running).to.equal(0) + expect(err).to.eql(null); + expect(result).to.eql({a: 'a1', b: 'b2', c: 'c3'}) + done(); + }); + }); + }); + + context('mapValuesSeries', function () { + it('basics', function (done) { + var running = 0 + var concurrency = { + a: 1, + b: 1, + c: 1 + } + async.mapValuesSeries(obj, function (val, key, next) { + running++; + async.setImmediate(function () { + expect(running).to.equal(concurrency[key]); + running--; + next(null, key + val); + }); + }, function (err, result) { + expect(running).to.equal(0) + expect(err).to.eql(null); + expect(result).to.eql({a: 'a1', b: 'b2', c: 'c3'}) + done(); + }); + }); + }); +}); From 44b41a0511dd0da6ce246e138b7b336f4b86788f Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Tue, 31 May 2016 16:03:16 -0700 Subject: [PATCH 2/5] add docs --- lib/mapValues.js | 42 ++++++++++++++++++++++++++++++++++++++++++ lib/mapValuesLimit.js | 19 +++++++++++++++++++ lib/mapValuesSeries.js | 17 +++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/lib/mapValues.js b/lib/mapValues.js index 60b82355b..83ffc43c3 100644 --- a/lib/mapValues.js +++ b/lib/mapValues.js @@ -1,4 +1,46 @@ import mapValuesLimit from './mapValuesLimit'; import doLimit from './internal/doLimit'; + +/** + * A relative of `map`, designed for use with objects. + * + * Produces a new Object by mapping each value of `obj` through the `iteratee` + * function. The `iteratee` is called each `value` and `key` from `obj` and a + * callback for when it has finished processing. Each of these callbacks takes + * two arguments: an `error`, and the transformed item from `obj`. If `iteratee` + * passes an error to its callback, the main `callback` (for the `mapValues` + * function) is immediately called with the error. + * + * Note, the order of the keys in the result is not guaranteed. The keys will + * be roughly in the order they complete, (but this is very engine-specific) + * + * @name mapValues + * @static + * @memberOf async + * @category Collection + * @param {Object} obj - A collection to iterate over. + * @param {Function} iteratee - A function to apply to each value and key in + * `coll`. The iteratee is passed a `callback(err, transformed)` which must be + * called once it has completed with an error (which can be `null`) and a + * transformed value. Invoked with (value, key, callback). + * @param {Function} [callback] - A callback which is called when all `iteratee` + * functions have finished, or an error occurs. Results is an array of the + * transformed items from the `obj`. Invoked with (err, result). + * @example + * + * async.mapValues({ + * f1: 'file1', + * f2: 'file2', + * f3: 'file3' + * }, fs.stat, function(err, result) { + * // results is now a map of stats for each file, e.g. + * // { + * // f1: [stats for file1], + * // f2: [stats for file2], + * // f3: [stats for file3] + * // } + * }); + */ + export default doLimit(mapValuesLimit, Infinity); diff --git a/lib/mapValuesLimit.js b/lib/mapValuesLimit.js index f2698f04a..a1a43a165 100644 --- a/lib/mapValuesLimit.js +++ b/lib/mapValuesLimit.js @@ -1,5 +1,24 @@ import eachOfLimit from './eachOfLimit'; +/** + * The same as `mapValues` but runs a maximum of `limit` async operations at a + * time. + * + * @name mapValuesLimit + * @static + * @memberOf async + * @see async.mapValues + * @category Collection + * @param {Object} obj - A collection to iterate over. + * @param {number} limit - The maximum number of async operations at a time. + * @param {Function} iteratee - A function to apply to each value in `obj`. + * The iteratee is passed a `callback(err, transformed)` which must be called + * once it has completed with an error (which can be `null`) and a + * transformed value. Invoked with (value, key, callback). + * @param {Function} [callback] - A callback which is called when all `iteratee` + * functions have finished, or an error occurs. Result is an object of the + * transformed values from the `obj`. Invoked with (err, result). + */ export default function mapValuesLimit(obj, limit, iteratee, callback) { var newObj = {}; eachOfLimit(obj, limit, function(val, key, next) { diff --git a/lib/mapValuesSeries.js b/lib/mapValuesSeries.js index f093a9e33..163d474d8 100644 --- a/lib/mapValuesSeries.js +++ b/lib/mapValuesSeries.js @@ -1,4 +1,21 @@ import mapValuesLimit from './mapValuesLimit'; import doLimit from './internal/doLimit'; +/** + * The same as `mapValues` but runs only a single async operation at a time. + * + * @name mapValuesSeries + * @static + * @memberOf async + * @see async.mapValues + * @category Collection + * @param {Object} obj - A collection to iterate over. + * @param {Function} iteratee - A function to apply to each value in `obj`. + * The iteratee is passed a `callback(err, transformed)` which must be called + * once it has completed with an error (which can be `null`) and a + * transformed value. Invoked with (value, key, callback). + * @param {Function} [callback] - A callback which is called when all `iteratee` + * functions have finished, or an error occurs. Result is an object of the + * transformed values from the `obj`. Invoked with (err, result). + */ export default doLimit(mapValuesLimit, 1); From 89f8fe4c3874e421f10c5d9ed5e9e1df98f8aaea Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Tue, 31 May 2016 16:05:17 -0700 Subject: [PATCH 3/5] fix lint errors --- lib/mapValuesLimit.js | 4 ++-- mocha_test/mapValues.js | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/mapValuesLimit.js b/lib/mapValuesLimit.js index a1a43a165..762b871e7 100644 --- a/lib/mapValuesLimit.js +++ b/lib/mapValuesLimit.js @@ -26,8 +26,8 @@ export default function mapValuesLimit(obj, limit, iteratee, callback) { if (err) return next(err); newObj[key] = result; next(); - }) + }); }, function (err) { callback(err, newObj); - }) + }); } diff --git a/mocha_test/mapValues.js b/mocha_test/mapValues.js index 3938be2d3..44eb27d4f 100644 --- a/mocha_test/mapValues.js +++ b/mocha_test/mapValues.js @@ -7,12 +7,12 @@ describe('mapValues', function () { context('mapValuesLimit', function () { it('basics', function (done) { - var running = 0 + var running = 0; var concurrency = { a: 2, b: 2, c: 1 - } + }; async.mapValuesLimit(obj, 2, function (val, key, next) { running++; async.setImmediate(function () { @@ -21,9 +21,9 @@ describe('mapValues', function () { next(null, key + val); }); }, function (err, result) { - expect(running).to.equal(0) + expect(running).to.equal(0); expect(err).to.eql(null); - expect(result).to.eql({a: 'a1', b: 'b2', c: 'c3'}) + expect(result).to.eql({a: 'a1', b: 'b2', c: 'c3'}); done(); }); }); @@ -44,12 +44,12 @@ describe('mapValues', function () { context('mapValues', function () { it('basics', function (done) { - var running = 0 + var running = 0; var concurrency = { a: 3, b: 2, c: 1 - } + }; async.mapValues(obj, function (val, key, next) { running++; async.setImmediate(function () { @@ -58,9 +58,9 @@ describe('mapValues', function () { next(null, key + val); }); }, function (err, result) { - expect(running).to.equal(0) + expect(running).to.equal(0); expect(err).to.eql(null); - expect(result).to.eql({a: 'a1', b: 'b2', c: 'c3'}) + expect(result).to.eql({a: 'a1', b: 'b2', c: 'c3'}); done(); }); }); @@ -68,12 +68,12 @@ describe('mapValues', function () { context('mapValuesSeries', function () { it('basics', function (done) { - var running = 0 + var running = 0; var concurrency = { a: 1, b: 1, c: 1 - } + }; async.mapValuesSeries(obj, function (val, key, next) { running++; async.setImmediate(function () { @@ -82,9 +82,9 @@ describe('mapValues', function () { next(null, key + val); }); }, function (err, result) { - expect(running).to.equal(0) + expect(running).to.equal(0); expect(err).to.eql(null); - expect(result).to.eql({a: 'a1', b: 'b2', c: 'c3'}) + expect(result).to.eql({a: 'a1', b: 'b2', c: 'c3'}); done(); }); }); From 5f6e76d9659c992a37b9ec488d6aff58652b918a Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Tue, 31 May 2016 16:36:09 -0700 Subject: [PATCH 4/5] have map always return arrays --- lib/internal/map.js | 12 ++++++++++-- lib/map.js | 6 +++++- mocha_test/map.js | 18 +++++++----------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/internal/map.js b/lib/internal/map.js index 45c3eae4d..b587f599c 100644 --- a/lib/internal/map.js +++ b/lib/internal/map.js @@ -1,15 +1,23 @@ import isArrayLike from 'lodash/isArrayLike'; import getIterator from './getIterator'; import noop from 'lodash/noop'; +import okeys from 'lodash/keys'; +import indexOf from 'lodash/_baseIndexOf'; import once from './once'; export default function _asyncMap(eachfn, arr, iteratee, callback) { callback = once(callback || noop); arr = arr || []; - var results = isArrayLike(arr) || getIterator(arr) ? [] : {}; + var results = []; + var keys; + if (!isArrayLike(arr) && !getIterator(arr)) { + keys = okeys(arr); + } + eachfn(arr, function (value, index, callback) { iteratee(value, function (err, v) { - results[index] = v; + var idx = keys ? indexOf(keys, index, 0) : index; + results[idx] = v; callback(err); }); }, function (err) { diff --git a/lib/map.js b/lib/map.js index 892456b5a..19642c829 100644 --- a/lib/map.js +++ b/lib/map.js @@ -14,6 +14,10 @@ import doLimit from './internal/doLimit'; * in order. However, the results array will be in the same order as the * original `coll`. * + * If `map` is passed an Object, the results will be an Array. The results + * will roughly be in the order of the original Objects' keys (but this can + * vary across JavaScript engines) + * * @name map * @static * @memberOf async @@ -24,7 +28,7 @@ import doLimit from './internal/doLimit'; * once it has completed with an error (which can be `null`) and a * transformed item. Invoked with (item, callback). * @param {Function} [callback] - A callback which is called when all `iteratee` - * functions have finished, or an error occurs. Results is an array of the + * functions have finished, or an error occurs. Results is an Array of the * transformed items from the `coll`. Invoked with (err, results). * @example * diff --git a/mocha_test/map.js b/mocha_test/map.js index a53742730..e7d4fa310 100644 --- a/mocha_test/map.js +++ b/mocha_test/map.js @@ -122,12 +122,10 @@ describe("map", function() { callback(null, val * 2); }, function(err, result) { if (err) throw err; - expect(Object.prototype.toString.call(result)).to.equal('[object Object]'); - expect(result).to.eql({ - a: 2, - b: 4, - c: 6 - }); + expect(Object.prototype.toString.call(result)).to.equal('[object Array]'); + expect(result).to.contain(2); + expect(result).to.contain(4); + expect(result).to.contain(6); done(); }); }); @@ -170,11 +168,9 @@ describe("map", function() { callback(null, val * 2); }, function(err, result) { if (err) throw err; - expect(result).to.eql({ - a: 2, - b: 4, - c: 6 - }); + expect(result).to.contain(2); + expect(result).to.contain(4); + expect(result).to.contain(6); done(); }); }); From 611a4424b4490592fd037279f65e22630e66ffd4 Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Wed, 1 Jun 2016 13:04:09 -0700 Subject: [PATCH 5/5] simplify index tracking --- lib/internal/map.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/internal/map.js b/lib/internal/map.js index b587f599c..2c2a75016 100644 --- a/lib/internal/map.js +++ b/lib/internal/map.js @@ -1,23 +1,16 @@ -import isArrayLike from 'lodash/isArrayLike'; -import getIterator from './getIterator'; import noop from 'lodash/noop'; -import okeys from 'lodash/keys'; -import indexOf from 'lodash/_baseIndexOf'; import once from './once'; export default function _asyncMap(eachfn, arr, iteratee, callback) { callback = once(callback || noop); arr = arr || []; var results = []; - var keys; - if (!isArrayLike(arr) && !getIterator(arr)) { - keys = okeys(arr); - } + var counter = 0; - eachfn(arr, function (value, index, callback) { + eachfn(arr, function (value, _, callback) { + var index = counter++; iteratee(value, function (err, v) { - var idx = keys ? indexOf(keys, index, 0) : index; - results[idx] = v; + results[index] = v; callback(err); }); }, function (err) {