diff --git a/CHANGELOG.md b/CHANGELOG.md index c8239118f990..a0f6aced21f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Changelog ##### Unreleased +- Added polyfill of stable sort in `{ Array, %TypedArray% }.prototype.sort`, [#769](https://github.com/zloirock/core-js/issues/769) - `.at` marked as supported from V8 9.2 ##### 3.13.1 - 2021.05.29 diff --git a/README.md b/README.md index e8c415394bc7..509ec6da5b8e 100644 --- a/README.md +++ b/README.md @@ -541,7 +541,7 @@ class Array { slice(start?: number, end?: number): Array; // with adding support of @@species splice(start?: number, deleteCount?: number, ...items: Array): Array; // with adding support of @@species some(callbackfn: (value: any, index: number, target: any) => boolean, thisArg?: any): boolean; - sort(comparefn?: (a: any, b: any) => number): this; + sort(comparefn?: (a: any, b: any) => number): this; // with modern behavior like stable sort values(): Iterator; @@iterator(): Iterator; @@unscopables: { [newMethodNames: string]: true }; @@ -1510,7 +1510,7 @@ class %TypedArray% { set(array: ArrayLike, offset?: number): void; slice(start?: number, end?: number): %TypedArray%; some(callbackfn: (value: number, index: number, target: %TypedArray%) => boolean, thisArg?: any): boolean; - sort(comparefn?: (a: number, b: number) => number): this; + sort(comparefn?: (a: number, b: number) => number): this; // with modern behavior like stable sort subarray(begin?: number, end?: number): %TypedArray%; toString(): string; toLocaleString(): string; diff --git a/packages/core-js-compat/src/data.js b/packages/core-js-compat/src/data.js index 3c683d3684f8..8568cd7584b4 100644 --- a/packages/core-js-compat/src/data.js +++ b/packages/core-js-compat/src/data.js @@ -236,9 +236,8 @@ const data = { safari: '8.0', }, 'es.array.sort': { - chrome: '63', + chrome: '70', firefox: '4', - ie: '9', safari: '12.0', }, 'es.array.species': { @@ -1229,9 +1228,8 @@ const data = { safari: '10.0', }, 'es.typed-array.sort': { - chrome: '45', - edge: '13', - firefox: '46', + chrome: '74', + firefox: '67', safari: '10.0', }, 'es.typed-array.subarray': { diff --git a/packages/core-js/internals/array-sort.js b/packages/core-js/internals/array-sort.js new file mode 100644 index 000000000000..7e4af389b92e --- /dev/null +++ b/packages/core-js/internals/array-sort.js @@ -0,0 +1,114 @@ +'use strict'; +var aFunction = require('../internals/a-function'); +var toObject = require('../internals/to-object'); +var toLength = require('../internals/to-length'); +var fails = require('../internals/fails'); +var arrayMethodIsStrict = require('../internals/array-method-is-strict'); + +var test = []; +var nativeSort = test.sort; +var floor = Math.floor; + +// IE8- +var FAILS_ON_UNDEFINED = fails(function () { + test.sort(undefined); +}); +// V8 bug +var FAILS_ON_NULL = fails(function () { + test.sort(null); +}); +// Old WebKit +var STRICT_METHOD = arrayMethodIsStrict('sort'); + +var STABLE_SORT = !fails(function () { + var result = ''; + var code, chr, value, index; + + // generate an array with more 512 elements (Chakra and old V8 fails only in this case) + for (code = 65; code < 76; code++) { + chr = String.fromCharCode(code); + switch (code) { + case 66: case 69: case 70: case 72: value = 3; break; + case 68: case 71: value = 4; break; + default: value = 2; + } + + for (index = 0; index < 47; index++) { + test.push({ k: chr + index, v: value }); + } + } + + test.sort(function (a, b) { return b.v - a.v; }); + + for (index = 0; index < test.length; index++) { + chr = test[index].k.charAt(0); + if (result.charAt(result.length - 1) !== chr) result += chr; + } + + return result !== 'DGBEFHACIJK'; +}); + +var FORCED = FAILS_ON_UNDEFINED || !FAILS_ON_NULL || !STRICT_METHOD || !STABLE_SORT; + +var mergeSort = function (array, comparefn) { + var length = array.length; + var middle = floor(length / 2); + if (length < 2) return array; + return merge( + mergeSort(array.slice(0, middle), comparefn), + mergeSort(array.slice(middle), comparefn), + comparefn + ); +}; + +var merge = function (left, right, comparefn) { + var llength = left.length; + var rlength = right.length; + var lindex = 0; + var rindex = 0; + var result = []; + + while (lindex < llength || rindex < rlength) { + if (lindex < llength && rindex < rlength) { + result.push(sortCompare(left[lindex], right[rindex], comparefn) <= 0 ? left[lindex++] : right[rindex++]); + } else { + result.push(lindex < llength ? left[lindex++] : right[rindex++]); + } + } return result; +}; + +var sortCompare = function (x, y, comparefn) { + if (x === undefined && y === undefined) return 0; + if (x === undefined) return 1; + if (y === undefined) return -1; + if (comparefn !== undefined) { + return +comparefn(x, y) || 0; + } return String(x) > String(y) ? 1 : -1; +}; + +// `Array.prototype.sort` method +// https://tc39.es/ecma262/#sec-array.prototype.sort +module.exports = FORCED ? function sort(comparefn) { + if (comparefn !== undefined) aFunction(comparefn); + + var array = toObject(this); + + if (STABLE_SORT) return comparefn === undefined ? nativeSort.call(array) : nativeSort.call(array, comparefn); + + var items = []; + var arrayLength = toLength(array.length); + var itemsLength, index; + + for (index = 0; index < arrayLength; index++) { + if (index in array) items.push(array[index]); + } + + items = mergeSort(items, comparefn); + itemsLength = items.length; + index = 0; + + while (index < itemsLength) array[index] = items[index++]; + while (index < arrayLength) delete array[index++]; + + return array; +} : nativeSort; diff --git a/packages/core-js/modules/es.array.sort.js b/packages/core-js/modules/es.array.sort.js index fd36157804a2..7e27a9e01582 100644 --- a/packages/core-js/modules/es.array.sort.js +++ b/packages/core-js/modules/es.array.sort.js @@ -1,32 +1,9 @@ 'use strict'; var $ = require('../internals/export'); -var aFunction = require('../internals/a-function'); -var toObject = require('../internals/to-object'); -var fails = require('../internals/fails'); -var arrayMethodIsStrict = require('../internals/array-method-is-strict'); - -var test = []; -var nativeSort = test.sort; - -// IE8- -var FAILS_ON_UNDEFINED = fails(function () { - test.sort(undefined); -}); -// V8 bug -var FAILS_ON_NULL = fails(function () { - test.sort(null); -}); -// Old WebKit -var STRICT_METHOD = arrayMethodIsStrict('sort'); - -var FORCED = FAILS_ON_UNDEFINED || !FAILS_ON_NULL || !STRICT_METHOD; +var sort = require('../internals/array-sort'); // `Array.prototype.sort` method // https://tc39.es/ecma262/#sec-array.prototype.sort -$({ target: 'Array', proto: true, forced: FORCED }, { - sort: function sort(comparefn) { - return comparefn === undefined - ? nativeSort.call(toObject(this)) - : nativeSort.call(toObject(this), aFunction(comparefn)); - } +$({ target: 'Array', proto: true, forced: [].sort !== sort }, { + sort: sort }); diff --git a/packages/core-js/modules/es.typed-array.sort.js b/packages/core-js/modules/es.typed-array.sort.js index 46cc25e4b0ac..a22e95b09b59 100644 --- a/packages/core-js/modules/es.typed-array.sort.js +++ b/packages/core-js/modules/es.typed-array.sort.js @@ -1,12 +1,34 @@ 'use strict'; var ArrayBufferViewCore = require('../internals/array-buffer-view-core'); +var fails = require('../internals/fails'); +var $sort = require('../internals/array-sort'); var aTypedArray = ArrayBufferViewCore.aTypedArray; var exportTypedArrayMethod = ArrayBufferViewCore.exportTypedArrayMethod; -var $sort = [].sort; + +var STABLE_SORT = !fails(function () { + // eslint-disable-next-line es/no-typed-arrays -- required for testing + var array = new Uint16Array(516); + var expected = Array(516); + var index, mod; + + for (index = 0; index < 516; index++) { + mod = index % 4; + array[index] = 515 - index; + expected[index] = index - 2 * mod + 3; + } + + array.sort(function (a, b) { + return (a / 4 | 0) - (b / 4 | 0); + }); + + for (index = 0; index < 516; index++) { + if (array[index] !== expected[index]) return true; + } +}); // `%TypedArray%.prototype.sort` method // https://tc39.es/ecma262/#sec-%typedarray%.prototype.sort exportTypedArrayMethod('sort', function sort(comparefn) { return $sort.call(aTypedArray(this), comparefn); -}); +}, !STABLE_SORT); diff --git a/tests/compat/tests.js b/tests/compat/tests.js index ba30ae67c239..2c9403287e96 100644 --- a/tests/compat/tests.js +++ b/tests/compat/tests.js @@ -382,7 +382,33 @@ GLOBAL.tests = { [1, 2, 3].sort(null); } catch (error2) { [1, 2, 3].sort(undefined); - return true; + + // stable sort + var array = []; + var result = ''; + var code, chr, value, index; + + // generate an array with more 512 elements (Chakra and old V8 fails only in this case) + for (code = 65; code < 76; code++) { + chr = String.fromCharCode(code); + switch (code) { + case 66: case 69: case 70: case 72: value = 3; break; + case 68: case 71: value = 4; break; + default: value = 2; + } + for (index = 0; index < 47; index++) { + array.push({ k: chr + index, v: value }); + } + } + + array.sort(function (a, b) { return b.v - a.v; }); + + for (index = 0; index < array.length; index++) { + chr = array[index].k.charAt(0); + if (result.charAt(result.length - 1) !== chr) result += chr; + } + + return result === 'DGBEFHACIJK'; } } }, @@ -1039,7 +1065,24 @@ GLOBAL.tests = { return Int8Array.prototype.some; }], 'es.typed-array.sort': [ARRAY_BUFFER_VIEWS_SUPPORT, function () { - return Int8Array.prototype.sort; + // stable sort + var array = new Uint16Array(516); + var expected = Array(516); + var index, mod; + + for (index = 0; index < 516; index++) { + mod = index % 4; + array[index] = 515 - index; + expected[index] = index - 2 * mod + 3; + } + + array.sort(function (a, b) { + return (a / 4 | 0) - (b / 4 | 0); + }); + + for (index = 0; index < 516; index++) { + if (array[index] !== expected[index]) return; + } return true; }], 'es.typed-array.subarray': [ARRAY_BUFFER_VIEWS_SUPPORT, function () { return Int8Array.prototype.subarray; diff --git a/tests/pure/es.array.sort.js b/tests/pure/es.array.sort.js index 73074a0c14dc..5dce031e3f8c 100644 --- a/tests/pure/es.array.sort.js +++ b/tests/pure/es.array.sort.js @@ -7,6 +7,48 @@ QUnit.test('Array#sort', assert => { assert.notThrows(() => sort([1, 2, 3], undefined), 'works with undefined'); assert.throws(() => sort([1, 2, 3], null), 'throws on null'); assert.throws(() => sort([1, 2, 3], {}), 'throws on {}'); + + const expected = Array(516); + let array = Array(516); + let index, mod, code, chr, value; + + for (index = 0; index < 516; index++) { + mod = index % 4; + array[index] = 515 - index; + expected[index] = index - 2 * mod + 3; + } + + sort(array, (a, b) => (a / 4 | 0) - (b / 4 | 0)); + + assert.same(String(array), String(expected), 'stable #1'); + + let result = ''; + array = []; + + // generate an array with more 512 elements (Chakra and old V8 fails only in this case) + for (code = 65; code < 76; code++) { + chr = String.fromCharCode(code); + + switch (code) { + case 66: case 69: case 70: case 72: value = 3; break; + case 68: case 71: value = 4; break; + default: value = 2; + } + + for (index = 0; index < 47; index++) { + array.push({ k: chr + index, v: value }); + } + } + + sort(array, (a, b) => b.v - a.v); + + for (index = 0; index < array.length; index++) { + chr = array[index].k.charAt(0); + if (result.charAt(result.length - 1) !== chr) result += chr; + } + + assert.same(result, 'DGBEFHACIJK', 'stable #2'); + if (STRICT) { assert.throws(() => sort(null), TypeError, 'ToObject(this)'); assert.throws(() => sort(undefined), TypeError, 'ToObject(this)'); diff --git a/tests/tests/es.array.sort.js b/tests/tests/es.array.sort.js index c6bb7c9d9043..ee7a275fb361 100644 --- a/tests/tests/es.array.sort.js +++ b/tests/tests/es.array.sort.js @@ -7,6 +7,48 @@ QUnit.test('Array#sort', assert => { assert.name(sort, 'sort'); assert.looksNative(sort); assert.nonEnumerable(Array.prototype, 'sort'); + + const expected = Array(516); + let array = Array(516); + let index, mod, code, chr, value; + + for (index = 0; index < 516; index++) { + mod = index % 4; + array[index] = 515 - index; + expected[index] = index - 2 * mod + 3; + } + + array.sort((a, b) => (a / 4 | 0) - (b / 4 | 0)); + + assert.same(String(array), String(expected), 'stable #1'); + + let result = ''; + array = []; + + // generate an array with more 512 elements (Chakra and old V8 fails only in this case) + for (code = 65; code < 76; code++) { + chr = String.fromCharCode(code); + + switch (code) { + case 66: case 69: case 70: case 72: value = 3; break; + case 68: case 71: value = 4; break; + default: value = 2; + } + + for (index = 0; index < 47; index++) { + array.push({ k: chr + index, v: value }); + } + } + + array.sort((a, b) => b.v - a.v); + + for (index = 0; index < array.length; index++) { + chr = array[index].k.charAt(0); + if (result.charAt(result.length - 1) !== chr) result += chr; + } + + assert.same(result, 'DGBEFHACIJK', 'stable #2'); + assert.notThrows(() => [1, 2, 3].sort(undefined).length === 3, 'works with undefined'); assert.throws(() => [1, 2, 3].sort(null), 'throws on null'); assert.throws(() => [1, 2, 3].sort({}), 'throws on {}'); diff --git a/tests/tests/es.typed-array.sort.js b/tests/tests/es.typed-array.sort.js new file mode 100644 index 000000000000..3182cf3c2966 --- /dev/null +++ b/tests/tests/es.typed-array.sort.js @@ -0,0 +1,63 @@ +import { DESCRIPTORS, GLOBAL, STRICT, TYPED_ARRAYS } from '../helpers/constants'; + +if (DESCRIPTORS) QUnit.test('%TypedArrayPrototype%.sort', assert => { + // we can't implement %TypedArrayPrototype% in all engines, so run all tests for each typed array constructor + for (const name in TYPED_ARRAYS) { + const TypedArray = GLOBAL[name]; + const { sort } = TypedArray.prototype; + assert.isFunction(sort, `${ name }::sort is function`); + assert.arity(sort, 1, `${ name }::sort arity is 1`); + assert.name(sort, 'sort', `${ name }::sort name is 'sort'`); + assert.looksNative(sort, `${ name }::sort looks native`); + + if (name.indexOf('8') === -1) { + const expected = Array(516); + let array = new TypedArray(516); + let index, mod, j, k, postfix; + + for (index = 0; index < 516; index++) { + mod = index % 4; + array[index] = 515 - index; + expected[index] = index - 2 * mod + 3; + } + + array.sort((a, b) => (a / 4 | 0) - (b / 4 | 0)); + + assert.same(String(array), String(expected), 'stable #1'); + + let result = ''; + array = new TypedArray(520); + index = 0; + + for (j = 0; j < 10; j++) { + switch (j) { + case 1: case 4: case 5: case 7: postfix = 3; break; + case 3: case 6: postfix = 4; break; + default: postfix = 2; + } + + for (k = 0; k < 52; k++) { + array[index] = 10 * index++ + postfix; + } + } + + array.sort((a, b) => b % 10 - a % 10); + + for (index = 0; index < array.length; index++) { + j = String((array[index] / 520) | 0); + if (result.charAt(result.length - 1) !== j) result += j; + } + + assert.same(result, '3614570289', 'stable #2'); + } + + assert.throws(() => sort.call([0], () => { /* empty */ }), "isn't generic"); + assert.notThrows(() => new TypedArray([1, 2, 3]).sort(undefined).length === 3, 'works with undefined'); + assert.throws(() => new TypedArray([1, 2, 3]).sort(null), 'throws on null'); + assert.throws(() => new TypedArray([1, 2, 3]).sort({}), 'throws on {}'); + if (STRICT) { + assert.throws(() => sort.call(null), TypeError, 'ToObject(this)'); + assert.throws(() => sort.call(undefined), TypeError, 'ToObject(this)'); + } + } +});