Skip to content

Commit

Permalink
Merge pull request #941 from zloirock/stable-sort
Browse files Browse the repository at this point in the history
close #769
  • Loading branch information
zloirock committed Jun 5, 2021
2 parents 8cf605d + a914d65 commit 778c62e
Show file tree
Hide file tree
Showing 14 changed files with 548 additions and 20 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,7 @@
## Changelog
##### Unreleased
- Added polyfill of stable sort in `{ Array, %TypedArray% }.prototype.sort`, [#769](https://github.com/zloirock/core-js/issues/769)
- Fixed `Safari` 14.0- `%TypedArray%.prototype.sort` validation of arguments bug
- `.at` marked as supported from V8 9.2

##### 3.13.1 - 2021.05.29
Expand Down
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -541,7 +541,7 @@ class Array {
slice(start?: number, end?: number): Array<mixed>; // with adding support of @@species
splice(start?: number, deleteCount?: number, ...items: Array<mixed>): Array<mixed>; // 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<value>;
@@iterator(): Iterator<value>;
@@unscopables: { [newMethodNames: string]: true };
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 5 additions & 6 deletions packages/core-js-compat/src/data.js
Expand Up @@ -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': {
Expand Down Expand Up @@ -1229,10 +1228,10 @@ const data = {
safari: '10.0',
},
'es.typed-array.sort': {
chrome: '45',
edge: '13',
firefox: '46',
safari: '10.0',
chrome: '74',
firefox: '67',
// 10.0 - 14.0 accept incorrect arguments
safari: '14.1',
},
'es.typed-array.subarray': {
chrome: '26',
Expand Down
45 changes: 45 additions & 0 deletions packages/core-js/internals/array-sort.js
@@ -0,0 +1,45 @@
// TODO: use something more complex like timsort?
var floor = Math.floor;

var mergeSort = function (array, comparefn) {
var length = array.length;
var middle = floor(length / 2);
return length < 8 ? insertionSort(array, comparefn) : merge(
mergeSort(array.slice(0, middle), comparefn),
mergeSort(array.slice(middle), comparefn),
comparefn
);
};

var insertionSort = function (array, comparefn) {
var length = array.length;
var i = 1;
var element, j;

while (i < length) {
j = i;
element = array[i];
while (j && comparefn(array[j - 1], element) > 0) {
array[j] = array[--j];
}
if (j !== i++) array[j] = element;
} return array;
};

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(comparefn(left[lindex], right[rindex]) <= 0 ? left[lindex++] : right[rindex++]);
} else {
result.push(lindex < llength ? left[lindex++] : right[rindex++]);
}
} return result;
};

module.exports = mergeSort;
5 changes: 5 additions & 0 deletions packages/core-js/internals/engine-ff-version.js
@@ -0,0 +1,5 @@
var userAgent = require('../internals/engine-user-agent');

var firefox = userAgent.match(/firefox\/(\d+)/i);

module.exports = !!firefox && +firefox[1];
3 changes: 3 additions & 0 deletions packages/core-js/internals/engine-is-ie-or-edge.js
@@ -0,0 +1,3 @@
var UA = require('../internals/engine-user-agent');

module.exports = /MSIE|Trident/.test(UA);
5 changes: 5 additions & 0 deletions packages/core-js/internals/engine-webkit-version.js
@@ -0,0 +1,5 @@
var userAgent = require('../internals/engine-user-agent');

var webkit = userAgent.match(/AppleWebKit\/(\d+)\./);

module.exports = !!webkit && +webkit[1];
Expand Up @@ -2,14 +2,13 @@
var IS_PURE = require('../internals/is-pure');
var global = require('../internals/global');
var fails = require('../internals/fails');
var userAgent = require('../internals/engine-user-agent');
var WEBKIT = require('../internals/engine-webkit-version');

