Agzam Agzam - 14 days ago 3
Javascript Question

Natural sort, array of objects, multiple columns, reverse, etc

I desperately need to implement client side sorting that emulates sorting through our tastypie api, which can take multiple fields and return sorted data. So if for example I have data like:

arr = [
{ name: 'Foo LLC', budget: 3500, number_of_reqs: 1040 },
{ name: '22nd Amendment', budget: 1500, number_of_reqs: 2000 },
{ name: 'STS 10', budget: 50000, number_of_reqs: 500 },
...
etc.
]


and given columns to sort e.g.:
['name', '-number_of_reqs']
it should sort by
name
(ascending) and
number_of_reqs
(descending). I can't get my head around this,
first of all it has to be "natural sort", it supposed to be fairly easy to get if we're talking about sorting a single column, but I need to be able to sort in multiple.

Also I'm not sure why I'm getting different results (from the way how api does it) when using lodash's
_.sortBy
? Is
_.sortBy
not "natural" or it's our api broken?

Also I was looking for an elegant solution. Just recently started using Ramdajs, it's so freaking awesome. I bet it would be easier to build sorting I need using that? I've tried, still can't get it right. Little help?

upd:

I found this and using it with Ramda like this:

fn = R.compose(R.sort(naturalSort), R.pluck("name"))
fn(arr)


seems to work for flat array, yet I still need to find a way to apply it for multiple fields in my array

Answer
fn = R.compose(R.sort(naturalSort), R.pluck("name"))

seems to be working

Really? I would expect that to return a sorted array of names, not sort an array of objects by their name property.

Using sortBy unfortunately doesn't let us supply a custom comparison function (required for natural sort), and combining multiple columns in a single value that compares consistently might be possible but is cumbersome.

I still don't know how to do it for multiple fields

Functional programming can do a lot here, unfortunately Ramda isn't really equipped with useful functions for comparators (except R.comparator). We need three additional helpers:

  • on (like the one from Haskell), which takes an a -> b transformation and a b -> b -> Number comparator function to yield a comparator on two as. We can create it with Ramda like this:

    var on = R.curry(function(map, cmp) {
        return R.useWith(cmp, map, map);
        return R.useWith(cmp, [map, map]); // since Ramda >0.18 
    });
    
  • or - just like ||, but on numbers not limited to booleans like R.or. This can be used to chain two comparators together, with the second only being invoked if the first yields 0 (equality). Alternatively, a library like thenBy could be used for this. But let's define it ourselves:

    var or = R.curry(function(fst, snd, a, b) {
        return fst(a, b) || snd(a, b);
    });
    
  • negate - a function that inverses a comparison:

    function negate(cmp) {
        return R.compose(R.multiply(-1), cmp);
    }
    

Now, equipped with these we only need our comparison functions (that natural sort is an adapted version of the one you found, see also Sort Array Elements (string with numbers), natural sort for more):

var NUMBER_GROUPS = /(-?\d*\.?\d+)/g;
function naturalCompare(a, b) {
    var aa = String(a).split(NUMBER_GROUPS),
        bb = String(b).split(NUMBER_GROUPS),
        min = Math.min(aa.length, bb.length);

    for (var i = 0; i < min; i+=2) {
        var x = aa[i].toLowerCase(),
            y = bb[i].toLowerCase();
        if (x < y) return -1;
        if (x > y) return 1;
        var z = parseFloat(a[i+1]) - parseFloat(b[i+1]);
        if (z != 0) return z;
    }
    return bb.length - aa.length;
}
function stringCompare(a, b) {
    a = String(a); b = String(b);
    return +(a>b)||-(a<b);
}
function numberCompare(a, b) {
    return a-b;
}

And now we can compose exactly the comparison on objects that you want:

fn = R.sort(or(on(R.prop("name"), naturalCompare),
               on(R.prop("number_of_reqs"), negate(numberCompare))));
fn(arr)