Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mapping over Objects #1177

Merged
merged 5 commits into from
Jun 5, 2016
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -115,6 +118,9 @@ export default {
map: map,
mapLimit: mapLimit,
mapSeries: mapSeries,
mapValues: mapValues,
mapValuesLimit: mapValuesLimit,
mapValuesSeries: mapValuesSeries,
memoize: memoize,
nextTick: nextTick,
parallel: parallel,
Expand Down Expand Up @@ -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,
Expand Down
46 changes: 46 additions & 0 deletions lib/mapValues.js
Original file line number Diff line number Diff line change
@@ -0,0 +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);
33 changes: 33 additions & 0 deletions lib/mapValuesLimit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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) {
iteratee(val, key, function (err, result) {
if (err) return next(err);
newObj[key] = result;
next();
});
}, function (err) {
callback(err, newObj);
});
}
21 changes: 21 additions & 0 deletions lib/mapValuesSeries.js
Original file line number Diff line number Diff line change
@@ -0,0 +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);
92 changes: 92 additions & 0 deletions mocha_test/mapValues.js
Original file line number Diff line number Diff line change
@@ -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};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth testing with an array as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would the expected behavior be?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{0: <val>, 1: <val>, 2: <val>}

I would also be fine with leaving this behaviour implied


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]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I'm experimenting with this way of verifying concurrency. It should be more reliable than asserting a concurrency after some timeout value. Could help with some of our flaky tests.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, let me know if you want testingbot keys to try running in selenium

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd have to refactor other tests to be like this style, tracking concurrency and verifying things finish in a certain order, but it might be worth it 😓

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();
});
});
});
});