From 64704e16c14a0da7b7255cb4b729b8bbd46cf5ee Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sun, 6 Mar 2016 10:10:08 -0800 Subject: [PATCH] Add `_.flatMapDeep` and `_.flatMapDepth`. --- fp/_mapping.js | 21 +++--- lodash.js | 118 ++++++++++++++++++++------------ test/test.js | 178 +++++++++++++++++++++++++++---------------------- 3 files changed, 186 insertions(+), 131 deletions(-) diff --git a/fp/_mapping.js b/fp/_mapping.js index 1d33d4b0a..1dbf0288b 100644 --- a/fp/_mapping.js +++ b/fp/_mapping.js @@ -50,12 +50,12 @@ exports.aryMethod = { 'curryRightN', 'debounce', 'defaults', 'defaultsDeep', 'delay', 'difference', 'drop', 'dropRight', 'dropRightWhile', 'dropWhile', 'endsWith', 'eq', 'every', 'filter', 'find', 'find', 'findIndex', 'findKey', 'findLast', 'findLastIndex', - 'findLastKey', 'flatMap', 'flattenDepth', 'forEach', 'forEachRight', 'forIn', - 'forInRight', 'forOwn', 'forOwnRight', 'get', 'groupBy', 'gt', 'gte', 'has', - 'hasIn', 'includes', 'indexOf', 'intersection', 'invertBy', 'invoke', 'invokeMap', - 'isEqual', 'isMatch', 'join', 'keyBy', 'lastIndexOf', 'lt', 'lte', 'map', - 'mapKeys', 'mapValues', 'matchesProperty', 'maxBy', 'merge', 'minBy', 'omit', - 'omitBy', 'overArgs', 'pad', 'padEnd', 'padStart', 'parseInt', 'partial', + 'findLastKey', 'flatMap', 'flatMapDeep', 'flattenDepth', 'forEach', 'forEachRight', + 'forIn', 'forInRight', 'forOwn', 'forOwnRight', 'get', 'groupBy', 'gt', 'gte', + 'has', 'hasIn', 'includes', 'indexOf', 'intersection', 'invertBy', 'invoke', + 'invokeMap', 'isEqual', 'isMatch', 'join', 'keyBy', 'lastIndexOf', 'lt', 'lte', + 'map', 'mapKeys', 'mapValues', 'matchesProperty', 'maxBy', 'merge', 'minBy', + 'omit', 'omitBy', 'overArgs', 'pad', 'padEnd', 'padStart', 'parseInt', 'partial', 'partialRight', 'partition', 'pick', 'pickBy', 'pull', 'pullAll', 'pullAt', 'random', 'range', 'rangeRight', 'rearg', 'reject', 'remove', 'repeat', 'result', 'sampleSize', 'some', 'sortBy', 'sortedIndex', 'sortedIndexOf', 'sortedLastIndex', @@ -68,9 +68,10 @@ exports.aryMethod = { '3': [ 'assignInWith', 'assignWith', 'clamp', 'differenceBy', 'differenceWith', 'getOr', 'inRange', 'intersectionBy', 'intersectionWith', 'isEqualWith', - 'isMatchWith', 'mergeWith', 'orderBy', 'pullAllBy', 'pullAllWith', 'reduce', - 'reduceRight', 'replace', 'set', 'slice', 'sortedIndexBy', 'sortedLastIndexBy', - 'transform', 'unionBy', 'unionWith', 'update', 'xorBy', 'xorWith', 'zipWith' + 'isMatchWith', 'flatMapDepth', 'mergeWith', 'orderBy', 'pullAllBy', + 'pullAllWith', 'reduce', 'reduceRight', 'replace', 'set', 'slice', + 'sortedIndexBy', 'sortedLastIndexBy', 'transform', 'unionBy', 'unionWith', + 'update', 'xorBy', 'xorWith', 'zipWith' ], '4': [ 'fill', 'setWith', 'updateWith' @@ -101,6 +102,8 @@ exports.iterateeAry = { 'findLastIndex': 1, 'findLastKey': 1, 'flatMap': 1, + 'flatMapDeep': 1, + 'flatMapDepth': 1, 'forEach': 1, 'forEachRight': 1, 'forIn': 1, diff --git a/lodash.js b/lodash.js index 8a8c133dd..f6de166f2 100644 --- a/lodash.js +++ b/lodash.js @@ -1456,23 +1456,24 @@ * `curry`, `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`, * `difference`, `differenceBy`, `differenceWith`, `drop`, `dropRight`, * `dropRightWhile`, `dropWhile`, `extend`, `extendWith`, `fill`, `filter`, - * `flatten`, `flattenDeep`, `flattenDepth`, `flip`, `flow`, `flowRight`, - * `fromPairs`, `functions`, `functionsIn`, `groupBy`, `initial`, `intersection`, - * `intersectionBy`, `intersectionWith`, `invert`, `invertBy`, `invokeMap`, - * `iteratee`, `keyBy`, `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, - * `matches`, `matchesProperty`, `memoize`, `merge`, `mergeWith`, `method`, - * `methodOf`, `mixin`, `negate`, `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, - * `over`, `overArgs`, `overEvery`, `overSome`, `partial`, `partialRight`, - * `partition`, `pick`, `pickBy`, `plant`, `property`, `propertyOf`, `pull`, - * `pullAll`, `pullAllBy`, `pullAllWith`, `pullAt`, `push`, `range`, - * `rangeRight`, `rearg`, `reject`, `remove`, `rest`, `reverse`, `sampleSize`, - * `set`, `setWith`, `shuffle`, `slice`, `sort`, `sortBy`, `splice`, `spread`, - * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, `tap`, `throttle`, - * `thru`, `toArray`, `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, - * `transform`, `unary`, `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, - * `uniqWith`, `unset`, `unshift`, `unzip`, `unzipWith`, `update`, `values`, - * `valuesIn`, `without`, `wrap`, `xor`, `xorBy`, `xorWith`, `zip`, `zipObject`, - * `zipObjectDeep`, and `zipWith` + * `flatMap`, `flatMapDeep`, `flatMapDepth`, `flatten`, `flattenDeep`, + * `flattenDepth`, `flip`, `flow`, `flowRight`, `fromPairs`, `functions`, + * `functionsIn`, `groupBy`, `initial`, `intersection`, `intersectionBy`, + * `intersectionWith`, `invert`, `invertBy`, `invokeMap`, `iteratee`, `keyBy`, + * `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, `matchesProperty`, + * `memoize`, `merge`, `mergeWith`, `method`, `methodOf`, `mixin`, `negate`, + * `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, `over`, `overArgs`, + * `overEvery`, `overSome`, `partial`, `partialRight`, `partition`, `pick`, + * `pickBy`, `plant`, `property`, `propertyOf`, `pull`, `pullAll`, `pullAllBy`, + * `pullAllWith`, `pullAt`, `push`, `range`, `rangeRight`, `rearg`, `reject`, + * `remove`, `rest`, `reverse`, `sampleSize`, `set`, `setWith`, `shuffle`, + * `slice`, `sort`, `sortBy`, `splice`, `spread`, `tail`, `take`, `takeRight`, + * `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, `toArray`, + * `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, `transform`, `unary`, + * `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, `uniqWith`, `unset`, + * `unshift`, `unzip`, `unzipWith`, `update`, `values`, `valuesIn`, `without`, + * `wrap`, `xor`, `xorBy`, `xorWith`, `zip`, `zipObject`, `zipObjectDeep`, + * and `zipWith` * * The wrapper methods that are **not** chainable by default are: * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`, @@ -7587,27 +7588,6 @@ return new LodashWrapper(this.value(), this.__chain__); } - /** - * This method is the wrapper version of `_.flatMap`. - * - * @name flatMap - * @memberOf _ - * @category Seq - * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * function duplicate(n) { - * return [n, n]; - * } - * - * _([1, 2]).flatMap(duplicate).value(); - * // => [1, 1, 2, 2] - */ - function wrapperFlatMap(iteratee) { - return this.map(iteratee).flatten(); - } - /** * Gets the next value on a wrapped object following the * [iterator protocol](https://mdn.io/iteration_protocols#iterator). @@ -7938,9 +7918,9 @@ } /** - * Creates an array of flattened values by running each element in `collection` - * through `iteratee` and concating its result to the other mapped values. - * The iteratee is invoked with three arguments: (value, index|key, collection). + * Creates a flattened array of values by running each element in `collection` + * through `iteratee` and flattening the mapped results. The iteratee is invoked + * with three arguments: (value, index|key, collection). * * @static * @memberOf _ @@ -7962,6 +7942,56 @@ return baseFlatten(map(collection, iteratee), 1); } + /** + * This method is like `_.flatMap` except that it recursively flattens the + * mapped results. + * + * @static + * @memberOf _ + * @since 4.7.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new flattened array. + * @example + * + * function duplicate(n) { + * return [[[n, n]]]; + * } + * + * _.flatMapDeep([1, 2], duplicate); + * // => [1, 1, 2, 2] + */ + function flatMapDeep(collection, iteratee) { + return baseFlatten(map(collection, iteratee), INFINITY); + } + + /** + * This method is like `_.flatMap` except that it recursively flattens the + * mapped results up to `depth` times. + * + * @static + * @memberOf _ + * @since 4.7.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration. + * @param {number} [depth=1] The maximum recursion depth. + * @returns {Array} Returns the new flattened array. + * @example + * + * function duplicate(n) { + * return [[[n, n]]]; + * } + * + * _.flatMapDepth([1, 2], duplicate, 2); + * // => [[1, 1], [2, 2]] + */ + function flatMapDepth(collection, iteratee, depth) { + depth = depth === undefined ? 1 : toInteger(depth); + return baseFlatten(map(collection, iteratee), depth); + } + /** * Iterates over elements of `collection` invoking `iteratee` for each element. * The iteratee is invoked with three arguments: (value, index|key, collection). @@ -11938,8 +11968,7 @@ /** * Creates an object with the same keys as `object` and values generated by * running each own enumerable string keyed property of `object` through - * `iteratee`. The iteratee is invoked with three arguments: - * (value, key, object). + * `iteratee`. The iteratee is invoked with three arguments: (value, key, object). * * @static * @memberOf _ @@ -14896,6 +14925,8 @@ lodash.fill = fill; lodash.filter = filter; lodash.flatMap = flatMap; + lodash.flatMapDeep = flatMapDeep; + lodash.flatMapDepth = flatMapDepth; lodash.flatten = flatten; lodash.flattenDeep = flattenDeep; lodash.flattenDepth = flattenDepth; @@ -15387,7 +15418,6 @@ lodash.prototype.at = wrapperAt; lodash.prototype.chain = wrapperChain; lodash.prototype.commit = wrapperCommit; - lodash.prototype.flatMap = wrapperFlatMap; lodash.prototype.next = wrapperNext; lodash.prototype.plant = wrapperPlant; lodash.prototype.reverse = wrapperReverse; diff --git a/test/test.js b/test/test.js index f1cff6122..144ea3914 100644 --- a/test/test.js +++ b/test/test.js @@ -5696,32 +5696,62 @@ /*--------------------------------------------------------------------------*/ - QUnit.module('lodash.flatMap'); + QUnit.module('lodash.flatMapDepth'); (function() { - var array = [1, 2, 3, 4]; + var array = [1, [2, [3, [4]], 5]]; + + QUnit.test('should use a default `depth` of `1`', function(assert) { + assert.expect(1); + + assert.deepEqual(_.flatMapDepth(array), [1, 2, [3, [4]], 5]); + }); + + QUnit.test('should treat a `depth` of < `1` as a shallow clone', function(assert) { + assert.expect(2); + + lodashStable.each([-1, 0], function(depth) { + assert.deepEqual(_.flatMapDepth(array, identity, depth), [1, [2, [3, [4]], 5]]); + }); + }); + + QUnit.test('should coerce `depth` to an integer', function(assert) { + assert.expect(1); + + assert.deepEqual(_.flatMapDepth(array, identity, 2.2), [1, 2, 3, [4], 5]); + }); + }()); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('flatMap methods'); + + lodashStable.each(['flatMap', 'flatMapDeep', 'flatMapDepth'], function(methodName) { + var func = _[methodName], + isDeep = methodName == 'flatMapDeep', + array = [1, 2, 3, 4]; function duplicate(n) { return [n, n]; } - QUnit.test('should map values in `array` to a new flattened array', function(assert) { + QUnit.test('`_.' + methodName + '` should map values in `array` to a new flattened array', function(assert) { assert.expect(1); - var actual = _.flatMap(array, duplicate), + var actual = func(array, duplicate), expected = lodashStable.flatten(lodashStable.map(array, duplicate)); assert.deepEqual(actual, expected); }); - QUnit.test('should work with "_.property" shorthands', function(assert) { + QUnit.test('`_.' + methodName + '` should work with "_.property" shorthands', function(assert) { assert.expect(1); var objects = [{ 'a': [1, 2] }, { 'a': [3, 4] }]; - assert.deepEqual(_.flatMap(objects, 'a'), array); + assert.deepEqual(func(objects, 'a'), array); }); - QUnit.test('should iterate over own properties of objects', function(assert) { + QUnit.test('`_.' + methodName + '` should iterate over own string keyed properties of objects', function(assert) { assert.expect(1); function Foo() { @@ -5729,59 +5759,55 @@ } Foo.prototype.b = [3, 4]; - var actual = _.flatMap(new Foo, identity); + var actual = func(new Foo, identity); assert.deepEqual(actual, [1, 2]); }); - QUnit.test('should use `_.identity` when `iteratee` is nullish', function(assert) { - assert.expect(1); + QUnit.test('`_.' + methodName + '` should use `_.identity` when `iteratee` is nullish', function(assert) { + assert.expect(2); var array = [[1, 2], [3, 4]], + object = { 'a': [1, 2], 'b': [3, 4] }, values = [, null, undefined], expected = lodashStable.map(values, lodashStable.constant([1, 2, 3, 4])); - var actual = lodashStable.map(values, function(value, index) { - return index ? _.flatMap(array, value) : _.flatMap(array); + lodashStable.each([array, object], function(collection) { + var actual = lodashStable.map(values, function(value, index) { + return index ? func(collection, value) : func(collection); + }); + + assert.deepEqual(actual, expected); }); - - assert.deepEqual(actual, expected); }); - QUnit.test('should work on an object with no `iteratee`', function(assert) { - assert.expect(1); - - var actual = _.flatMap({ 'a': [1, 2], 'b': [3, 4] }); - assert.deepEqual(actual, array); - }); - - QUnit.test('should handle object arguments with non-number length properties', function(assert) { - assert.expect(1); - - var object = { 'length': [1, 2] }; - assert.deepEqual(_.flatMap(object, identity), [1, 2]); - }); - - QUnit.test('should accept a falsey `collection` argument', function(assert) { + QUnit.test('`_.' + methodName + '` should accept a falsey `collection` argument', function(assert) { assert.expect(1); var expected = lodashStable.map(falsey, alwaysEmptyArray); var actual = lodashStable.map(falsey, function(collection, index) { try { - return index ? _.flatMap(collection) : _.flatMap(); + return index ? func(collection) : func(); } catch (e) {} }); assert.deepEqual(actual, expected); }); - QUnit.test('should treat number values for `collection` as empty', function(assert) { + QUnit.test('`_.' + methodName + '` should treat number values for `collection` as empty', function(assert) { assert.expect(1); - assert.deepEqual(_.flatMap(1), []); + assert.deepEqual(func(1), []); }); - QUnit.test('should work in a lazy sequence', function(assert) { + QUnit.test('`_.' + methodName + '` should work with objects with non-number length properties', function(assert) { + assert.expect(1); + + var object = { 'length': [1, 2] }; + assert.deepEqual(func(object, identity), [1, 2]); + }); + + QUnit.test('`_.' + methodName + '` should work in a lazy sequence', function(assert) { assert.expect(2); if (!isNpm) { @@ -5790,16 +5816,16 @@ lodashStable.times(2, function(index) { var array = index ? largeArray : smallArray, - actual = _(array).filter(isEven).flatMap(duplicate).take(2).value(); + actual = _(array).filter(isEven)[methodName](duplicate).take(2).value(); - assert.deepEqual(actual, _.take(_.flatMap(_.filter(array, isEven), duplicate), 2)); + assert.deepEqual(actual, _.take(func(_.filter(array, isEven), duplicate), 2)); }); } else { skipAssert(assert, 2); } }); - }()); + }); /*--------------------------------------------------------------------------*/ @@ -6394,7 +6420,7 @@ var array = [1, 2, 3], func = _[methodName]; - QUnit.test('`_.' + methodName + '` iterates over own properties of objects', function(assert) { + QUnit.test('`_.' + methodName + '` iterates over own string keyed properties of objects', function(assert) { assert.expect(1); function Foo() { @@ -12769,7 +12795,7 @@ assert.deepEqual(_.map(objects, 'a'), ['x', 'y']); }); - QUnit.test('should iterate over own properties of objects', function(assert) { + QUnit.test('should iterate over own string keyed properties of objects', function(assert) { assert.expect(1); function Foo() { @@ -12782,47 +12808,19 @@ }); QUnit.test('should use `_.identity` when `iteratee` is nullish', function(assert) { - assert.expect(1); + assert.expect(2); - var values = [, null, undefined], + var object = { 'a': 1, 'b': 2 }, + values = [, null, undefined], expected = lodashStable.map(values, lodashStable.constant([1, 2])); - var actual = lodashStable.map(values, function(value, index) { - return index ? _.map(array, value) : _.map(array); - }); - - assert.deepEqual(actual, expected); - }); - - QUnit.test('should work on an object with no `iteratee`', function(assert) { - assert.expect(1); - - var actual = _.map({ 'a': 1, 'b': 2 }); - assert.deepEqual(actual, array); - }); - - QUnit.test('should handle object arguments with non-number length properties', function(assert) { - assert.expect(1); - - var value = { 'value': 'x' }, - object = { 'length': { 'value': 'x' } }; - - assert.deepEqual(_.map(object, identity), [value]); - }); - - QUnit.test('should treat a nodelist as an array-like object', function(assert) { - assert.expect(1); - - if (document) { - var actual = _.map(document.getElementsByTagName('body'), function(element) { - return element.nodeName.toLowerCase(); + lodashStable.each([array, object], function(collection) { + var actual = lodashStable.map(values, function(value, index) { + return index ? _.map(collection, value) : _.map(collection); }); - assert.deepEqual(actual, ['body']); - } - else { - skipAssert(assert); - } + assert.deepEqual(actual, expected); + }); }); QUnit.test('should accept a falsey `collection` argument', function(assert) { @@ -12845,6 +12843,30 @@ assert.deepEqual(_.map(1), []); }); + QUnit.test('should treat a nodelist as an array-like object', function(assert) { + assert.expect(1); + + if (document) { + var actual = _.map(document.getElementsByTagName('body'), function(element) { + return element.nodeName.toLowerCase(); + }); + + assert.deepEqual(actual, ['body']); + } + else { + skipAssert(assert); + } + }); + + QUnit.test('should work with objects with non-number length properties', function(assert) { + assert.expect(1); + + var value = { 'value': 'x' }, + object = { 'length': { 'value': 'x' } }; + + assert.deepEqual(_.map(object, identity), [value]); + }); + QUnit.test('should return a wrapped value when chaining', function(assert) { assert.expect(1); @@ -12988,7 +13010,7 @@ func = _[methodName], object = { 'a': 1, 'b': 2 }; - QUnit.test('should iterate over own properties of objects', function(assert) { + QUnit.test('`_.' + methodName + '` should iterate over own string keyed properties of objects', function(assert) { assert.expect(1); function Foo() { @@ -13000,7 +13022,7 @@ assert.deepEqual(actual, { 'a': 'a' }); }); - QUnit.test('should accept a falsey `object` argument', function(assert) { + QUnit.test('`_.' + methodName + '` should accept a falsey `object` argument', function(assert) { assert.expect(1); var expected = lodashStable.map(falsey, alwaysEmptyObject); @@ -13014,7 +13036,7 @@ assert.deepEqual(actual, expected); }); - QUnit.test('should return a wrapped value when chaining', function(assert) { + QUnit.test('`_.' + methodName + '` should return a wrapped value when chaining', function(assert) { assert.expect(1); if (!isNpm) { @@ -24701,7 +24723,7 @@ var acceptFalsey = lodashStable.difference(allMethods, rejectFalsey); QUnit.test('should accept falsey arguments', function(assert) { - assert.expect(300); + assert.expect(302); var emptyArrays = lodashStable.map(falsey, alwaysEmptyArray);