diff --git a/lodash.js b/lodash.js index 07df6a77c..17594cf2a 100644 --- a/lodash.js +++ b/lodash.js @@ -528,6 +528,26 @@ return result; } + /** + * The base implementation of `_.sortBy` and `_.sortByMultiple` which uses + * `comparer` to define the sort order of `array` and replaces criteria objects + * with their corresponding values. + * + * @private + * @param {Array} array The array to sort. + * @param {Function} comparer The function to define sort order. + * @returns {Array} Returns `array`. + */ + function baseSortBy(array, comparer) { + var length = array.length; + + array.sort(comparer); + while (length--) { + array[length] = array[length].value; + } + return array; + } + /** * Used by `_.max` and `_.min` as the default callback for string values. * @@ -586,8 +606,8 @@ } /** - * Used by `_.sortBy` to compare multiple properties of each element in a - * collection and stable sort them in ascending order. + * Used by `_.sortByMultiple` to compare multiple properties of each element + * in a collection and stable sort them in ascending order. * * @private * @param {Object} object The object to compare to `other`. @@ -1000,10 +1020,10 @@ * `omit`, `once`, `pairs`, `partial`, `partialRight`, `partition`, `pick`, * `pluck`, `property`, `propertyOf`, `pull`, `pullAt`, `push`, `range`, * `rearg`, `reject`, `remove`, `rest`, `reverse`, `shuffle`, `slice`, `sort`, - * `sortBy`, `splice`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, - * `tap`, `throttle`, `thru`, `times`, `toArray`, `transform`, `union`, `uniq`, - * `unshift`, `unzip`, `values`, `valuesIn`, `where`, `without`, `wrap`, `xor`, - * `zip`, and `zipObject` + * `sortBy`, `sortByMultiple`, `splice`, `take`, `takeRight`, `takeRightWhile`, + * `takeWhile`, `tap`, `throttle`, `thru`, `times`, `toArray`, `transform`, + * `union`, `uniq`, `unshift`, `unzip`, `values`, `valuesIn`, `where`, + * `without`, `wrap`, `xor`, `zip`, and `zipObject` * * The non-chainable wrapper functions are: * `attempt`, `camelCase`, `capitalize`, `clone`, `cloneDeep`, `deburr`, @@ -6020,9 +6040,6 @@ * If a property name is provided for `iteratee` the created "_.pluck" style * callback returns the property value of the given element. * - * If an array of property names is provided for `iteratee` the collection - * is sorted by each property value. - * * If an object is provided for `iteratee` the created "_.where" style callback * returns `true` for elements that have the properties of the given object, * else `false`. @@ -6032,8 +6049,8 @@ * @category Collection * @param {Array|Object|string} collection The collection to iterate over. * @param {Array|Function|Object|string} [iteratee=_.identity] The function - * invoked per iteration. If property name(s) or an object is provided it - * is used to create a "_.pluck" or "_.where" style callback respectively. + * invoked per iteration. If a property name or an object is provided it is + * used to create a "_.pluck" or "_.where" style callback respectively. * @param {*} [thisArg] The `this` binding of `iteratee`. * @returns {Array} Returns the new sorted array. * @example @@ -6045,52 +6062,74 @@ * // => [3, 1, 2] * * var users = [ - * { 'user': 'barney', 'age': 36 }, - * { 'user': 'fred', 'age': 40 }, - * { 'user': 'barney', 'age': 26 }, - * { 'user': 'fred', 'age': 30 } + * { 'user': 'fred' }, + * { 'user': 'pebbles' }, + * { 'user': 'barney' } * ]; * * // using "_.pluck" callback shorthand - * _.map(_.sortBy(users, 'age'), _.values); - * // => [['barney', 26], ['fred', 30], ['barney', 36], ['fred', 40]] - * - * // sorting by multiple properties - * _.map(_.sortBy(users, ['user', 'age']), _.values); - * // = > [['barney', 26], ['barney', 36], ['fred', 30], ['fred', 40]] + * _.pluck(_.sortBy(users, 'user'), 'user'); + * // => ['barney', 'fred', 'pebbles'] */ function sortBy(collection, iteratee, thisArg) { if (thisArg && isIterateeCall(collection, iteratee, thisArg)) { iteratee = null; } + iteratee = getCallback(iteratee, thisArg, 3); + var index = -1, length = collection ? collection.length : 0, - multi = iteratee && isArray(iteratee), result = isLength(length) ? Array(length) : []; - if (!multi) { - iteratee = getCallback(iteratee, thisArg, 3); - } baseEach(collection, function(value, key, collection) { - if (multi) { - var length = iteratee.length, - criteria = Array(length); + result[++index] = { 'criteria': iteratee(value, key, collection), 'index': index, 'value': value }; + }); + return baseSortBy(result, compareAscending); + } - while (length--) { - criteria[length] = value == null ? undefined : value[iteratee[length]]; - } - } else { - criteria = iteratee(value, key, collection); + /** + * This method is like `_.sortBy` except that it sorts by property names + * instead of an iteratee function. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {...(string|string[])} props The property names to sort by, + * specified as individual property names or arrays of property names. + * @returns {Array} Returns the new sorted array. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 }, + * { 'user': 'barney', 'age': 26 }, + * { 'user': 'fred', 'age': 30 } + * ]; + * + * _.map(_.sortBy(users, ['user', 'age']), _.values); + * // => [['barney', 26], ['barney', 36], ['fred', 30], ['fred', 40]] + */ + function sortByMultiple(collection) { + var args = arguments; + if (args.length == 4 && isIterateeCall(args[1], args[2], args[3])) { + args = [collection, args[1]]; + } + var index = -1, + length = collection ? collection.length : 0, + props = baseFlatten(args, false, false, 1), + result = isLength(length) ? Array(length) : []; + + baseEach(collection, function(value, key, collection) { + var length = props.length, + criteria = Array(length); + + while (length--) { + criteria[length] = value == null ? undefined : value[props[length]]; } result[++index] = { 'criteria': criteria, 'index': index, 'value': value }; }); - - length = result.length; - result.sort(multi ? compareMultipleAscending : compareAscending); - while (length--) { - result[length] = result[length].value; - } - return result; + return baseSortBy(result, compareMultipleAscending); } /** @@ -9965,6 +10004,7 @@ lodash.shuffle = shuffle; lodash.slice = slice; lodash.sortBy = sortBy; + lodash.sortByMultiple = sortByMultiple; lodash.take = take; lodash.takeRight = takeRight; lodash.takeRightWhile = takeRightWhile; diff --git a/test/test.js b/test/test.js index 571ee5813..e3c4fa01a 100644 --- a/test/test.js +++ b/test/test.js @@ -10712,8 +10712,8 @@ }); test('should work with a "_.pluck" style `iteratee`', 1, function() { - var actual = _.pluck(_.sortBy(objects, 'b'), 'b'); - deepEqual(actual, [1, 2, 3, 4]); + var actual = _.pluck(_.sortBy(objects.concat(undefined), 'b'), 'b'); + deepEqual(actual, [1, 2, 3, 4, undefined]); }); test('should work with an object for `collection`', 1, function() { @@ -10728,21 +10728,6 @@ deepEqual(_.sortBy(1), []); }); - test('should support sorting by an array of properties', 1, function() { - var actual = _.sortBy(objects, ['a', 'b']); - deepEqual(actual, [objects[2], objects[0], objects[3], objects[1]]); - }); - - test('should not error on nullish elements when sorting by multiple properties', 1, function() { - var actual = _.sortBy(objects.concat(undefined), ['a', 'b']); - deepEqual(actual, [objects[2], objects[0], objects[3], objects[1], undefined]); - }); - - test('should perform a stable sort when sorting by multiple properties (test in IE > 8, Opera, and V8)', 1, function() { - var actual = _.sortBy(stableOrder, ['a', 'c']); - deepEqual(actual, stableOrder); - }); - test('should coerce arrays returned from `iteratee`', 1, function() { var actual = _.sortBy(objects, function(object) { var result = [object.a, object.b]; @@ -10754,15 +10739,72 @@ }); test('should work as an iteratee for `_.map`', 1, function() { - var array = [[2, 1, 3], [3, 2, 1]], - actual = _.map(array, _.sortBy); - + var actual = _.map([[2, 1, 3], [3, 2, 1]], _.sortBy); deepEqual(actual, [[1, 2, 3], [1, 2, 3]]); }); }()); /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.sortByMultiple'); + + (function() { + function Pair(a, b, c) { + this.a = a; + this.b = b; + this.c = c; + } + + var objects = [ + { 'a': 'x', 'b': 3 }, + { 'a': 'y', 'b': 4 }, + { 'a': 'x', 'b': 1 }, + { 'a': 'y', 'b': 2 } + ]; + + var stableOrder = [ + new Pair(1, 1, 1), new Pair(1, 2, 1), + new Pair(1, 1, 1), new Pair(1, 2, 1), + new Pair(1, 3, 1), new Pair(1, 4, 1), + new Pair(1, 5, 1), new Pair(1, 6, 1), + new Pair(2, 1, 2), new Pair(2, 2, 2), + new Pair(2, 3, 2), new Pair(2, 4, 2), + new Pair(2, 5, 2), new Pair(2, 6, 2), + new Pair(undefined, 1, 1), new Pair(undefined, 2, 1), + new Pair(undefined, 3, 1), new Pair(undefined, 4, 1), + new Pair(undefined, 5, 1), new Pair(undefined, 6, 1) + ]; + + test('should sort mutliple properties in ascending order', 1, function() { + var actual = _.sortByMultiple(objects, ['a', 'b']); + deepEqual(actual, [objects[2], objects[0], objects[3], objects[1]]); + }); + + test('should perform a stable sort (test in IE > 8, Opera, and V8)', 1, function() { + var actual = _.sortByMultiple(stableOrder, ['a', 'c']); + deepEqual(actual, stableOrder); + }); + + test('should not error on nullish elements', 1, function() { + var actual = _.sortByMultiple(objects.concat(undefined), ['a', 'b']); + deepEqual(actual, [objects[2], objects[0], objects[3], objects[1], undefined]); + }); + + test('should work as an iteratee for `_.reduce`', 1, function() { + var objects = [ + { 'a': 'x', '0': 3 }, + { 'a': 'y', '0': 4 }, + { 'a': 'x', '0': 1 }, + { 'a': 'y', '0': 2 } + ]; + + var actual = _.reduce([['a']], _.sortByMultiple, objects); + deepEqual(actual, [objects[0], objects[2], objects[1], objects[3]]); + }); + }()); + + /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.sortedIndex'); (function() { @@ -13409,6 +13451,7 @@ 'sample', 'shuffle', 'sortBy', + 'sortByMultiple', 'take', 'times', 'toArray', @@ -13447,7 +13490,7 @@ var acceptFalsey = _.difference(allMethods, rejectFalsey); - test('should accept falsey arguments', 200, function() { + test('should accept falsey arguments', 202, function() { var emptyArrays = _.map(falsey, _.constant([])), isExposed = '_' in root, oldDash = root._; @@ -13489,7 +13532,7 @@ }); }); - test('should return an array', 68, function() { + test('should return an array', 70, function() { var array = [1, 2, 3]; _.each(returnArrays, function(methodName) {