-
Notifications
You must be signed in to change notification settings - Fork 27.4k
fix(orderBy): ensure correct ordering with arrays of objects and no predicate #12072
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -176,88 +176,114 @@ | |
orderByFilter.$inject = ['$parse']; | ||
function orderByFilter($parse) { | ||
return function(array, sortPredicate, reverseOrder) { | ||
|
||
if (!(isArrayLike(array))) return array; | ||
sortPredicate = isArray(sortPredicate) ? sortPredicate : [sortPredicate]; | ||
|
||
if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; } | ||
if (sortPredicate.length === 0) { sortPredicate = ['+']; } | ||
sortPredicate = sortPredicate.map(function(predicate) { | ||
var descending = false, get = predicate || identity; | ||
if (isString(predicate)) { | ||
|
||
var predicates = processPredicates(sortPredicate, reverseOrder); | ||
|
||
// The next three lines are a version of a Swartzian Transform idiom from Perl | ||
// (sometimes called the Decorate-Sort-Undecorate idiom) | ||
// See https://en.wikipedia.org/wiki/Schwartzian_transform | ||
var compareValues = Array.prototype.map.call(array, getComparisonObject); | ||
compareValues.sort(doComparison); | ||
array = compareValues.map(function(item) { return item.value; }); | ||
|
||
return array; | ||
|
||
function getComparisonObject(value, index) { | ||
return { | ||
value: value, | ||
predicateValues: predicates.map(function(predicate) { | ||
return getPredicateValue(predicate.get(value), index); | ||
}) | ||
}; | ||
} | ||
|
||
function doComparison(v1, v2) { | ||
var result = 0; | ||
for (var index=0, length = predicates.length; index < length; ++index) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for (...; !result && index < length; ...) { ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But it is a bit obscure. I would prefer to make it more explicit: function doComparison(v1, v2) {
var result = 0;
for (var index=0, length = predicates.length; index < length; ++index) {
result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending;
if (result) break;
}
return result;
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Works for me |
||
result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending; | ||
if (result) break; | ||
} | ||
return result; | ||
} | ||
}; | ||
|
||
function processPredicates(sortPredicate, reverseOrder) { | ||
reverseOrder = reverseOrder ? -1 : 1; | ||
return sortPredicate.map(function(predicate) { | ||
var descending = 1, get = identity; | ||
|
||
if (isFunction(predicate)) { | ||
get = predicate; | ||
} else if (isString(predicate)) { | ||
if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { | ||
descending = predicate.charAt(0) == '-'; | ||
descending = predicate.charAt(0) == '-' ? -1 : 1; | ||
predicate = predicate.substring(1); | ||
} | ||
if (predicate === '') { | ||
// Effectively no predicate was passed so we compare identity | ||
return reverseComparator(compare, descending); | ||
} | ||
get = $parse(predicate); | ||
if (get.constant) { | ||
var key = get(); | ||
return reverseComparator(function(a, b) { | ||
return compare(a[key], b[key]); | ||
}, descending); | ||
if (predicate !== '') { | ||
get = $parse(predicate); | ||
if (get.constant) { | ||
var key = get(); | ||
get = function(value) { return value[key]; }; | ||
} | ||
} | ||
} | ||
return reverseComparator(function(a, b) { | ||
return compare(get(a),get(b)); | ||
}, descending); | ||
return { get: get, descending: descending * reverseOrder }; | ||
}); | ||
return slice.call(array).sort(reverseComparator(comparator, reverseOrder)); | ||
} | ||
|
||
function comparator(o1, o2) { | ||
for (var i = 0; i < sortPredicate.length; i++) { | ||
var comp = sortPredicate[i](o1, o2); | ||
if (comp !== 0) return comp; | ||
} | ||
return 0; | ||
} | ||
function reverseComparator(comp, descending) { | ||
return descending | ||
? function(a, b) {return comp(b,a);} | ||
: comp; | ||
function isPrimitive(value) { | ||
switch (typeof value) { | ||
case 'number': /* falls through */ | ||
case 'boolean': /* falls through */ | ||
case 'string': | ||
return true; | ||
default: | ||
return false; | ||
} | ||
} | ||
|
||
function isPrimitive(value) { | ||
switch (typeof value) { | ||
case 'number': /* falls through */ | ||
case 'boolean': /* falls through */ | ||
case 'string': | ||
return true; | ||
default: | ||
return false; | ||
} | ||
function objectValue(value, index) { | ||
// If `valueOf` is a valid function use that | ||
if (typeof value.valueOf === 'function') { | ||
value = value.valueOf(); | ||
if (isPrimitive(value)) return value; | ||
} | ||
// If `toString` is a valid function and not the one from `Object.prototype` use that | ||
if (hasCustomToString(value)) { | ||
value = value.toString(); | ||
if (isPrimitive(value)) return value; | ||
} | ||
// We have a basic object so we use the position of the object in the collection | ||
return index; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. trying to think when we end up returning There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add a comment. Is that enough or are you saying that the code might be wrong? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Before, we had that if the property was an object (that does not meet a This is, if you sort [{foo: 2}, {foo: 3}, {foo: 4}, {foo: {}}] by "foo", There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well.... it is true that the sort will change on the second pass but I think it is reasonable to say that sorting mixed types like that is a rather strange case, no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, this is not a problem. First of all the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is very minor (famous last words), so there is no needs to keep this going on. Lets land this |
||
} | ||
|
||
function objectToString(value) { | ||
if (value === null) return 'null'; | ||
if (typeof value.valueOf === 'function') { | ||
value = value.valueOf(); | ||
if (isPrimitive(value)) return value; | ||
} | ||
if (typeof value.toString === 'function') { | ||
value = value.toString(); | ||
if (isPrimitive(value)) return value; | ||
} | ||
return ''; | ||
function getPredicateValue(value, index) { | ||
var type = typeof value; | ||
if (value === null) { | ||
type = 'string'; | ||
value = 'null'; | ||
} else if (type === 'string') { | ||
value = value.toLowerCase(); | ||
} else if (type === 'object') { | ||
value = objectValue(value, index); | ||
} | ||
return { value: value, type: type }; | ||
} | ||
|
||
function compare(v1, v2) { | ||
var t1 = typeof v1; | ||
var t2 = typeof v2; | ||
if (t1 === t2 && t1 === "object") { | ||
v1 = objectToString(v1); | ||
v2 = objectToString(v2); | ||
} | ||
if (t1 === t2) { | ||
if (t1 === "string") { | ||
v1 = v1.toLowerCase(); | ||
v2 = v2.toLowerCase(); | ||
} | ||
if (v1 === v2) return 0; | ||
return v1 < v2 ? -1 : 1; | ||
} else { | ||
return t1 < t2 ? -1 : 1; | ||
function compare(v1, v2) { | ||
var result = 0; | ||
if (v1.type === v2.type) { | ||
if (v1.value !== v2.value) { | ||
result = v1.value < v2.value ? -1 : 1; | ||
} | ||
} else { | ||
result = v1.type < v2.type ? -1 : 1; | ||
} | ||
}; | ||
return result; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm... thinking a little more about this, I think my proposal and the current code are wrong as we are modifying the original
sortPredicate
and we should not do thatThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. We should not modify the passed in predicate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is the current implementation modifying the passed in predicate ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not. But I agree that we should start doing that either :-)