-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat(orderBy): make orderBy filter accept a custom compare function. #10368
Conversation
We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for the commit author(s). If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA (login here to double check)? If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google. |
CLAs look good, thanks! |
}, descending); | ||
} | ||
} | ||
return reverseComparator(function(a, b) { | ||
return compare(get(a),get(b)); | ||
return compare(get(a),get(b),defaultCompare); |
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.
I know it was like this before, but there should be spaces between the arguments (according to the current JSCS rules).
@gkalpak |
I can see how this would be useful for helping people write a custom filter + same algorithm |
Expose the inner compare function used by orderBy to the outside world. This will make it possible to use orderBy with a very specific sort requirement.
Yes please, this would be great for i18n support. |
Okay, in case it helps anyone, here's a quick and dirty solution. I took the orderBy code and slightly modified it to support locale-aware string sorting. I expose a method to let the filter know which locale to use. If the method's unused, it should fallback to the browser's locale. I also introduced a new notation, the For brevity, controller usage: angular.module('exampleApp', ['orderByLocaleAware'])
.controller('exampleController', function ($scope, orderByLocaleAwareId) {
$scope.setLocaleId('he-IL');
$scope.sortColumns = ['@+firstName', '@-lastName'];
$scope.data = [
{ firstName: 'Elad', lastName: 'Efrat' },
{ firstName: 'אלעד', lastName: 'אפרת' }
];
}); and filter usage: <div ng-controller="exampleController">
<table>
<tr ng-repeat="record in data | orderByLocaleAware:sortColumns">
<td>{{ record.firstName }}</td>
<td>{{ record.lastName }}</td>
</tr>
</table>
</div> I'm going to use it in my Angular i18n/l10n helper module, angular-kencale, but I can release it as a standalone module as well. Here's a full example you can play with, just copy/paste to (say) <!doctype html>
<html ng-app="exampleApp">
<head>
<meta charset="utf-8">
<style>
.strong { font-weight: bold };
</style>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
<script type="text/javascript">
angular.module('exampleApp', ['orderByLocaleAware'])
.controller('exampleController', function ($scope, orderByLocaleAwareId) {
$scope.setLocaleId = function(localeId) {
orderByLocaleAwareId.localeId = localeId;
}
$scope.getLocaleId = function() {
return orderByLocaleAwareId.localeId;
}
// Set an initial locale.
$scope.setLocaleId('he-IL');
// The '@' symbol indicates the string is to be ordered by locale.
$scope.sortColumns = ['@firstName', '@lastName'];
// Sample data. For he-IL, Hebrew names should appear first. For en-US, English names.
$scope.data = [
{ firstName: 'Elad', lastName: 'Efrat' },
{ firstName: 'אלעד', lastName: 'אפרת' }
];
});
</script>
</head>
<body>
<div ng-controller="exampleController">
<p>
Locale:
<a href="#" ng-click="setLocaleId('en-US')" ng-class="{ strong: getLocaleId() === 'en-US' }">en-US</a>
<a href="#" ng-click="setLocaleId('he-IL')" ng-class="{ strong: getLocaleId() === 'he-IL' }">he-IL</a>
</p>
<table>
<tr ng-repeat="record in data | orderByLocaleAware:sortColumns">
<td>{{ record.firstName }}</td>
<td>{{ record.lastName }}</td>
</tr>
</table>
</div>
<!-- The module, copied almost verbatim from AngularJS. -->
<script type="text/javascript">
angular.module('orderByLocaleAware', [])
.service('orderByLocaleAwareId', function () {
this.localeId = null;
})
.filter('orderByLocaleAware', function ($parse, orderByLocaleAwareId) {
return function(array, sortPredicate, reverseOrder) {
if (!array || !angular.isArray(array)) return array;
if (!angular.isArray(sortPredicate)) { sortPredicate = [sortPredicate]; }
if (sortPredicate.length === 0) { sortPredicate = ['+']; }
var predicates = processPredicates(sortPredicate, reverseOrder);
// Add a predicate at the end that evaluates to the element index. This makes the
// sort stable as it works as a tie-breaker when all the input predicates cannot
// distinguish between two elements.
predicates.push({ get: function() { return {}; }, descending: reverseOrder ? -1 : 1});
// 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) {
result = compare(v1.predicateValues[index], v2.predicateValues[index], predicates[index].localeAware) * 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 = angular.identity, localeAware = false;
if (angular.isFunction(predicate)) {
get = predicate;
} else if (angular.isString(predicate)) {
if (predicate.charAt(0) == '@') {
localeAware = true;
predicate = predicate.substring(1);
}
if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) {
descending = predicate.charAt(0) == '-' ? -1 : 1;
predicate = predicate.substring(1);
}
if (predicate !== '') {
get = $parse(predicate);
if (get.constant) {
var key = get();
get = function(value) { return value[key]; };
}
}
}
return { get: get, descending: descending * reverseOrder, localeAware: localeAware };
});
}
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;
}
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, localeAware) {
var result = 0;
if (v1.type === v2.type) {
if (v1.value !== v2.value) {
result = (localeAware && v1.type === 'string') ? v1.value.localeCompare(v2.value, orderByLocaleAwareId.localeId) : (v1.value < v2.value ? -1 : 1);
}
} else {
result = v1.type < v2.type ? -1 : 1;
}
return result;
}
function hasCustomToString(obj) {
return angular.isFunction(obj.toString) && obj.toString !== Object.prototype.toString;
}
});
</script>
</body>
</html> |
👍 |
I think that in addition to being able to set compare function as another filter argument there should be a possibility to set a default compare function with a provider (optimally: also to pass a service or a service name just as we can do with I'm not convinced compare function should be another argument. |
I've just tried it and it works well.... but very slow: a few seconds for 2000 elements. According to MDN |
With this change the 3rd argument is interpreted as a comparator function, used to compare the values returned by the predicates. Leaving it empty falls back to the default comparator and a value of `false` is a special case that also falls back to the default comparator but reverses the order. Thus the new implementation is backwards compatible. Helps with angular#12572 (maybe this is as close as we want to get). Fixes angular#13238 Fixes angular#14455 Closes angular#5123 Closes angular#8112 Closes angular#10368
With this change the 3rd argument is interpreted as a comparator function, used to compare the values returned by the predicates. Leaving it empty falls back to the default comparator and a value of `false` is a special case that also falls back to the default comparator but reverses the order. Thus the new implementation is backwards compatible. This commit also xpands the documentation to cover the algorithm used to sort elements and adds a few more unit and e2e tests (unrelated to the change). Helps with angular#12572 (maybe this is as close as we want to get). Fixes angular#13238 Fixes angular#14455 Closes angular#5123 Closes angular#8112 Closes angular#10368
With this change the 3rd argument is interpreted as a comparator function, used to compare the values returned by the predicates. Leaving it empty falls back to the default comparator and a value of `false` is a special case that also falls back to the default comparator but reverses the order. Thus the new implementation is backwards compatible. This commit also expands the documentation to cover the algorithm used to sort elements and adds a few more unit and e2e tests (unrelated to the change). Helps with angular#12572 (maybe this is as close as we want to get). Fixes angular#13238 Fixes angular#14455 Closes angular#5123 Closes angular#8112 Closes angular#10368
Add an optional, 4th argument (`comparator`) for specifying a custom comparator function, used to compare the values returned by the predicates. Omitting the argument, falls back to the default, built-in comparator. The 3rd argument (`reverse`) can still be used for controlling the sorting order (i.e. ascending/descending). Additionally, the documentation has been expanded to cover the algorithm used by the built-in comparator and a few more unit and e2e tests (unrelated to the change) have been added. Helps with #12572 (maybe this is as close as we want to get). Fixes #13238 Fixes #14455 Closes #5123 Closes #8112 Closes #10368 Closes #14468
Expose the inner compare function used by orderBy to the outside world.
This will make it possible to use orderBy with a very specific sort requirement like sorting using the localeCompare or making a few entries appear first in the list.