Languages such as Python and Java have special methods for sorting custom classes. In JavaScript, toString()
can be overridden, but this does not work easily for numeric values.
As a workaround, I added a method called compareTo()
to the class, although this still requires a function to call it.
class NumericSortable {
constructor(newVal) {
this.val = newVal
}
compareTo(other) {
return this.val - other.val
}
}
const objectList = [
new NumericSortable(3),
new NumericSortable(1),
new NumericSortable(20),
]
objectList.sort(
function(a, b) { return a.compareTo(b) })
console.log(objectList)
Is there a way to modify the class so it can be sorted without requiring a function to be defined inside sort()
?
Perhaps there is a good way to override toString()
that will work for numeric values. However, solutions such as localeCompare()
or a collator require two arguments, and they would not be passed to the overridden toString()
method at the same time.
CodePudding user response:
You can add a static method to your NumericSortable
class, and pass that into the sort
call. This idea can be extended to any custom class that need to define how two instances are to be compared for sorting.
class NumericSortable {
constructor(value) {
this.value = value;
}
static compare(a,b) {
return a.value - b.value;
}
}
const arr = [
new NumericSortable(3),
new NumericSortable(1),
new NumericSortable(20),
];
arr.sort(NumericSortable.compare);
console.log(arr);
This makes things more explicit, and easier for anyone else reading the code to reason about how the array is being sorted.
CodePudding user response:
I like to make a function that returns a sort function for these cases.
function by(prop){
return function(a,b){return a[prop]-b[prop];};
}
this let's you specify the object's to-be-sorted property at call-time, letting one generic function do a lot of heavy lifting.
objectList.sort(by("val"))
This avoids the need for a custom callback each sort, though with fat arrows that's not the burden it used to be anyway...
CodePudding user response:
If no comparator is provided, each time two items in the array are compared, they'll be coerced to strings, and then those strings will be compared lexiographically to determine which object will come before the other in the sorted array. So, if you don't want to pass a comparator, adding a toString
method to implement the desired sorting logic is the only other approach.
Unfortunately, for your situation, lexiographic comparison alone based on the .val
s won't cut it; 1 will come before 20, and 20 will come before 3. If the numbers involved won't get so high as to have e
s in their string version, you could .padStart
the returned string so that each compared numeric string will have the same number of characters (thereby allowing lexiographic comparison to work).
class NumericSortable {
constructor(newVal) {
this.val = newVal
}
// Unused now
compareTo(other) {
return this.val - other.val
}
toString() {
return String(this.val).padStart(15, '0');
}
}
const objectList = [
new NumericSortable(3),
new NumericSortable(1),
new NumericSortable(20),
]
objectList.sort()
console.log(objectList)
You may wish to account for negative numbers too.
Still, this whole approach is a bit smelly. When possible, I'd prefer a comparator instead of having to fiddle with the strings to get them to compare properly.
CodePudding user response:
From my above comment ...
"The OP needs to wrap the build-in
sort
method into an own/custom implementation ofArray.prototype.sort
. But why should one do it? Just for the sake of not writing a comparing sort-callback?"
Having said the above, I herby nevertheless provide an implementation of the above mentioned approach just in order to prove to the OP that it's manageable (after all it is exactly what the OP did ask for), but also to show to the audience that the effort of doing so is much greater than other already provided suggestions / solutions.
class NumericSortable {
constructor(newVal) {
this.val = newVal;
}
compareTo(other) {
return this.val - other.val;
}
}
const objectList = [
new NumericSortable(3),
new NumericSortable(1),
new NumericSortable(20),
];
objectList
// - sorting by overwritten custom `sort` function with
// an already build-in `compareTo` based custom compare
// function and no additionally passed compare function.
.sort();
console.log({ objectList });
objectList
// - reverse sorting by overwritten custom `sort` function
// with an additionally passed compare function.
.sort((a, b) => (-1 * a.compareTo(b)));
console.log({ objectList });
console.log(
'... simple sort-implementation defaul-test ...\n[1,4,9,0,6,3].sort() ...',
[1,4,9,0,6,3].sort()
);
console.log(
'... simple sort-implementation defaul-test ...\n["foo", "biz", "baz", "bar"].sort() ...',
["foo", "biz", "baz", "bar"].sort()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
(function (arrProto) {
// save the native `sort` reference.
const coreApiSort = arrProto.sort;
// type detection helper.
function isFunction(value) {
return (
'function' === typeof value &&
'function' === typeof value.call &&
'function' === typeof value.apply
);
}
// different comparison helper functionality.
function defaultCompare(a, b) {
return (a < b && -1) || (a > b && 1) || 0;
}
function localeCompare(a, b) {
return a?.localeCompare?.(b) ?? defaultCompare(a, b);
}
function customDefaultCompare(a, b) {
const isCustomComparableA = isFunction(a.compareTo);
const isCustomComparableB = isFunction(b.compareTo);
return (isCustomComparableA && isCustomComparableB)
? a.compareTo(b)
: localeCompare(a, b);
}
// the new `sort` functionality.
function customSort(customCompare) {
return coreApiSort
// - (kind of "super") delegation to the
// before saved native `sort` reference ...
.call(this, (
// ... by using a cascade of different
// comparison functionality.
isFunction(customCompare)
&& customCompare
|| customDefaultCompare
));
}
Object
// - overwrite the Array prototype's native `sort`
// method with the newly implemented custom `sort`.
.defineProperty(arrProto, 'sort', {
value: customSort,
});
}(Array.prototype));
</script>