2

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

iLemming
  • 34,477
  • 60
  • 195
  • 309

5 Answers5

8
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++) {
        var x = aa[i].toLowerCase(),
            y = bb[i].toLowerCase();
        if (x < y) return -1;
        if (x > y) return 1;
        i++;
        if (i >= min) break;
        var z = parseFloat(aa[i]) - parseFloat(bb[i]);
        if (z != 0) return z;
    }
    return aa.length - bb.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)
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Wow... this is marvelous answer, explains well beyond what I asked for. Thank you very much. Indeed - functional way sometimes feels not very straightforward until you learn to see it. – iLemming Oct 12 '14 at 18:16
  • 1
    @Bergi: Note that Ramda's `or` does work as you need. The documentation obviously needs an update. Approximately, `R.or(f, g) => function() {return !!(f.apply(this, arguments) || g.apply(this, arguments));}` – Scott Sauyet Oct 12 '14 at 21:58
  • @ScottSauyet: Exactly that `!!` before the expression is the problem? My denial is not about arity or signature or so, but because of that `Boolean` cast. – Bergi Oct 12 '14 at 22:15
  • @Bergi: D'oh, of course. I need to read more closely! :-). It works well in the style comparators I use, because I build them not out of functions which return positive or negative values but instead on predicates that report whether the first argument is smaller than the second. Nice answer, in any case. – Scott Sauyet Oct 12 '14 at 22:26
  • @Bergi: This feedback made us realize that there was simply no reason for the boolean casts in the and/or functions. They're removed in HEAD and won't be in the next release. – Scott Sauyet Oct 15 '14 at 23:53
  • This is really nice @Bergi :) One small update is to the `on` function Since 0.18.0 of ramda the signature of this function has changed https://github.com/ramda/ramda/commit/fecd002ff497c3a3b00fa630713d42c6fa19afe0 – Matt Stevens Nov 22 '16 at 16:12
  • @Bergi noticed that the naturalCompare is not quite right. A correct version can be seen in this jsBin http://jsbin.com/torolipovi/edit?js,console – Matt Stevens Nov 22 '16 at 18:38
  • @MattStevens Oh, so many mistakes! Thanks for the proper testing that I should've done myself. I've fixed them now. – Bergi Nov 22 '16 at 19:02
1

I think this works.

var 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: 5000 },
  { name: 'STS 10',         budget: 50000, number_of_reqs: 500  }
];

var columns = ['name', 'number_of_reqs'];

var NUMBER_GROUPS = /(-?\d*\.?\d+)/g;
var naturalSort = function (a, b, columnname) {
  var a_field1 = a[columnname],
      b_field1 = b[columnname],
      aa = String(a_field1).split(NUMBER_GROUPS),
      bb = String(b_field1).split(NUMBER_GROUPS),
      min = Math.min(aa.length, bb.length);

  for (var i = 0; i < min; i++) {
    var x = parseFloat(aa[i]) || aa[i].toLowerCase(),
        y = parseFloat(bb[i]) || bb[i].toLowerCase();
    if (x < y) return -1;
    else if (x > y) return 1;
  }

  return 0;
};

arr.sort(function(a, b) {
  var result;
  for (var i = 0; i < columns.length; i++) {
    result = naturalSort(a, b, columns[i]);
    if (result !== 0) return result; // or -result for decending
  }
  return 0; //If both are exactly the same
});

console.log(arr);
Winestone
  • 1,500
  • 9
  • 19
  • 1
    yeah, should work... yet not very elegant. I bet it's possible to make it more declarative with Ramda – iLemming Oct 12 '14 at 09:24
  • @Agzam just curious, what do you mean by declarative? – Winestone Oct 12 '14 at 09:25
  • instead of telling it how to do it (looping through columns, etc.), maybe it's possible to rewrite it giving more functional emphasis. And it should read better - "take this data, manipulate it this way - sort it over these columns", etc. – iLemming Oct 12 '14 at 09:34
  • Errr... seems that thing needs some polishing. Doesn't seem to be doing reverse and doesn't take into account non-string fields - numbers, nulls, etc. – iLemming Oct 12 '14 at 10:50
  • @Agzam Really? numbers don't work? It seemed to sort the two "STS 10"'s by their ``number_of_reqs`` just fine. – Winestone Oct 12 '14 at 10:53
  • @Agzam and reverse seems to be working too, pass me your code via jsFiddle or something? – Winestone Oct 12 '14 at 10:55
  • No when you pass it like this ['name', '-number_of_reqs'] it won't even compare, because object won't have a property named `-number_of_reqs`, so need to use the tail(with no dash) and reverse `a_field` and `b_field` – iLemming Oct 12 '14 at 11:02
  • @Agzam Can you guarantee that none of the objects within arr will start with a '``-``'? If so, I'll change code to detect the negative sign and sort accordingly. – Winestone Oct 12 '14 at 11:05
  • no don't worry about that. I'll take it from here. I got it. Thanks. I also need to add a few cases where a_field is null and b_field not, etc... – iLemming Oct 12 '14 at 11:13
