Skip to content

Commit a9faebf

Browse files
committed
Fix sort() method to perform a stable sort
1 parent 3106129 commit a9faebf

File tree

4 files changed

+95
-37
lines changed

4 files changed

+95
-37
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Disallowed a backslash before closing quote or apostrophe in string literals
88
- Improved error locations in string literals
99
- Fixed `match()` method to work well for RegExp with `g` flag and for strings when `matchAll` is true
10+
- Fixed `sort()` method to perform a stable sort for old js-engines and always place `undefined` values last
1011
- Fixed range in details for bad input errors
1112
- Fixed suggestion support in template literals (#33)
1213
- Fixed suggestions for `=` and `!=` operators by avoiding unfold array values

src/lang/compile-buildin.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const TYPE_STRING = 4;
88
const TYPE_NULL = 5;
99
const TYPE_OBJECT = 6;
1010
const TYPE_OTHER = 7;
11+
const TYPE_UNDEFINED = 8;
1112

1213
function cmpType(value) {
1314
switch (typeof value) {
@@ -19,6 +20,8 @@ function cmpType(value) {
1920
return TYPE_STRING;
2021
case 'object':
2122
return value === null ? TYPE_NULL : TYPE_OBJECT;
23+
case 'undefined':
24+
return TYPE_UNDEFINED;
2225
default:
2326
return TYPE_OTHER;
2427
}

src/methods.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,34 @@ function matchEntry(match) {
1717
};
1818
}
1919

20+
const stableSortSize = isSortStable(20) ? Infinity : isSortStable(10) ? 10 : 0;
21+
22+
function isSortStable(n) {
23+
return Array.from({ length: n }, (_, idx) => ({ idx }))
24+
.sort((a, b) => (a.idx % 2) - (b.idx % 2))
25+
.every((a, idx) =>
26+
idx < n / 2 ? (a.idx >> 1 === idx) : Math.ceil(n / 2) + (a.idx >> 1) === idx
27+
);
28+
}
29+
30+
function stableSort(array, cmp) {
31+
// check size, e.g. old v8 had stable sort only for arrays with length less or equal 10
32+
if (array.length <= stableSortSize) {
33+
return array.slice().sort(cmp);
34+
}
35+
36+
return array
37+
.map((value, idx) => ({ value, idx }))
38+
.sort((a, b) =>
39+
(a.value === undefined
40+
? b.value !== undefined
41+
: b.value === undefined
42+
? -1
43+
: cmp(a.value, b.value)) || (a.idx - b.idx)
44+
)
45+
.map(item => item.value);
46+
}
47+
2048
export default Object.freeze({
2149
bool: buildin.bool,
2250
filter: buildin.filter,
@@ -97,9 +125,11 @@ export default Object.freeze({
97125

98126
return a < b ? -1 : a > b;
99127
};
128+
} else {
129+
sorter = buildin.cmp;
100130
}
101131

102-
return current.slice().sort(sorter);
132+
return stableSort(current, sorter);
103133
},
104134
reverse(current) {
105135
if (!Array.isArray(current)) {

test/method-sort.js

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ describe('sort()', () => {
142142
});
143143

144144
describe('mixed value types', () => {
145+
const filterUndefined = (arr) => arr.filter(x => x !== undefined);
145146
const fn = () => {};
146147
const data = [
147148
true,
@@ -162,47 +163,70 @@ describe('sort()', () => {
162163
];
163164

164165
it('asc', () => {
166+
const expected = [
167+
false,
168+
true,
169+
NaN,
170+
-Infinity,
171+
1,
172+
4,
173+
Infinity,
174+
'2',
175+
'b',
176+
'z',
177+
null,
178+
{ c: 1 },
179+
{ b: 1 },
180+
fn,
181+
undefined
182+
];
183+
165184
assert.deepEqual(
166-
escapeNaN(query('sort($ asc)')(data)),
167-
escapeNaN([
168-
false,
169-
true,
170-
NaN,
171-
-Infinity,
172-
1,
173-
4,
174-
Infinity,
175-
'2',
176-
'b',
177-
'z',
178-
null,
179-
{ c: 1 },
180-
{ b: 1 },
181-
fn,
182-
undefined
183-
])
185+
escapeNaN(query('sort()')(data)),
186+
escapeNaN(expected)
187+
);
188+
assert.deepEqual(
189+
escapeNaN(query('sort()')(filterUndefined(data))),
190+
escapeNaN(filterUndefined(expected))
184191
);
185192
});
186193
it('desc', () => {
194+
const expected = [
195+
fn,
196+
{ c: 1 },
197+
{ b: 1 },
198+
null,
199+
'z',
200+
'b',
201+
'2',
202+
Infinity,
203+
4,
204+
1,
205+
-Infinity,
206+
NaN,
207+
true,
208+
false,
209+
undefined
210+
];
211+
212+
assert.deepEqual(
213+
escapeNaN(query('sort($ desc)')(data)),
214+
escapeNaN(expected)
215+
);
216+
assert.deepEqual(
217+
escapeNaN(query('sort($ desc)')(filterUndefined(data))),
218+
escapeNaN(filterUndefined(expected))
219+
);
220+
});
221+
222+
it('undefined should be always last', () => {
187223
assert.deepEqual(
188-
escapeNaN(query('sort($ desc)')([...data])),
189-
escapeNaN([
190-
fn,
191-
{ c: 1 },
192-
{ b: 1 },
193-
null,
194-
'z',
195-
'b',
196-
'2',
197-
Infinity,
198-
4,
199-
1,
200-
-Infinity,
201-
NaN,
202-
true,
203-
false,
204-
undefined
205-
])
224+
query('sort()')([
225+
1, 'zzz', undefined, 4, 'asd', undefined, 3, undefined, 2
226+
]),
227+
[
228+
1, 2, 3, 4, 'asd', 'zzz', undefined, undefined, undefined
229+
]
206230
);
207231
});
208232
});

0 commit comments

Comments
 (0)