// Forced replacement object prototype accessors methods
module.exports = IS_PURE || !fails(function () {
// This feature detection crashes old WebKit
// https://github.com/zloirock/core-js/issues/232
var webkit = userAgent.match(/AppleWebKit\/(\d+)\./);
if (webkit && +webkit[1] < 535) return;
if (WEBKIT && WEBKIT < 535) return;
var key = Math.random();
// In FF throws only define methods
// eslint-disable-next-line no-undef, no-useless-call -- required for testing
Expand Down
77 changes: 73 additions & 4 deletions packages/core-js/modules/es.array.sort.js
Expand Up @@ -2,8 +2,14 @@
var $ = require('../internals/export');
var aFunction = require('../internals/a-function');
var toObject = require('../internals/to-object');
var toLength = require('../internals/to-length');
var fails = require('../internals/fails');
var internalSort = require('../internals/array-sort');
var arrayMethodIsStrict = require('../internals/array-method-is-strict');
var FF = require('../internals/engine-ff-version');
var IE_OR_EDGE = require('../internals/engine-is-ie-or-edge');
var V8 = require('../internals/engine-v8-version');
var WEBKIT = require('../internals/engine-webkit-version');

var test = [];
var nativeSort = test.sort;
Expand All @@ -19,14 +25,77 @@ var FAILS_ON_NULL = fails(function () {
// Old WebKit
var STRICT_METHOD = arrayMethodIsStrict('sort');

var FORCED = FAILS_ON_UNDEFINED || !FAILS_ON_NULL || !STRICT_METHOD;
var STABLE_SORT = !fails(function () {
// feature detection can be too slow, so check engines versions
if (V8) return V8 < 70;
if (FF && FF > 3) return;
if (IE_OR_EDGE) return true;
if (WEBKIT) return WEBKIT < 603;

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 getSortCompare = function (comparefn) {
return function (x, y) {
if (y === undefined) return -1;
if (x === 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
$({ target: 'Array', proto: true, forced: FORCED }, {
sort: function sort(comparefn) {
return comparefn === undefined
? nativeSort.call(toObject(this))
: nativeSort.call(toObject(this), aFunction(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 = internalSort(items, getSortCompare(comparefn));
itemsLength = items.length;
index = 0;

while (index < itemsLength) array[index] = items[index++];
while (index < arrayLength) delete array[index++];

return array;
}
});
80 changes: 77 additions & 3 deletions packages/core-js/modules/es.typed-array.sort.js
@@ -1,12 +1,86 @@
'use strict';
var ArrayBufferViewCore = require('../internals/array-buffer-view-core');
var global = require('../internals/global');
var fails = require('../internals/fails');
var aFunction = require('../internals/a-function');
var toLength = require('../internals/to-length');
var internalSort = require('../internals/array-sort');
var FF = require('../internals/engine-ff-version');
var IE_OR_EDGE = require('../internals/engine-is-ie-or-edge');
var V8 = require('../internals/engine-v8-version');
var WEBKIT = require('../internals/engine-webkit-version');

var aTypedArray = ArrayBufferViewCore.aTypedArray;
var exportTypedArrayMethod = ArrayBufferViewCore.exportTypedArrayMethod;
var $sort = [].sort;
var Uint16Array = global.Uint16Array;
var nativeSort = Uint16Array && Uint16Array.prototype.sort;

// WebKit
var ACCEPT_INCORRECT_ARGUMENTS = !!nativeSort && !fails(function () {
var array = new Uint16Array(2);
array.sort(null);
array.sort({});
});

var STABLE_SORT = !!nativeSort && !fails(function () {
// feature detection can be too slow, so check engines versions
if (V8) return V8 < 74;
if (FF) return FF < 67;
if (IE_OR_EDGE) return true;
if (WEBKIT) return WEBKIT < 602;

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;
}
});

var getSortCompare = function (comparefn) {
return function (x, y) {
if (comparefn !== undefined) return +comparefn(x, y) || 0;
// eslint-disable-next-line no-self-compare -- NaN check
if (y !== y) return -1;
// eslint-disable-next-line no-self-compare -- NaN check
if (x !== x) return 1;
if (x === 0 && y === 0) return 1 / x > 0 && 1 / y < 0 ? 1 : -1;
return x > y;
};
};

// `%TypedArray%.prototype.sort` method
// https://tc39.es/ecma262/#sec-%typedarray%.prototype.sort
exportTypedArrayMethod('sort', function sort(comparefn) {
return $sort.call(aTypedArray(this), comparefn);
});
var array = this;
if (comparefn !== undefined) aFunction(comparefn);
if (STABLE_SORT) return nativeSort.call(array, comparefn);

aTypedArray(array);
var arrayLength = toLength(array.length);
var items = Array(arrayLength);
var index;

for (index = 0; index < arrayLength; index++) {
items[index] = array[index];
}

items = internalSort(array, getSortCompare(comparefn));

for (index = 0; index < arrayLength; index++) {
array[index] = items[index];
}

return array;
}, !STABLE_SORT || ACCEPT_INCORRECT_ARGUMENTS);
52 changes: 50 additions & 2 deletions tests/compat/tests.js
Expand Up @@ -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';
}
}
},
Expand Down Expand Up @@ -1039,7 +1065,29 @@ GLOBAL.tests = {
return Int8Array.prototype.some;
}],
'es.typed-array.sort': [ARRAY_BUFFER_VIEWS_SUPPORT, function () {
return Int8Array.prototype.sort;
try {
new Uint16Array(1).sort(null);
new Uint16Array(1).sort({});
return false;
} catch (error) { /* empty */ }
// 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;
Expand Down

0 comments on commit 778c62e

Please sign in to comment.