Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add polyfill of stable sort #941

Merged
merged 8 commits into from Jun 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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