diff --git a/build/pre-compile.js b/build/pre-compile.js index b1646a5ad..069f770d2 100644 --- a/build/pre-compile.js +++ b/build/pre-compile.js @@ -151,6 +151,7 @@ 'head', 'identity', 'include', + 'index', 'indexOf', 'initial', 'inject', @@ -304,7 +305,7 @@ // minify internal properties used by 'compareAscending', `_.clone`, `_.merge`, and `_.sortBy` (function() { - var properties = ['criteria', 'source', 'value'], + var properties = ['criteria', 'index', 'source', 'value'], snippets = source.match(/( +)(?:function clone|function compareAscending|var merge|var sortBy)\b[\s\S]+?\n\1}/g); if (!snippets) { diff --git a/lodash.js b/lodash.js index 956985fe7..e3be19c51 100644 --- a/lodash.js +++ b/lodash.js @@ -678,15 +678,18 @@ } /** - * Used by `sortBy` to compare transformed values of `collection`, sorting - * them in ascending order. + * Used by `sortBy` to compare transformed `collection` values, sorting them + * stabily in ascending order. * * @private * @param {Object} a The object to compare to `b`. * @param {Object} b The object to compare to `a`. - * @returns {Number} Returns `-1` if `a` < `b`, `0` if `a` == `b`, or `1` if `a` > `b`. + * @returns {Number} Returns the sort order indicator of `1` or `-1`. */ function compareAscending(a, b) { + var ai = a.index, + bi = b.index; + a = a.criteria; b = b.criteria; @@ -696,7 +699,9 @@ if (b === undefined) { return -1; } - return a < b ? -1 : a > b ? 1 : 0; + // ensure a stable sort in V8 and other engines + // http://code.google.com/p/v8/issues/detail?id=90 + return a < b ? -1 : a > b ? 1 : ai < bi ? -1 : 1; } /** @@ -746,7 +751,7 @@ function isPlainObject(value) { // avoid non-objects and false positives for `arguments` objects in IE < 9 var result = false; - if (!(value && typeof value == 'object') || (noArgumentsClass && isArguments(value))) { + if (!(value && typeof value == 'object') || (noArgsClass && isArguments(value))) { return result; } // IE < 9 presents DOM nodes as `Object` objects except they have `toString` @@ -2272,11 +2277,13 @@ 'array': 'result[index] = {\n' + ' criteria: callback(value, index, collection),\n' + + ' index: index,\n' + ' value: value\n' + '}', 'object': 'result' + (isKeysFast ? '[ownIndex] = ' : '.push') + '({\n' + ' criteria: callback(value, index, collection),\n' + + ' index: index,\n' + ' value: value\n' + '})' }, diff --git a/test/test.js b/test/test.js index c3804c36a..4c33bbdae 100644 --- a/test/test.js +++ b/test/test.js @@ -1112,6 +1112,28 @@ QUnit.module('lodash.sortBy'); (function() { + test('should perform a stable sort', function() { + function Pair(x, y) { + this.x = x; + this.y = y; + } + + var collection = [ + new Pair(1, 1), new Pair(1, 2), + new Pair(1, 3), new Pair(1, 4), + new Pair(1, 5), new Pair(1, 6), + new Pair(2, 1), new Pair(2, 2), + new Pair(2, 3), new Pair(2, 4), + new Pair(2, 5), new Pair(2, 6) + ]; + + var actual = _.sortBy(collection, function(pair) { + return pair.x; + }); + + deepEqual(actual, collection); + }); + test('supports the `thisArg` argument', function() { var actual = _.sortBy([1, 2, 3], function(num) { return this.sin(num);