Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 48e1f56

Browse files
fix(orderBy): ensure correct ordering with arrays of objects and no predicate
By refactoring to use a Schwartzian transform, we can ensure that objects with no custom `toString` or `toValue` methods are just ordered using their position in the original collection. Closes #11866 Closes #11312 Closes #4282
1 parent c5a3d8f commit 48e1f56

File tree

2 files changed

+138
-69
lines changed

2 files changed

+138
-69
lines changed

src/ng/filter/orderBy.js

+93-67
Original file line numberDiff line numberDiff line change
@@ -176,88 +176,114 @@
176176
orderByFilter.$inject = ['$parse'];
177177
function orderByFilter($parse) {
178178
return function(array, sortPredicate, reverseOrder) {
179+
179180
if (!(isArrayLike(array))) return array;
180-
sortPredicate = isArray(sortPredicate) ? sortPredicate : [sortPredicate];
181+
182+
if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; }
181183
if (sortPredicate.length === 0) { sortPredicate = ['+']; }
182-
sortPredicate = sortPredicate.map(function(predicate) {
183-
var descending = false, get = predicate || identity;
184-
if (isString(predicate)) {
184+
185+
var predicates = processPredicates(sortPredicate, reverseOrder);
186+
187+
// The next three lines are a version of a Swartzian Transform idiom from Perl
188+
// (sometimes called the Decorate-Sort-Undecorate idiom)
189+
// See https://en.wikipedia.org/wiki/Schwartzian_transform
190+
var compareValues = Array.prototype.map.call(array, getComparisonObject);
191+
compareValues.sort(doComparison);
192+
array = compareValues.map(function(item) { return item.value; });
193+
194+
return array;
195+
196+
function getComparisonObject(value, index) {
197+
return {
198+
value: value,
199+
predicateValues: predicates.map(function(predicate) {
200+
return getPredicateValue(predicate.get(value), index);
201+
})
202+
};
203+
}
204+
205+
function doComparison(v1, v2) {
206+
var result = 0;
207+
for (var index=0, length = predicates.length; index < length; ++index) {
208+
result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending;
209+
if (result) break;
210+
}
211+
return result;
212+
}
213+
};
214+
215+
function processPredicates(sortPredicate, reverseOrder) {
216+
reverseOrder = reverseOrder ? -1 : 1;
217+
return sortPredicate.map(function(predicate) {
218+
var descending = 1, get = identity;
219+
220+
if (isFunction(predicate)) {
221+
get = predicate;
222+
} else if (isString(predicate)) {
185223
if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) {
186-
descending = predicate.charAt(0) == '-';
224+
descending = predicate.charAt(0) == '-' ? -1 : 1;
187225
predicate = predicate.substring(1);
188226
}
189-
if (predicate === '') {
190-
// Effectively no predicate was passed so we compare identity
191-
return reverseComparator(compare, descending);
192-
}
193-
get = $parse(predicate);
194-
if (get.constant) {
195-
var key = get();
196-
return reverseComparator(function(a, b) {
197-
return compare(a[key], b[key]);
198-
}, descending);
227+
if (predicate !== '') {
228+
get = $parse(predicate);
229+
if (get.constant) {
230+
var key = get();
231+
get = function(value) { return value[key]; };
232+
}
199233
}
200234
}
201-
return reverseComparator(function(a, b) {
202-
return compare(get(a),get(b));
203-
}, descending);
235+
return { get: get, descending: descending * reverseOrder };
204236
});
205-
return slice.call(array).sort(reverseComparator(comparator, reverseOrder));
237+
}
206238

207-
function comparator(o1, o2) {
208-
for (var i = 0; i < sortPredicate.length; i++) {
209-
var comp = sortPredicate[i](o1, o2);
210-
if (comp !== 0) return comp;
211-
}
212-
return 0;
213-
}
214-
function reverseComparator(comp, descending) {
215-
return descending
216-
? function(a, b) {return comp(b,a);}
217-
: comp;
239+
function isPrimitive(value) {
240+
switch (typeof value) {
241+
case 'number': /* falls through */
242+
case 'boolean': /* falls through */
243+
case 'string':
244+
return true;
245+
default:
246+
return false;
218247
}
248+
}
219249

220-
function isPrimitive(value) {
221-
switch (typeof value) {
222-
case 'number': /* falls through */
223-
case 'boolean': /* falls through */
224-
case 'string':
225-
return true;
226-
default:
227-
return false;
228-
}
250+
function objectValue(value, index) {
251+
// If `valueOf` is a valid function use that
252+
if (typeof value.valueOf === 'function') {
253+
value = value.valueOf();
254+
if (isPrimitive(value)) return value;
255+
}
256+
// If `toString` is a valid function and not the one from `Object.prototype` use that
257+
if (hasCustomToString(value)) {
258+
value = value.toString();
259+
if (isPrimitive(value)) return value;
229260
}
261+
// We have a basic object so we use the position of the object in the collection
262+
return index;
263+
}
230264

231-
function objectToString(value) {
232-
if (value === null) return 'null';
233-
if (typeof value.valueOf === 'function') {
234-
value = value.valueOf();
235-
if (isPrimitive(value)) return value;
236-
}
237-
if (typeof value.toString === 'function') {
238-
value = value.toString();
239-
if (isPrimitive(value)) return value;
240-
}
241-
return '';
265+
function getPredicateValue(value, index) {
266+
var type = typeof value;
267+
if (value === null) {
268+
type = 'string';
269+
value = 'null';
270+
} else if (type === 'string') {
271+
value = value.toLowerCase();
272+
} else if (type === 'object') {
273+
value = objectValue(value, index);
242274
}
275+
return { value: value, type: type };
276+
}
243277

244-
function compare(v1, v2) {
245-
var t1 = typeof v1;
246-
var t2 = typeof v2;
247-
if (t1 === t2 && t1 === "object") {
248-
v1 = objectToString(v1);
249-
v2 = objectToString(v2);
250-
}
251-
if (t1 === t2) {
252-
if (t1 === "string") {
253-
v1 = v1.toLowerCase();
254-
v2 = v2.toLowerCase();
255-
}
256-
if (v1 === v2) return 0;
257-
return v1 < v2 ? -1 : 1;
258-
} else {
259-
return t1 < t2 ? -1 : 1;
278+
function compare(v1, v2) {
279+
var result = 0;
280+
if (v1.type === v2.type) {
281+
if (v1.value !== v2.value) {
282+
result = v1.value < v2.value ? -1 : 1;
260283
}
284+
} else {
285+
result = v1.type < v2.type ? -1 : 1;
261286
}
262-
};
287+
return result;
288+
}
263289
}

test/ng/filter/orderBySpec.js

+45-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('Filter: orderBy', function() {
2323
});
2424

2525

26-
it('shouldSortArrayInReverse', function() {
26+
it('should reverse collection if `reverseOrder` param is truthy', function() {
2727
expect(orderBy([{a:15}, {a:2}], 'a', true)).toEqualData([{a:15}, {a:2}]);
2828
expect(orderBy([{a:15}, {a:2}], 'a', "T")).toEqualData([{a:15}, {a:2}]);
2929
expect(orderBy([{a:15}, {a:2}], 'a', "reverse")).toEqualData([{a:15}, {a:2}]);
@@ -116,7 +116,7 @@ describe('Filter: orderBy', function() {
116116
});
117117

118118

119-
it('should not reverse array of objects with no predicate', function() {
119+
it('should not reverse array of objects with no predicate and reverse is not `true`', function() {
120120
var array = [
121121
{ id: 2 },
122122
{ id: 1 },
@@ -126,6 +126,39 @@ describe('Filter: orderBy', function() {
126126
expect(orderBy(array)).toEqualData(array);
127127
});
128128

129+
it('should reverse array of objects with no predicate and reverse is `true`', function() {
130+
var array = [
131+
{ id: 2 },
132+
{ id: 1 },
133+
{ id: 4 },
134+
{ id: 3 }
135+
];
136+
var reversedArray = [
137+
{ id: 3 },
138+
{ id: 4 },
139+
{ id: 1 },
140+
{ id: 2 }
141+
];
142+
expect(orderBy(array, '', true)).toEqualData(reversedArray);
143+
});
144+
145+
146+
it('should reverse array of objects with predicate of "-"', function() {
147+
var array = [
148+
{ id: 2 },
149+
{ id: 1 },
150+
{ id: 4 },
151+
{ id: 3 }
152+
];
153+
var reversedArray = [
154+
{ id: 3 },
155+
{ id: 4 },
156+
{ id: 1 },
157+
{ id: 2 }
158+
];
159+
expect(orderBy(array, '-')).toEqualData(reversedArray);
160+
});
161+
129162

130163
it('should not reverse array of objects with null prototype and no predicate', function() {
131164
var array = [2,1,4,3].map(function(id) {
@@ -151,6 +184,16 @@ describe('Filter: orderBy', function() {
151184
null
152185
]);
153186
});
187+
188+
189+
it('should sort array of arrays as Array.prototype.sort', function() {
190+
expect(orderBy([['one'], ['two'], ['three']])).toEqualData([['one'], ['three'], ['two']]);
191+
});
192+
193+
194+
it('should sort mixed array of objects and values in a stable way', function() {
195+
expect(orderBy([{foo: 2}, {foo: {}}, {foo: 3}, {foo: 4}], 'foo')).toEqualData([{foo: 2}, {foo: 3}, {foo: 4}, {foo: {}}]);
196+
});
154197
});
155198

156199

0 commit comments

Comments
 (0)