Skip to content

Commit

Permalink
add polyfill of stable sort
Browse files Browse the repository at this point in the history
  • Loading branch information
zloirock committed Jun 4, 2021
1 parent f23cec3 commit 98555de
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 37 deletions.
1 change: 1 addition & 0 deletions 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
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
8 changes: 3 additions & 5 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,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': {
Expand Down
114 changes: 114 additions & 0 deletions 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;
29 changes: 3 additions & 26 deletions 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
});
26 changes: 24 additions & 2 deletions 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);
47 changes: 45 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,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;
Expand Down
42 changes: 42 additions & 0 deletions tests/pure/es.array.sort.js
Expand Up @@ -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)');
Expand Down
42 changes: 42 additions & 0 deletions tests/tests/es.array.sort.js
Expand Up @@ -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 {}');
Expand Down

0 comments on commit 98555de

Please sign in to comment.