A key parameter for Array.prototype.sort

It is useful to provide a key parameter or a mapper function as the sort key while sorting.
Popular libraries like Lodash, Underscore and Ramda already provide this method (sortBy).

array.sort((a, b) => (a.key > b.key) - (a.key < b.key)) may be enough in certain cases, but when the mapper does expensive calculation, one must do something like array.map(value => ({ key: someExpensiveCalculation(value), value })).sort((a, b) => (a.key > b.key) - (a.key < b.key)).map(({ value }) => value). This is verbose and inefficient. This can be solved by appending an optional argument to Array.prototype.sort, which the implementer can optimize further.

It roughly does the following:

function IsCallable(arg) {
	try {
		[].forEach(arg);
		return true;
	} catch {
		return false;
	}
};
const oldSort = Array.prototype.sort;
Array.prototype.sort = function(comparefn, keyOrMapfn) {
	if (typeof keyOrMapfn === "undefined")
		return oldSort.call(this, comparefn);
	if (!IsCallable(comparefn) && typeof comparefn !== "undefined")
		throw new TypeError("Array.prototype.sort requires the comparator argument to be a function or undefined");

	if (IsCallable(keyOrMapfn))
		for (let i = 0; i < this.length; i++)
			this[i] = { key: keyOrMapfn(this[i], this), value: this[i] };
	else
		for (let i = 0; i < this.length; i++)
			this[i] = { key: this[i][keyOrMapfn], value: this[i] };

	if (typeof comparefn === "undefined")
		oldSort.call(this, (x, y) => (String(x.key) > String(y.key)) - (String(x.key) < String(y.key)));
	else
		// Assert: IsCallable(keyOrMapfn) is true.
		oldSort.call(this, (x, y) => comparefn(x.key, y.key));

	for (let i = 0; i < this.length; i++)
		this[i] = this[i].value;
	return this;
};

Yeah, I would like to have this functionality.

Unfortunately I don't know if it will be web-compatible to add an additional parameter; that breaks things sometimes. So it might end up being a new sortBy method instead.

And I think probably it would only take a function, not also a key; sortBy(x => x.y) isn't that much harder to write than sortBy('y'), and is much clearer. (When we added groupBy - now renamed group - we only allowed a function, for the same reason.)

4 Likes

I like the idea, but I would prefer if the {key, value} objects were not leaked. In your sample implementation, they can be accessed when calling sortBy on a proxy or when accessing the array elements during the keyOrMapFn/compareFn calls. And they leave a mess in the array if any of those function calls throws an exception.

I don't think it's that inefficient. You cannot really avoid allocating extra space at least for the "key" values, and probably not avoid allocating some extra housekeeping space around each key (whether an index or just a copy of the value) either.

Which begs the question: should sortBy even reorder the array in-place, or should it be a sortedBy method that returns a new array?

2 Likes