diff --git a/fp/_mapping.js b/fp/_mapping.js index 3679633fe..7e13e2929 100644 --- a/fp/_mapping.js +++ b/fp/_mapping.js @@ -57,12 +57,12 @@ module.exports = { 'findLast', 'findLastIndex', 'findLastKey', 'flatMap', 'forEach', 'forEachRight', 'forIn', 'forInRight', 'forOwn', 'forOwnRight', 'get', 'groupBy', 'gt', 'gte', 'has', 'hasIn', 'includes', 'indexOf', 'intersection', - 'invoke', 'invokeMap', 'isEqual', 'isMatch', 'join', 'keyBy', 'lastIndexOf', - 'lt', 'lte', 'map', 'mapKeys', 'mapValues', 'matchesProperty', 'maxBy', - 'merge', 'minBy', 'omit', 'omitBy', 'orderBy', 'overArgs', 'pad', 'padEnd', - 'padStart', 'parseInt', 'partition', 'pick', 'pickBy', 'pull', 'pullAll', - 'pullAt', 'random', 'range', 'rangeRight', 'rearg', 'reject', 'remove', - 'repeat', 'result', 'sampleSize', 'some', 'sortBy', 'sortedIndex', + 'invertBy', 'invoke', 'invokeMap', 'isEqual', 'isMatch', 'join', 'keyBy', + 'lastIndexOf', 'lt', 'lte', 'map', 'mapKeys', 'mapValues', 'matchesProperty', + 'maxBy', 'merge', 'minBy', 'omit', 'omitBy', 'orderBy', 'overArgs', 'pad', + 'padEnd', 'padStart', 'parseInt', 'partition', 'pick', 'pickBy', 'pull', + 'pullAll', 'pullAt', 'random', 'range', 'rangeRight', 'rearg', 'reject', + 'remove', 'repeat', 'result', 'sampleSize', 'some', 'sortBy', 'sortedIndex', 'sortedIndexOf', 'sortedLastIndex', 'sortedLastIndexOf', 'sortedUniqBy', 'split', 'startsWith', 'subtract', 'sumBy', 'take', 'takeRight', 'takeRightWhile', 'takeWhile', 'tap', 'throttle', 'thru', 'times', 'truncate', 'union', 'uniqBy', diff --git a/lodash.js b/lodash.js index 24d54f4a4..e85087be6 100644 --- a/lodash.js +++ b/lodash.js @@ -411,6 +411,27 @@ return func.apply(thisArg, args); } + /** + * A specialized version of `baseAggregator` for arrays. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} setter The function to set `accumulator` values. + * @param {Function} iteratee The iteratee to transform keys. + * @param {Object} accumulator The initial aggregated object. + * @returns {Function} Returns `accumulator`. + */ + function arrayAggregator(array, setter, iteratee, accumulator) { + var index = -1, + length = array.length; + + while (++index < length) { + var value = array[index]; + setter(accumulator, value, iteratee(value), array); + } + return accumulator; + } + /** * Creates a new array concatenating `array` with `other`. * @@ -1378,21 +1399,21 @@ * `differenceBy`, `differenceWith`, `drop`, `dropRight`, `dropRightWhile`, * `dropWhile`, `fill`, `filter`, `flatten`, `flattenDeep`, `flip`, `flow`, * `flowRight`, `fromPairs`, `functions`, `functionsIn`, `groupBy`, `initial`, - * `intersection`, `intersectionBy`, `intersectionWith`, `invert`, `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`, `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`, `values`, `valuesIn`, `without`, - * `wrap`, `xor`, `xorBy`, `xorWith`, `zip`, `zipObject`, `zipObjectDeep`, - * and `zipWith` + * `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`, `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`, `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`, @@ -2124,6 +2145,24 @@ } } + /** + * Aggregates elements of `collection` on `accumulator` with keys transformed + * by `iteratee` and values set by `setter`. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} setter The function to set `accumulator` values. + * @param {Function} iteratee The iteratee to transform keys. + * @param {Object} accumulator The initial aggregated object. + * @returns {Function} Returns `accumulator`. + */ + function baseAggregator(collection, setter, iteratee, accumulator) { + baseEach(collection, function(value, key, collection) { + setter(accumulator, value, iteratee(value), collection); + }); + return accumulator; + } + /** * The base implementation of `_.assign` without support for multiple sources * or `customizer` functions. @@ -2670,6 +2709,24 @@ return result; } + /** + * The base implementation of `_.invert` and `_.invertBy` which inverts + * `object` with values transformed by `iteratee` and set by `setter`. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} setter The function to set `accumulator` values. + * @param {Function} iteratee The iteratee to transform values. + * @param {Object} accumulator The initial inverted object. + * @returns {Function} Returns `accumulator`. + */ + function baseInverter(object, setter, iteratee, accumulator) { + baseForOwn(object, function(value, key, object) { + setter(accumulator, iteratee(value), key, object); + }); + return accumulator; + } + /** * The base implementation of `_.invoke` without support for individual * method arguments. @@ -3865,29 +3922,16 @@ * Creates a function like `_.groupBy`. * * @private - * @param {Function} setter The function to set keys and values of the accumulator object. - * @param {Function} [initializer] The function to initialize the accumulator object. + * @param {Function} setter The function to set accumulator values. + * @param {Function} [initializer] The accumulator object initializer. * @returns {Function} Returns the new aggregator function. */ function createAggregator(setter, initializer) { return function(collection, iteratee) { - var result = initializer ? initializer() : {}; - iteratee = getIteratee(iteratee); + var func = isArray(collection) ? arrayAggregator : baseAggregator, + accumulator = initializer ? initializer() : {}; - if (isArray(collection)) { - var index = -1, - length = collection.length; - - while (++index < length) { - var value = collection[index]; - setter(result, value, iteratee(value), collection); - } - } else { - baseEach(collection, function(value, key, collection) { - setter(result, value, iteratee(value), collection); - }); - } - return result; + return func(collection, setter, getIteratee(iteratee), accumulator); }; } @@ -4220,6 +4264,20 @@ return wrapper; } + /** + * Creates a function like `_.invertBy`. + * + * @private + * @param {Function} setter The function to set accumulator values. + * @param {Function} toIteratee The function to resolve iteratees. + * @returns {Function} Returns the new inverter function. + */ + function createInverter(setter, toIteratee) { + return function(object, iteratee) { + return baseInverter(object, setter, toIteratee(iteratee), {}); + }; + } + /** * Creates a function like `_.over`. * @@ -7683,17 +7741,17 @@ * @returns {Object} Returns the composed aggregate object. * @example * - * var keyData = [ + * var array = [ * { 'dir': 'left', 'code': 97 }, * { 'dir': 'right', 'code': 100 } * ]; * - * _.keyBy(keyData, function(o) { + * _.keyBy(array, function(o) { * return String.fromCharCode(o.code); * }); * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } * - * _.keyBy(keyData, 'dir'); + * _.keyBy(array, 'dir'); * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } } */ var keyBy = createAggregator(function(result, value, key) { @@ -10978,12 +11036,42 @@ * _.invert(object); * // => { '1': 'c', '2': 'b' } */ - function invert(object) { - return arrayReduce(keys(object), function(result, key) { - result[object[key]] = key; - return result; - }, {}); - } + var invert = createInverter(function(result, value, key) { + result[value] = key; + }, constant(identity)); + + /** + * This method is like `_.invert` except that the inverted object is generated + * from the results of running each element of `object` through `iteratee`. + * The corresponding inverted value of each inverted key is an array of keys + * responsible for generating the inverted value. The iteratee is invoked + * with one argument: (value). + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to invert. + * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Object} Returns the new inverted object. + * @example + * + * var object = { 'a': 1, 'b': 2, 'c': 1 }; + * + * _.invertBy(object); + * // => { '1': ['a', 'c'], '2': ['b'] } + * + * _.invertBy(object, function(value) { + * return 'group' + value; + * }); + * // => { 'group1': ['a', 'c'], 'group2': ['b'] } + */ + var invertBy = createInverter(function(result, value, key) { + if (hasOwnProperty.call(result, value)) { + result[value].push(key); + } else { + result[value] = [key]; + } + }, getIteratee); /** * Invokes the method at `path` of `object`. @@ -13944,6 +14032,7 @@ lodash.intersectionBy = intersectionBy; lodash.intersectionWith = intersectionWith; lodash.invert = invert; + lodash.invertBy = invertBy; lodash.invokeMap = invokeMap; lodash.iteratee = iteratee; lodash.keyBy = keyBy; diff --git a/test/test.js b/test/test.js index bfb0a81c6..3de3a36d3 100644 --- a/test/test.js +++ b/test/test.js @@ -3197,15 +3197,12 @@ QUnit.module('lodash.countBy'); (function() { - var array = [4.2, 6.1, 6.4]; + var array = [6.1, 4.2, 6.3]; - QUnit.test('should work with an iteratee', function(assert) { + QUnit.test('should transform keys by `iteratee`', function(assert) { assert.expect(1); - var actual = _.countBy(array, function(num) { - return Math.floor(num); - }, Math); - + var actual = _.countBy(array, Math.floor); assert.deepEqual(actual, { '4': 1, '6': 2 }); }); @@ -3233,7 +3230,7 @@ QUnit.test('should only add values to own, not inherited, properties', function(assert) { assert.expect(2); - var actual = _.countBy([4.2, 6.1, 6.4], function(num) { + var actual = _.countBy(array, function(num) { return Math.floor(num) > 4 ? 'hasOwnProperty' : 'constructor'; }); @@ -3257,7 +3254,7 @@ QUnit.test('should work with an object for `collection`', function(assert) { assert.expect(1); - var actual = _.countBy({ 'a': 4.2, 'b': 6.1, 'c': 6.4 }, function(num) { + var actual = _.countBy({ 'a': 6.1, 'b': 4.2, 'c': 6.3 }, function(num) { return Math.floor(num); }); @@ -6466,12 +6463,19 @@ QUnit.module('lodash.groupBy'); (function() { - var array = [4.2, 6.1, 6.4]; + var array = [6.1, 4.2, 6.3]; + + QUnit.test('should transform keys by `iteratee`', function(assert) { + assert.expect(1); + + var actual = _.groupBy(array, Math.floor); + assert.deepEqual(actual, { '4': [4.2], '6': [6.1, 6.3] }); + }); QUnit.test('should use `_.identity` when `iteratee` is nullish', function(assert) { assert.expect(1); - var array = [4, 6, 6], + var array = [6, 4, 6], values = [, null, undefined], expected = lodashStable.map(values, lodashStable.constant({ '4': [4], '6': [6, 6] })); @@ -6492,12 +6496,12 @@ QUnit.test('should only add values to own, not inherited, properties', function(assert) { assert.expect(2); - var actual = _.groupBy([4.2, 6.1, 6.4], function(num) { + var actual = _.groupBy(array, function(num) { return Math.floor(num) > 4 ? 'hasOwnProperty' : 'constructor'; }); assert.deepEqual(actual.constructor, [4.2]); - assert.deepEqual(actual.hasOwnProperty, [6.1, 6.4]); + assert.deepEqual(actual.hasOwnProperty, [6.1, 6.3]); }); QUnit.test('should work with a number for `iteratee`', function(assert) { @@ -6516,11 +6520,8 @@ QUnit.test('should work with an object for `collection`', function(assert) { assert.expect(1); - var actual = _.groupBy({ 'a': 4.2, 'b': 6.1, 'c': 6.4 }, function(num) { - return Math.floor(num); - }); - - assert.deepEqual(actual, { '4': [4.2], '6': [6.1, 6.4] }); + var actual = _.groupBy({ 'a': 6.1, 'b': 4.2, 'c': 6.3 }, Math.floor); + assert.deepEqual(actual, { '4': [4.2], '6': [6.1, 6.3] }); }); QUnit.test('should work in a lazy sequence', function(assert) { @@ -7406,6 +7407,61 @@ /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.invertBy'); + + (function() { + var object = { 'a': 1, 'b': 2, 'c': 1 }; + + QUnit.test('should transform keys by `iteratee`', function(assert) { + assert.expect(1); + + var expected = { 'group1': ['a', 'c'], 'group2': ['b'] }; + + var actual = _.invertBy(object, function(value) { + return 'group' + value; + }); + + assert.deepEqual(actual, expected); + }); + + QUnit.test('should use `_.identity` when `iteratee` is nullish', function(assert) { + assert.expect(1); + + var values = [, null, undefined], + expected = lodashStable.map(values, lodashStable.constant({ '1': ['a', 'c'], '2': ['b'] })); + + var actual = lodashStable.map(values, function(value, index) { + return index ? _.invertBy(object, value) : _.invertBy(object); + }); + + assert.deepEqual(actual, expected); + }); + + QUnit.test('should use `_.identity` when `iteratee` is nullish', function(assert) { + assert.expect(1); + + var values = [, null, undefined], + expected = lodashStable.map(values, lodashStable.constant({ '1': ['a', 'c'], '2': ['b'] })); + + var actual = lodashStable.map(values, function(value, index) { + return index ? _.invertBy(object, value) : _.invertBy(object); + }); + + assert.deepEqual(actual, expected); + }); + + QUnit.test('should only add multiple values to own, not inherited, properties', function(assert) { + assert.expect(1); + + var expected = { 'hasOwnProperty': ['a'], 'constructor': ['b'] }, + object = { 'a': 'hasOwnProperty', 'b': 'constructor' }; + + assert.ok(lodashStable.isEqual(_.invertBy(object), expected)); + }); + }()); + + /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.invoke'); (function() { @@ -11227,6 +11283,23 @@ QUnit.module('lodash.keyBy'); (function() { + var array = [ + { 'dir': 'left', 'code': 97 }, + { 'dir': 'right', 'code': 100 } + ]; + + QUnit.test('should transform keys by `iteratee`', function(assert) { + assert.expect(1); + + var expected = { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } }; + + var actual = _.keyBy(array, function(object) { + return String.fromCharCode(object.code); + }); + + assert.deepEqual(actual, expected); + }); + QUnit.test('should use `_.identity` when `iteratee` is nullish', function(assert) { assert.expect(1); @@ -11244,19 +11317,21 @@ QUnit.test('should work with a "_.property" style `iteratee`', function(assert) { assert.expect(1); - var actual = _.keyBy(['one', 'two', 'three'], 'length'); - assert.deepEqual(actual, { '3': 'two', '5': 'three' }); + var expected = { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } }, + actual = _.keyBy(array, 'dir'); + + assert.deepEqual(actual, expected); }); QUnit.test('should only add values to own, not inherited, properties', function(assert) { assert.expect(2); - var actual = _.keyBy([4.2, 6.1, 6.4], function(num) { + var actual = _.keyBy([6.1, 4.2, 6.3], function(num) { return Math.floor(num) > 4 ? 'hasOwnProperty' : 'constructor'; }); assert.deepEqual(actual.constructor, 4.2); - assert.deepEqual(actual.hasOwnProperty, 6.4); + assert.deepEqual(actual.hasOwnProperty, 6.3); }); QUnit.test('should work with a number for `iteratee`', function(assert) { @@ -11275,11 +11350,8 @@ QUnit.test('should work with an object for `collection`', function(assert) { assert.expect(1); - var actual = _.keyBy({ 'a': 4.2, 'b': 6.1, 'c': 6.4 }, function(num) { - return Math.floor(num); - }); - - assert.deepEqual(actual, { '4': 4.2, '6': 6.4 }); + var actual = _.keyBy({ 'a': 6.1, 'b': 4.2, 'c': 6.3 }, Math.floor); + assert.deepEqual(actual, { '4': 4.2, '6': 6.3 }); }); QUnit.test('should work in a lazy sequence', function(assert) { @@ -15393,7 +15465,7 @@ (function() { var array = [1, 0, 1]; - QUnit.test('should return two groups of elements', function(assert) { + QUnit.test('should split elements into two groups by `predicate`', function(assert) { assert.expect(3); assert.deepEqual(_.partition([], identity), [[], []]); @@ -15439,10 +15511,7 @@ QUnit.test('should work with an object for `collection`', function(assert) { assert.expect(1); - var actual = _.partition({ 'a': 1.1, 'b': 0.2, 'c': 1.3 }, function(num) { - return Math.floor(num); - }); - + var actual = _.partition({ 'a': 1.1, 'b': 0.2, 'c': 1.3 }, Math.floor); assert.deepEqual(actual, [[1.1, 1.3], [0.2]]); }); }()); @@ -18068,7 +18137,7 @@ { 'a': 'y', 'b': 2 } ]; - QUnit.test('should sort in ascending order', function(assert) { + QUnit.test('should sort in ascending order by `iteratee`', function(assert) { assert.expect(1); var actual = lodashStable.map(_.sortBy(objects, function(object) { @@ -18102,10 +18171,7 @@ QUnit.test('should work with an object for `collection`', function(assert) { assert.expect(1); - var actual = _.sortBy({ 'a': 1, 'b': 2, 'c': 3 }, function(num) { - return Math.sin(num); - }); - + var actual = _.sortBy({ 'a': 1, 'b': 2, 'c': 3 }, Math.sin); assert.deepEqual(actual, [3, 1, 2]); }); @@ -23353,7 +23419,7 @@ var acceptFalsey = lodashStable.difference(allMethods, rejectFalsey); QUnit.test('should accept falsey arguments', function(assert) { - assert.expect(288); + assert.expect(289); var emptyArrays = lodashStable.map(falsey, alwaysEmptyArray);