Skip to content

Commit 778c62e

Browse files
authored
Merge pull request #941 from zloirock/stable-sort
close #769
2 parents 8cf605d + a914d65 commit 778c62e

14 files changed

+548
-20
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Changelog
22
##### Unreleased
3+
- Added polyfill of stable sort in `{ Array, %TypedArray% }.prototype.sort`, [#769](https://github.com/zloirock/core-js/issues/769)
4+
- Fixed `Safari` 14.0- `%TypedArray%.prototype.sort` validation of arguments bug
35
- `.at` marked as supported from V8 9.2
46

57
##### 3.13.1 - 2021.05.29

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ class Array {
541541
slice(start?: number, end?: number): Array<mixed>; // with adding support of @@species
542542
splice(start?: number, deleteCount?: number, ...items: Array<mixed>): Array<mixed>; // with adding support of @@species
543543
some(callbackfn: (value: any, index: number, target: any) => boolean, thisArg?: any): boolean;
544-
sort(comparefn?: (a: any, b: any) => number): this;
544+
sort(comparefn?: (a: any, b: any) => number): this; // with modern behavior like stable sort
545545
values(): Iterator<value>;
546546
@@iterator(): Iterator<value>;
547547
@@unscopables: { [newMethodNames: string]: true };
@@ -1510,7 +1510,7 @@ class %TypedArray% {
15101510
set(array: ArrayLike, offset?: number): void;
15111511
slice(start?: number, end?: number): %TypedArray%;
15121512
some(callbackfn: (value: number, index: number, target: %TypedArray%) => boolean, thisArg?: any): boolean;
1513-
sort(comparefn?: (a: number, b: number) => number): this;
1513+
sort(comparefn?: (a: number, b: number) => number): this; // with modern behavior like stable sort
15141514
subarray(begin?: number, end?: number): %TypedArray%;
15151515
toString(): string;
15161516
toLocaleString(): string;

packages/core-js-compat/src/data.js

+5-6
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,8 @@ const data = {
236236
safari: '8.0',
237237
},
238238
'es.array.sort': {
239-
chrome: '63',
239+
chrome: '70',
240240
firefox: '4',
241-
ie: '9',
242241
safari: '12.0',
243242
},
244243
'es.array.species': {
@@ -1229,10 +1228,10 @@ const data = {
12291228
safari: '10.0',
12301229
},
12311230
'es.typed-array.sort': {
1232-
chrome: '45',
1233-
edge: '13',
1234-
firefox: '46',
1235-
safari: '10.0',
1231+
chrome: '74',
1232+
firefox: '67',
1233+
// 10.0 - 14.0 accept incorrect arguments
1234+
safari: '14.1',
12361235
},
12371236
'es.typed-array.subarray': {
12381237
chrome: '26',
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// TODO: use something more complex like timsort?
2+
var floor = Math.floor;
3+
4+
var mergeSort = function (array, comparefn) {
5+
var length = array.length;
6+
var middle = floor(length / 2);
7+
return length < 8 ? insertionSort(array, comparefn) : merge(
8+
mergeSort(array.slice(0, middle), comparefn),
9+
mergeSort(array.slice(middle), comparefn),
10+
comparefn
11+
);
12+
};
13+
14+
var insertionSort = function (array, comparefn) {
15+
var length = array.length;
16+
var i = 1;
17+
var element, j;
18+
19+
while (i < length) {
20+
j = i;
21+
element = array[i];
22+
while (j && comparefn(array[j - 1], element) > 0) {
23+
array[j] = array[--j];
24+
}
25+
if (j !== i++) array[j] = element;
26+
} return array;
27+
};
28+
29+
var merge = function (left, right, comparefn) {
30+
var llength = left.length;
31+
var rlength = right.length;
32+
var lindex = 0;
33+
var rindex = 0;
34+
var result = [];
35+
36+
while (lindex < llength || rindex < rlength) {
37+
if (lindex < llength && rindex < rlength) {
38+
result.push(comparefn(left[lindex], right[rindex]) <= 0 ? left[lindex++] : right[rindex++]);
39+
} else {
40+
result.push(lindex < llength ? left[lindex++] : right[rindex++]);
41+
}
42+
} return result;
43+
};
44+
45+
module.exports = mergeSort;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
var userAgent = require('../internals/engine-user-agent');
2+
3+
var firefox = userAgent.match(/firefox\/(\d+)/i);
4+
5+
module.exports = !!firefox && +firefox[1];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
var UA = require('../internals/engine-user-agent');
2+
3+
module.exports = /MSIE|Trident/.test(UA);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
var userAgent = require('../internals/engine-user-agent');
2+
3+
var webkit = userAgent.match(/AppleWebKit\/(\d+)\./);
4+
5+
module.exports = !!webkit && +webkit[1];

packages/core-js/internals/object-prototype-accessors-forced.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
var IS_PURE = require('../internals/is-pure');
33
var global = require('../internals/global');
44
var fails = require('../internals/fails');
5-
var userAgent = require('../internals/engine-user-agent');
5+
var WEBKIT = require('../internals/engine-webkit-version');
66

77
// Forced replacement object prototype accessors methods
88
module.exports = IS_PURE || !fails(function () {
99
// This feature detection crashes old WebKit
1010
// https://github.com/zloirock/core-js/issues/232
11-
var webkit = userAgent.match(/AppleWebKit\/(\d+)\./);
12-
if (webkit && +webkit[1] < 535) return;
11+
if (WEBKIT && WEBKIT < 535) return;
1312
var key = Math.random();
1413
// In FF throws only define methods
1514
// eslint-disable-next-line no-undef, no-useless-call -- required for testing

packages/core-js/modules/es.array.sort.js

+73-4
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
var $ = require('../internals/export');
33
var aFunction = require('../internals/a-function');
44
var toObject = require('../internals/to-object');
5+
var toLength = require('../internals/to-length');
56
var fails = require('../internals/fails');
7+
var internalSort = require('../internals/array-sort');
68
var arrayMethodIsStrict = require('../internals/array-method-is-strict');
9+
var FF = require('../internals/engine-ff-version');
10+
var IE_OR_EDGE = require('../internals/engine-is-ie-or-edge');
11+
var V8 = require('../internals/engine-v8-version');
12+
var WEBKIT = require('../internals/engine-webkit-version');
713

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

22-
var FORCED = FAILS_ON_UNDEFINED || !FAILS_ON_NULL || !STRICT_METHOD;
28+
var STABLE_SORT = !fails(function () {
29+
// feature detection can be too slow, so check engines versions
30+
if (V8) return V8 < 70;
31+
if (FF && FF > 3) return;
32+
if (IE_OR_EDGE) return true;
33+
if (WEBKIT) return WEBKIT < 603;
34+
35+
var result = '';
36+
var code, chr, value, index;
37+
38+
// generate an array with more 512 elements (Chakra and old V8 fails only in this case)
39+
for (code = 65; code < 76; code++) {
40+
chr = String.fromCharCode(code);
41+
42+
switch (code) {
43+
case 66: case 69: case 70: case 72: value = 3; break;
44+
case 68: case 71: value = 4; break;
45+
default: value = 2;
46+
}
47+
48+
for (index = 0; index < 47; index++) {
49+
test.push({ k: chr + index, v: value });
50+
}
51+
}
52+
53+
test.sort(function (a, b) { return b.v - a.v; });
54+
55+
for (index = 0; index < test.length; index++) {
56+
chr = test[index].k.charAt(0);
57+
if (result.charAt(result.length - 1) !== chr) result += chr;
58+
}
59+
60+
return result !== 'DGBEFHACIJK';
61+
});
62+
63+
var FORCED = FAILS_ON_UNDEFINED || !FAILS_ON_NULL || !STRICT_METHOD || !STABLE_SORT;
64+
65+
var getSortCompare = function (comparefn) {
66+
return function (x, y) {
67+
if (y === undefined) return -1;
68+
if (x === undefined) return 1;
69+
if (comparefn !== undefined) return +comparefn(x, y) || 0;
70+
return String(x) > String(y) ? 1 : -1;
71+
};
72+
};
2373

2474
// `Array.prototype.sort` method
2575
// https://tc39.es/ecma262/#sec-array.prototype.sort
2676
$({ target: 'Array', proto: true, forced: FORCED }, {
2777
sort: function sort(comparefn) {
28-
return comparefn === undefined
29-
? nativeSort.call(toObject(this))
30-
: nativeSort.call(toObject(this), aFunction(comparefn));
78+
if (comparefn !== undefined) aFunction(comparefn);
79+
80+
var array = toObject(this);
81+
82+
if (STABLE_SORT) return comparefn === undefined ? nativeSort.call(array) : nativeSort.call(array, comparefn);
83+
84+
var items = [];
85+
var arrayLength = toLength(array.length);
86+
var itemsLength, index;
87+
88+
for (index = 0; index < arrayLength; index++) {
89+
if (index in array) items.push(array[index]);
90+
}
91+
92+
items = internalSort(items, getSortCompare(comparefn));
93+
itemsLength = items.length;
94+
index = 0;
95+
96+
while (index < itemsLength) array[index] = items[index++];
97+
while (index < arrayLength) delete array[index++];
98+
99+
return array;
31100
}
32101
});
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,86 @@
11
'use strict';
22
var ArrayBufferViewCore = require('../internals/array-buffer-view-core');
3+
var global = require('../internals/global');
4+
var fails = require('../internals/fails');
5+
var aFunction = require('../internals/a-function');
6+
var toLength = require('../internals/to-length');
7+
var internalSort = require('../internals/array-sort');
8+
var FF = require('../internals/engine-ff-version');
9+
var IE_OR_EDGE = require('../internals/engine-is-ie-or-edge');
10+
var V8 = require('../internals/engine-v8-version');
11+
var WEBKIT = require('../internals/engine-webkit-version');
312

413
var aTypedArray = ArrayBufferViewCore.aTypedArray;
514
var exportTypedArrayMethod = ArrayBufferViewCore.exportTypedArrayMethod;
6-
var $sort = [].sort;
15+
var Uint16Array = global.Uint16Array;
16+
var nativeSort = Uint16Array && Uint16Array.prototype.sort;
17+
18+
// WebKit
19+
var ACCEPT_INCORRECT_ARGUMENTS = !!nativeSort && !fails(function () {
20+
var array = new Uint16Array(2);
21+
array.sort(null);
22+
array.sort({});
23+
});
24+
25+
var STABLE_SORT = !!nativeSort && !fails(function () {
26+
// feature detection can be too slow, so check engines versions
27+
if (V8) return V8 < 74;
28+
if (FF) return FF < 67;
29+
if (IE_OR_EDGE) return true;
30+
if (WEBKIT) return WEBKIT < 602;
31+
32+
var array = new Uint16Array(516);
33+
var expected = Array(516);
34+
var index, mod;
35+
36+
for (index = 0; index < 516; index++) {
37+
mod = index % 4;
38+
array[index] = 515 - index;
39+
expected[index] = index - 2 * mod + 3;
40+
}
41+
42+
array.sort(function (a, b) {
43+
return (a / 4 | 0) - (b / 4 | 0);
44+
});
45+
46+
for (index = 0; index < 516; index++) {
47+
if (array[index] !== expected[index]) return true;
48+
}
49+
});
50+
51+
var getSortCompare = function (comparefn) {
52+
return function (x, y) {
53+
if (comparefn !== undefined) return +comparefn(x, y) || 0;
54+
// eslint-disable-next-line no-self-compare -- NaN check
55+
if (y !== y) return -1;
56+
// eslint-disable-next-line no-self-compare -- NaN check
57+
if (x !== x) return 1;
58+
if (x === 0 && y === 0) return 1 / x > 0 && 1 / y < 0 ? 1 : -1;
59+
return x > y;
60+
};
61+
};
762

863
// `%TypedArray%.prototype.sort` method
964
// https://tc39.es/ecma262/#sec-%typedarray%.prototype.sort
1065
exportTypedArrayMethod('sort', function sort(comparefn) {
11-
return $sort.call(aTypedArray(this), comparefn);
12-
});
66+
var array = this;
67+
if (comparefn !== undefined) aFunction(comparefn);
68+
if (STABLE_SORT) return nativeSort.call(array, comparefn);
69+
70+
aTypedArray(array);
71+
var arrayLength = toLength(array.length);
72+
var items = Array(arrayLength);
73+
var index;
74+
75+
for (index = 0; index < arrayLength; index++) {
76+
items[index] = array[index];
77+
}
78+
79+
items = internalSort(array, getSortCompare(comparefn));
80+
81+
for (index = 0; index < arrayLength; index++) {
82+
array[index] = items[index];
83+
}
84+
85+
return array;
86+
}, !STABLE_SORT || ACCEPT_INCORRECT_ARGUMENTS);

tests/compat/tests.js

+50-2
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,33 @@ GLOBAL.tests = {
382382
[1, 2, 3].sort(null);
383383
} catch (error2) {
384384
[1, 2, 3].sort(undefined);
385-
return true;
385+
386+
// stable sort
387+
var array = [];
388+
var result = '';
389+
var code, chr, value, index;
390+
391+
// generate an array with more 512 elements (Chakra and old V8 fails only in this case)
392+
for (code = 65; code < 76; code++) {
393+
chr = String.fromCharCode(code);
394+
switch (code) {
395+
case 66: case 69: case 70: case 72: value = 3; break;
396+
case 68: case 71: value = 4; break;
397+
default: value = 2;
398+
}
399+
for (index = 0; index < 47; index++) {
400+
array.push({ k: chr + index, v: value });
401+
}
402+
}
403+
404+
array.sort(function (a, b) { return b.v - a.v; });
405+
406+
for (index = 0; index < array.length; index++) {
407+
chr = array[index].k.charAt(0);
408+
if (result.charAt(result.length - 1) !== chr) result += chr;
409+
}
410+
411+
return result === 'DGBEFHACIJK';
386412
}
387413
}
388414
},
@@ -1039,7 +1065,29 @@ GLOBAL.tests = {
10391065
return Int8Array.prototype.some;
10401066
}],
10411067
'es.typed-array.sort': [ARRAY_BUFFER_VIEWS_SUPPORT, function () {
1042-
return Int8Array.prototype.sort;
1068+
try {
1069+
new Uint16Array(1).sort(null);
1070+
new Uint16Array(1).sort({});
1071+
return false;
1072+
} catch (error) { /* empty */ }
1073+
// stable sort
1074+
var array = new Uint16Array(516);
1075+
var expected = Array(516);
1076+
var index, mod;
1077+
1078+
for (index = 0; index < 516; index++) {
1079+
mod = index % 4;
1080+
array[index] = 515 - index;
1081+
expected[index] = index - 2 * mod + 3;
1082+
}
1083+
1084+
array.sort(function (a, b) {
1085+
return (a / 4 | 0) - (b / 4 | 0);
1086+
});
1087+
1088+
for (index = 0; index < 516; index++) {
1089+
if (array[index] !== expected[index]) return;
1090+
} return true;
10431091
}],
10441092
'es.typed-array.subarray': [ARRAY_BUFFER_VIEWS_SUPPORT, function () {
10451093
return Int8Array.prototype.subarray;

0 commit comments

Comments
 (0)