1

Bergi's answer is useful and quite interesting, but it changes the API you requested. Here's one that creates the API you were seeking:

var multisort = (function() {
    var propLt = R.curry(function(name, a, b) {
        return a[name] < b[name];
    });
    return function(keys, objs) {
        if (arguments.length === 0) {throw new TypeError('cannot sort on nothing');}
        var fns = R.map(function(key) {
            return key.charAt(0) === "-" ? 
                R.pipe(R.comparator(propLt(R.substringFrom(1, key))), R.multiply(-1)) :
                R.comparator(propLt(key));
        }, keys);
        var sorter = function(a, b) {
            return R.reduce(function(acc, fn) {return acc || fn(a, b);}, 0, fns);
        }
        return arguments.length === 1 ? R.sort(sorter) : R.sort(sorter, objs);
    };
}());

multisort(['name', '-number_of_reqs'], arr); //=> sorted clone

It's manually curried rather than calling R.curry because a fair bit of the work is involved in creating the separate sort functions, which could then be reused if you are sorting many lists with the same set of keys. If that's not a concern, this could be simplified a bit.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • Looking more carefully at the natural compare links, this might not do. It would work fine for the small sample data, but if you wanted to sort "AB5" < "AB7" < "AB10" < "AB43" < "AB101", then this would need more work. You'd need some sort of natural compare in place of `propLt`. – Scott Sauyet Oct 12 '14 at 21:43
  • Yet this is really good food for thought. Thank you Scott. Thank you for so much awesomeness Ramda brought to my life. – iLemming Oct 13 '14 at 00:17
0

If you're willing to add another dependency to your project, @panosoft/ramda-utils comes with a compareProps function that does exactly what the original question was asking for.

So, given your original example, to sort descending by budget and then by name, you could do something like this:

var props = ["-budget", "name"];
var comparator = Ru.compareProps(props);
var sortedList = R.sort(comparator, arr);
Chris Jaynes
  • 2,868
  • 30
  • 29
-1

use the javascript native sort:

    

Array.prototype.multisort = function(columns) {
  var arr = this;
  arr.sort(function(a, b) {
    return compare(a, b, 0);
  });

  function compare(a, b, colindex) {
    if (colindex >= columns.length) return 0;

    var columnname = columns[colindex];
    var a_field1 = a[columnname];
    var b_field1 = b[columnname];
    var asc = (colindex % 2 === 0);

    if (a_field1 < b_field1) return asc ? -1 : 1;
    else if (a_field1 > b_field1) return asc ? 1 : -1;
    else return compare(a, b, colindex + 1);
  }
}



var 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: 5000    }, 
           { name: 'STS 10',        budget: 50000,number_of_reqs: 500    }];
arr.multisort(['name', 'number_of_reqs']);
if (window.console) window.console.log(arr);
gp.
  • 8,074
  • 3
  • 38
  • 39
  • what about ascending, descending? – iLemming Oct 12 '14 at 09:11
  • return result for ascending, -result for descending. You can also take a flag in compare method and negate the output for descending. – gp. Oct 12 '14 at 09:12
  • that's the most native you can get in javascript. build you logic on top. – gp. Oct 12 '14 at 09:13
  • @agzam if you replace his ``compare`` with [your link](http://devintorr.es/blog/2010/06/25/a-natural-sorting-comparator-for-javascripts-arrayprototypesort/)'s algo, it should work, hang on, let me try – Winestone Oct 12 '14 at 09:15
  • @gr I need alphanumerical comparator. See the question? Array has fields with strings with numbers in it. – iLemming Oct 12 '14 at 09:19
  • @Agzam I think javascript > and < operator compare string's in ASCII order, so it works except without the natural sort. – Winestone Oct 12 '14 at 09:24