It's not readily apparent from the documentation how to structure a sort with secondary sort criteria. For example, I usually do a secondary sort by name. I can make it work by calling the secondary compare function inside the primary function, but that seems to defeat the Ramda philosophy.
do you have some example code?
Yes. This is the result of refactoring some existing code to use Ramda. The recency sort puts items with the most recent errors first. Items with no recorded error go at the end of the list. For items with the same timestamp or no timestamp, the sort is by name.
var nameLowercase = R.pipe(R.prop('Name'), R.toLower);
var compareByName = function (a, b) {
return nameLowercase(a) < nameLowercase(b);
}
var compareByRecency = R.comparator(function (a, b) {
if (!a.LastErrorTimestamp && !b.LastErrorTimestamp) return compareByName(a, b);
if (!a.LastErrorTimestamp) return false; // 'b' has an error and 'a' doesn't
if (!b.LastErrorTimestamp) return true; // 'a' has an error and 'b' doesn't
if (a.LastErrorTimestamp == b.LastErrorTimestamp) { return compareByName(a, b); }
return a.LastErrorTimestamp > b.LastErrorTimestamp;
});
var sort = {
"alpha": {
label: "Alpha",
fn: R.sortBy(nameLowercase)
},
"recency": {
label: "Recent Errors First",
fn: R.sort(compareByRecency)
}
}
Somehow I missed this issue when it came up.
Do you have a suggested API, even one that breaks backward compatibility?
Couple options here were discussed heavily for underscore and lodash
Also see https://lodash.com/docs#sortByAll
In ramdas case I think I'd suggest 2
One possibility I see is a three-function API:
R.sortByAll([
R.ascend(R.prop('name')),
R.descend(R.prop('age'))
], people)
My idea was that ascend and descend returned comparators:
ascend :: (Obj -> String) -> Obj -> Obj -> -1 | 0 | 1
descend :: (Obj -> String) -> Obj -> Obj -> -1 | 0 | 1
sortByAll :: [(Obj -> String) -> Obj -> Obj -> -1 | 0 | 1] ->[Obj] -> [Obj] (sorted)
Or maybe it would be better if sortByAll also returned a comparator, and it was used like this:
R.sort(R.sortByAll([
R.ascend(R.prop('name')),
R.descend(R.prop('age'))
]), people)
Some thoughts, anyway.
@megawac: I don't particularly like any of those APIs.
https://github.com/jashkenas/underscore/pull/1751 is the most interesting, but I would prefer something that could be composed out of pieces than something that tries to do it all in one shot like this. The semver use-case is very nice, though.
The other two end up based on strings, and although I'm guessing that sorting on properties is the most common way people would want to sort, I would not want to be so restrictive. And while Underscore and lodash do plenty of function-or-string processing, that's an anathema to Ramda.
Didn't we have this already? You can use R.or to compose comparators. That was also the reason for #442
Thats not sufficient for multiple fields which can be truthy
@megawac: You would or the comparators, which by definition return 0 only when they see the two values as equal. So this works fine, but...
@bergus: or is binary. We don't have a version that works on a list of functions. So somehow it would have to be folded. I'm not sure of the best way to do so, but obviously it could be done.
@CrossEye Yeah, for a list of functions you'd use a fold, however I think that is quite trivial:
var sortByAll = R.reduceRight(R.or, function(){return 0}) // foldr1 would've been better imo
I would think that should suffice, you very seldomly have more than 2 or 3 comparison functions to chain.
Whoops, didn't realize you were suggesting to use sort instead of sortBy. Anyway I'd love an API like:
var set = [{a: 1, b: 2}, {a: 3, b: 1}, {a: 2, b: 2}]
sortByAll([prop('a'), prop('b')], set)
// => [{"a":1,"b":5},{"a":2,"b":2},{"a":2,"b":3}]
I dislike reduce and or logic because it (probably) will not short-circuit.
@AutoSponge: This sort of reduction (which has to be run against R.either, due to a recent renaming) would actually short-circuit.
Imagine
var test = R.reduceRight(R.either. R.always(false));
var fn = test([R.propEq('a', 1), R.propEq('b', 2)]);
fn({a: 1, b: 5}); //=> true
fn({a: 3, b: 2}); //=> true
fn({a: 6, b: 7}); //=> false
because that reduce is essentially equivalent to
var fn = R.either(R.propEq('a', 1), R.either(R.propEq('b', 2), R.always(false)));
and the implementation of either is a curried version of
function either(f, g) {
return function _either() {
return f.apply(this, arguments) || g.apply(this, arguments);
};
}
So in this case it would short-circuit. However, the result is not curried as well as we might like.
test([R.propEq('a', 1), R.propEq('b', 2)], {a:1, b:2}); //=> :: Object -> Boolean
test([R.propEq('a', 1), R.propEq('b', 2)])({a:1, b:2}); //=> true
But that's another discussion entirely.
@megawac:
The problem with that API is that it does not grant the flexibility to sort ascending or descending. It would be doable for numeric values because we could just negate. But it's not general.
That's why I suggested an API that took general comparators, similar to the on function @bergus described in that stackoverflow post. That suggestion is still more flexible, and probably better than mine above.
Closing without prejudice for lack of activity.
@CrossEye
What does this supposed to return? (It returns 0 actually, BUT why?)
let heightComp = R.comparator((a, b) => a.height < b.heigth)
let ageComp = R.comparator((a, b) => a.age < b.age)
let a = {height: 90, age: 10}
let b = {age: 20, height: 100}
R.or(heightComp, ageComp)(a, b)
Does R.or even supposed to take functions as arguments?
R.or is a function that captures js boolean || operator. So it will return the first value if it's truthy, otherwise the second value. R.comparator takes a predicate function and turns it into a function that will return 1, 0, or -1, which is useful for sorting.
it's not clear to me why you are passing a sorting comparator to a boolean operation. As to why it returning a 0: that is because of the typo in line 1: b.heigth. Correct that, and the function returns -1
I was wondering how to write more eleganlty with ramda:
let heightComp = R.comparator((a, b) => a.height < b.heigth)
let ageComp = R.comparator((a, b) => a.age < b.age)
// items are like {age: 20, height: 100}, first sort by height, then by age:
let sortedItems = item.sort((a, b ) => heightComp(a,b) || ageComp(a,b) )
can it be done more elegantly?
The original discussion here was closed; no one pursued it. But your request makes me think of two reasonable APIs that we could have for a multiple-field sorting.
First of all, what you have above should work, with only one modification, but only because of some JS trickery. We renamed or to either in Issue 843. I believe your code would work with either, and if you folded over multiple eithers, it should work for more comparison fields. But it only works because of Javascript's conflation of 0 and false, which some abhore, and you might too. Nonetheless, it should work.
But this also makes me think that it would be a useful API to make it easy to combine these, either by replacing comparator with something that takes a _list_ of functions instead or by adding a new function (comparators? multiComparator?) which accepts a list of existing comparators and does this dirty work, either simply folding with either or more explicitly calling each function in turn and returning the first non-zero result or the last result.
Does anyone find this compelling?
:-1: from me. I'd prefer to provide the tools to build such a thing rather than provide it directly.
I'd prefer to provide the tools to build such a thing rather than provide it directly.
Yeah, but you've already made it clear you're on the less-is-more side! :smile:
@CrossEye
I believe your code would work with either, and if you folded over multiple eithers, it should work for more comparison fields. But it only works because of Javascript's conflation of 0 and false
Right I found out before that eigher works didn't write about it. The problem that it works only in JS, but can not be used it in TypeScript as either requires boolean predicates.
Do you think this can be solved by allowing either predicates to be non boolean?
The original discussion here was closed; no one pursued it. But your request makes me think of two reasonable APIs that we could have for a multiple-field sorting.
@CrossEye, I remember struggling a lot to find a decent API too and I'm still not satisfied about the decisions I made; anyway here's what I came up with at the time.
In a nutshell I decided to have two functions sorter and sorterDesc to build sorting criteria, a Sorter type, to be passed to the sorting function. The Sorter type has two members: a boolean indicating whether the intended sorting is descending and a compare function (more on this later).
The sorting function then accepts either multiple simple "reader" functions or multiple Sorters. Some examples here.
I let the easiness of use win as, for starters, I dislike having optional arguments in the sorter function and having the sort functions accepting multiple types, but I think that the result is quite clean.
Having:
var _ = require("lamb");
You can:
var weights = ["2 Kg", "10 Kg", "1 Kg", "7 Kg"];
var sortAsNumbers = _.sortWith(parseFloat);
sortAsNumbers(weights) // => ["1 Kg", "2 Kg", "7 Kg", "10 Kg"]
Or:
var persons = [
{"name": "John"},
{"name": "Mario"},
{"name": "James"},
{"name": "Jane"}
];
var sortByNameDesc = _.sortWith(_.sorterDesc(_.getKey("name")));
sortByNameDesc(persons) // => [{"name":"Mario"}, {"name":"John"}, {"name":"Jane"}, {"name":"James"}]
Having a compare member in the Sorter type allows to use existing interfaces:
var chars = ["a", "è", "à ", "é", "c", "b", "e"];
var sortAsItalian = _.sortWith(new Intl.Collator("it"));
sortAsItalian(chars) // => ["a", "à ", "b", "c", "e", "é", "è"]
Having a sorter and a sorterDesc also tackles a very little thing that has always bothered me.
If the sorting function is agnostic about the sort order, when you reverse a "stably-ascending-sorted" array you won't get the same result you would have by performing a descending sort.
Note to self: remember that in your notes this could have been material for a blog post to start promoting your work and instead you are writing in the issue tracker of an already well-established library.
I think this issue should be re-opened.
I've read through the thread - and while there are some really interesting API suggestions - I'd like a simple implementation similar to Lodash.
Lodash supports an array of iteratees to sort by: https://lodash.com/docs/4.16.4#sortBy
I have a performant backwards compatabile implementation in my fork:
module.exports = _curry2(function sortBy(fns, list) {
if (_isFunction(fns)) {
fns = [fns];
}
return _slice(list).sort(function(a, b) {
var aa, bb
var i = 0
while (aa === bb && i < fns.length) {
aa = fns[i](a)
bb = fns[i](b)
i += 1
}
return aa < bb ? -1 : aa > bb ? 1 : 0;
});
});
This allows sorting by multiple fields:
R.sortBy([R.prop('score'), R.prop('title')], albums)
or
R.sortBy([
R.prop('genre'),
R.prop('score'),
R.prop('title')
], albums);
Keys with numerical values can be made "descending" with R.negate
R.sortBy([
R.compose(R.negate, R.prop('score')),
R.prop('title')
], albums);
This may not be the perfect API. But I think it makes sortBy a lot more useful in many real world situations.
cc: @CrossEye
I think I'd really prefer an API like
R.sortWith([
R.ascend(R.prop('genre')),
R.descend(R.prop('score')),
R.ascend(R.prop('title'))
], albums);
And if someone wanted to add their own gloss that used, say 'genre, -score, title', more power to them.
@CrossEye I don't think that ascend/descend API is feasible (though it looks pretty :))
Perhaps
R.sortByMultiple([
[R.prop('genre'), R.ascend],
[R.prop('score'), R.descend]
])
@CrossEye @megawac I like the sortWith proposal from @CrossEye.
If I understand correctly then R.ascend would be an alias of R.comparator.
The code could look like this:
function ascend(fn) {
return function(a, b) {
var aa = fn(a)
var bb = fn(b)
return aa < bb ? -1 : aa > bb ? 1 : 0;
}
}
function descend(fn) {
return function(a, b) {
var aa = fn(a)
var bb = fn(b)
return aa > bb ? -1 : aa < bb ? 1 : 0;
}
}
function sortWith(comparators, list) {
return _slice(list).sort(function(a, b) {
var result = 0
var i = 0
while (result === 0 && i < comparators.length) {
result = comparators[i](a, b)
i += 1
}
return result;
});
}
@megawac what about the proposed API isn't feasible?
Its also worth pointing out that if we added this API then we may have some duplication, e.g.
R.ascend === R.comparator
R.sort(comparator, list) === R.sortWith([comparator], list)
R.sortBy(fn, list) === R.sort(R.ascend(fn), list)
But before we get there, we probably need to decide on an API. As I mentioned in my earlier I think this is an important addition to the API. In many cases where sorting is required - it will be by more than one factor.
@davidgtonge:
Yes, I would probably favor deprecating comparator in favor of ascend.
I could also see deprecating sort, although really I'd prefer to use that name for sortWith, we couldn't do that in a single step without risking too much breakage.
@davidgtonge:
I'm having some connectivity troubles at home (for the umpteenth time; some day I'll pony up the big bucks to run cable lines the 400 feet from the road to my house.) When I can get things working, I will create a PR for this, unless someone beats me to it. (Hint, hint! :smile:)
Ah my mistake, I thought R.ascend/R.descend were taking a single element
(ala sortBy)
On Wed, Oct 19, 2016 at 9:28 AM, Scott Sauyet [email protected]
wrote:
@davidgtonge https://github.com/davidgtonge:
I'm having some connectivity troubles at home (for the umpteenth time;
some day I'll pony up the big bucks to run cable lines the 400 feet from
the road to my house.) When I can get things working, I will create a PR
for this, unless someone beats me to it. (Hint, hint! 😄)—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/ramda/ramda/issues/994#issuecomment-254812053, or mute
the thread
https://github.com/notifications/unsubscribe-auth/ADUIEOvUP3lE0Qwsa8YdgsKF-nNUIl-3ks5q1hrmgaJpZM4D4gNq
.
@CrossEye understood re connectivity :-)
I'll see if I can beat you to it with a PR
Please see the above pull request for an example implementation.
I misunderstood the current comparator method. It actually has a different purpose from ascend.
I misunderstood the current
comparatormethod. It actually has a different purpose fromascend.
Damn, I should really remember that. I'm quite sure I wrote both!
@CrossEye @davidgtonge @megawac how would you guys dynamically compose this functions (ascend, descend, ...) together?
Let's assume there were a table and by clicking on sorting buttons the user would generate such a definition of the sorting:
[{prop: "score", reverse: true}, {prop: "title"}]
or just
[{prop: "title"}]
Something like this would probably do it:
const toSorter = compose(sortWith, map(
dir => (dir.reverse ? descend : ascend)(prop(prop('prop', dir)))
))
const directions = [{prop: "score", reverse: true}, {prop: "title"}]
const tableSorter = toSorter(directions)
tableSorter([
{score: 35, title: 'def'},
{score: 22, title: 'ghi'},
{score: 49, title: 'jkl'},
{score: 35, title: 'abc'},
]) //=> [{score: 49, title: 'jkl'}, {s: 35, t: 'abc'}, {s: 35, t: 'def'}, {s: 22, t: 'ghi'}]
This looks funny, but makes sense: prop(prop('prop'...
Yes, this is the whole point. One would always find a workaround but this thread is also about having a nice API and one would mostly need sortWith to sort a list dynamically. If you looked at toSorter separately you would struggle to understand what the function does. At least I think that there definitely is space for improvement.
I guess I'm a little confused what you're asking for.
I wrote that quick solution as a way to convert your specific format to a useful sort function. It certainly is not how I'd suggest writing a one-off sorter for such data. For that, I would simply write
const tableSorter = sortWith([
descend(prop('score')),
ascend(prop('title'))
])
which to me looks quite readable.
Ramda's sorting API offers five functions:
sort is an immutable take on the built-in sorting, taking a comparator and a list and returning a sorted version of that list.sortWith extends that to take a list of comparators, which are checked in turn for the first non-zero response, sortBy allows you to sort on representations of your objects in some ordered type (Number, String, Date) by accepting a function that returns such a representation and a list.ascend and descend create comparators in the same way as sortBy, but don't do any sorting themselves. The results of these can be fed to sort, sortWith, or Array.prototype.sort.These allow you to build up all manner of sorting functions. But Ramda is intentionally a fairly low-level library: offering you useful tools, but leaving final assembly of them to the user.
So I doubt Ramda would ever be interested in directly supporting the sort of API of tableSorter above. It's too specific, and not flexible enough. If you're looking for that directly from Ramda, I'm guessing you're out of luck. But I think it's easy enough to write something like that on top of the functions Ramda offers.
All I wanted is to adjust the function toSorter and the input directions in order to make it more readable.
I don't know what would count as more readable to you. This has all the steps really spelled out:
const toSorter = (sortCriteria) => {
const sortFunctions = map(criterion => {
const sortField = prop('prop', criterion)
const sortDescending = propOr(false, 'reverse', criterion)
if (sortDescending) {
return descend(prop(sortField))
}
return ascend(prop(sortField))
}, sortCriteria)
return sortWith(sortFunctions)
}
It's much the same idea, but I would prefer this version:
const toSorter = (sortCriteria) => {
const sortFunctions = map(criterion => {
const sortField = prop('prop', criterion)
const sortDescending = propOr(false, 'reverse', criterion)
return sortDescending ? descend(prop(sortField)) : ascend(prop(sortField))
}, sortCriteria)
return sortWith(sortFunctions)
}
Each of these should have the same behavior as the one I listed above.
But I really have no idea if that's what you're looking for. I don't know how you would update the input to this function. I was under the impression that these were fixed.
Thank you @CrossEye. Please do not take it personally. I wanted to involve the community and see how different people would approach this task.
@1024gs:
I didn't take it personally. It just wasn't -- and still isn't -- clear to me what you're looking for. I can't tell if what I just supplied helps or not.
Most helpful comment
I guess I'm a little confused what you're asking for.
I wrote that quick solution as a way to convert your specific format to a useful sort function. It certainly is not how I'd suggest writing a one-off sorter for such data. For that, I would simply write
which to me looks quite readable.
Ramda's sorting API offers five functions:
sortis an immutable take on the built-in sorting, taking a comparator and a list and returning a sorted version of that list.sortWithextends that to take a list of comparators, which are checked in turn for the first non-zero response,sortByallows you to sort on representations of your objects in some ordered type (Number,String,Date) by accepting a function that returns such a representation and a list.ascendanddescendcreate comparators in the same way assortBy, but don't do any sorting themselves. The results of these can be fed tosort,sortWith, orArray.prototype.sort.These allow you to build up all manner of sorting functions. But Ramda is intentionally a fairly low-level library: offering you useful tools, but leaving final assembly of them to the user.
So I doubt Ramda would ever be interested in directly supporting the sort of API of
tableSorterabove. It's too specific, and not flexible enough. If you're looking for that directly from Ramda, I'm guessing you're out of luck. But I think it's easy enough to write something like that on top of the functions Ramda offers.